[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: Bug Report\nabout: Report unexpected program behavior to help us improve\ntitle: \"[BUG] Your Issue Name Here\"\nlabels: bug, needs validation\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.  \nAdd any applicable logs as well; such as an `dsiprouter.log`, or `kamailio.log`, etc...\n\n**Server Info:**\n - OS: *output from* `uname -a`\n - Distro: *output from* `cat /etc/os-release`\n - dSIPRouter Version: *output from* `dsiprouter version`  \n*If not on a release version include the branch name and last commit id*\n - Kamailio Version: *output from* `kamailio -v`\n - RTPengine Version: *output from* `rtpengine -v`\n - Python Package Versions: *if applicable, include output from* `/opt/dsiprouter/venv/bin/python -m pip freeze`\n\n**Client Info:**\n - Device: *e.g. Polycom VVX 350, Lenovo Thinkpad X1, ..*\n - OS: *e.g. Windows 11, Ubuntu 22.04, ..*\n - Client Software: *e.g. Mozilla Firefox 103.0, Zoiper 5.5.13, ..*\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: dSIPRouter Community Slack\n    url: https://dsiproutercommunity.slack.com/\n    about: Community hangout where questions can be asked and answered.\n  - name: dOpenSource Official Support\n    url: https://dopensource.com/shop/\n    about: Official support and addons can be purchased here.\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/default.md",
    "content": "## Description\n\nInclude a summary of the changes and the related issue.  \nInclude relevant motivation and context.  \nList any dependencies that are required for this change.\n\n## Type of change\n\nPlease delete options that are not relevant.\n\n- [ ] Bug fix (non-breaking change which fixes an issue)\n- [ ] New feature (non-breaking change which adds functionality)\n- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)\n- [ ] This change requires a documentation update\n\n## How Has This Been Tested?\n\nDescribe the tests that you ran to verify your changes.  \nProvide instructions that we can use to reproduce your results.  \nList any relevant details for your test configuration.\n\n- [ ] Test Script Committed\n- [ ] Instructions Provided Below\n\n**Test Configuration**:\n* Hardware:\n* OS/Distro:\n* Software:\n\n## Checklist:\n\n- [ ] My code follows the Contributing guidelines of this project\n- [ ] This PR is not a duplicate of another open PR?\n- [ ] I have performed a self-review of my code\n- [ ] I have made corresponding changes to the documentation\n- [ ] I have added tests that prove my fix is effective or that my feature works\n- [ ] New and existing unit tests pass locally with my changes\n- [ ] My code passes integration testing on a live system\n"
  },
  {
    "path": ".gitignore",
    "content": "# dSIPRouter Project Specific Ignored Files\n.idea\n/resources/terraform/do/*.tfstate\n/resources/terraform/do/*.backup\n/resources/terraform/do/.terraform\n/resources/terraform/do/*.hcl\n/resources/terraform/do/terraform.tfvars\n*/__pycache__/\nvenv/\n# TODO: these following files could be generated elsewhere\n# adding them to python path where needed to cleanup project dir\ndocs/build/\ngui/modules/fusionpbx/certs/cert.key\ngui/modules/fusionpbx/certs/cert_combined.crt"
  },
  {
    "path": ".readthedocs.yml",
    "content": "# .readthedocs.yml\n# Read the Docs configuration file\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.8\"\n    # You can also specify other tool versions:\n    # nodejs: \"20\"\n    # rust: \"1.70\"\n    # golang: \"1.20\"\n\n# Build documentation in the docs/ directory with Sphinx\nsphinx:\n  configuration: docs/source/user/conf.py\n\n# Build documentation with MkDocs\n#mkdocs:\n#  configuration: mkdocs.yml\n\n# Optionally build your docs in additional formats such as PDF\nformats:\n  - pdf\n\n# Optional but recommended, declare the Python requirements\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n  install:\n    - requirements: docs/requirements.txt"
  },
  {
    "path": "CHANGELOG.md",
    "content": "[//]: # (START_SECTION HEADER)\n## CHANGELOG\n\n\n\n[//]: # (END_SECTION HEADER)\n[//]: # (START_SECTION COMMITS\n7c86d935be95d9838f45727025d442f55a8456a4\n844fdfd70c526e3e3f4b94ca91de5a74bab12213\n8261be6d3e1772d94acb1d8c8de194a0b4afbdd6\nc1b270c5efb9f1640ed0a32a7d9c0d7f96b1a02d\n58096c74533221acc325c101627b8f8a8ada1282\nc8c6ba9ded2fc109099a8fd3c9e7775ea61560c5\na252b7e6646a684a44946f48bc93f78335d559f4\n31d9a61fe5e5698a83ba460a347ca2d9d681904f\n902c53e6ea1b56ba293362fd7147f954fe5b87f0\n10cdc797dddb3053dfa574dcd10e5fe0696cffe8\n2485a964ccf7d5eab5c719d9fe1d24221e79ff41\n37fe6c03ed7e798119732367dc1896fa93d3182f\n49ba3a7b761ef8f7c71de501c66bdadeeddc70ac\n5995b5b7296506b72e5863aba07d1343acb9e113\n23b1395497f61ec9a8f60ab63dd394397e1866b6\n3007c0e8499cc95673704a420dee938dd424ef9e\ne1e287dd579ff03c0407b90d047d76e10a2ad52f\n8f427d4f46d64c4ead9c32402d56f509f63850c9\n155f1594e37d88e14dc9699cc75cba04f3d6b2f0\nababcdd35782a7234b14e9a8c9630e2127bfc52e\n73cb9b23631150873b18c39696018a4f599738a9\n303bf86be04320131b8fce613e19aa71dfd7ce75\n52e07933e0845f85c95c6c05af3e4a7b9ad3f2c2\na39ef41fa5eb058903ca03f8b923570aac7ff15a\n3dd1156097fefd98be296dc312bbc03eb3c58002\nfd1696c6f460afdac86e5e56414b975ecc3c37ec\n0717ce38b28066de5749decbc3aa65bf65051956\nb580cc323974d7aba05932392940050f14350858\n5c5eb60127bc8b176e933c39f30a60a56d08a312\n49e9c8c9f4ab16572259a178182437a5b8ef44d8\nb46787c059a00d4a8b17a957df3f696f3dad682d\ne606c4765084386cf03740c99cb8916bcc78f7f3\n17a10889ddc23eee8f9392319fee27e6401bcb70\n08ea093039491b124b09bd4b21f63fd629019afd\n9e2e812346a002b11e66ef6bd87ce72e130b198d\nf298cc3ba4118807586579090c473df2035189e5\na85ae0c02f89bcf0539c56201a58e5d01585f062\nfbd0f7c84e4cd66e7f0d694a965abc259f333683\na638bc71661c89be5881a9d776b9f73a6ad0af97\n952f0655d49ad7ab60c106b4f108945070b7c8eb\na058f7f1054f5c65c330f0424e732ce6861793bc\nf3993379b9ba18b9b4a26eba2611c03ac64b568f\nfebc0d9073bf1834e0fc13dbaf893cb9179a82d6\n01171453f8bfe861f37aceb332ada09057ad4b59\nd61ec9564deeca1999c61f0683af50bf8b74730d\ncdfda0c7319e70217b1d6f5570ba7d242e181b4b\nfe6dee1e3bb7f94a87dca23d8f78512fbd7c03b7\n43d053b938a14191b97ce44048a8751021fc6255\n68a2dd685172e2b5521f1b698456b2f0dde077b4\n4f2a9f8ad407110852092ce543461972687588df\n656fbc1c50d503d7f14d13674da597ebc4e1f117\n186f48f0d295f28527564a3dc3103cab8993f65c\n2b76d501e66b0b22acc911e944f24ca09588cbd7\n9b1cab3e12f978ae79c7317b530881a1c6cbe8ae\n6fc2548389b0c6672f9141baf4e53e71bc297184\n3f73914f1225b2836cab1f13e366582a61c183b4\n6265abc736c8c08348a951da48b643bd67034b0e\nb57a2a3670df1bf0005a4992f2af073e4d85738a\na977b58eee52a00f10ea32f7e9a05f2222037a3d\nf47832ae5061628cef09e3198b7a33525f2cd0d9\nf2a522d40e62c96bd1e55de7e098cfda9c98fbfe\n92176ca0a908d5a778cb9a1081d7bd3a80df719c\n50380c10d0fbeb07295d13cde20aa6ac0a221def\ne6cbcdbf74426a74a890a97cd5a1ef93813eda99\n14712ee1e39628bc178d8591e22d8fc6b355a21d\n7a264c878a5440330ee03bccc501ea83c4fa33bb\n2ab2a009610830601e70476cea99ec7d867ae181\n36eb14bf4f109b1db9e11497f036f7768669a8bd\nbda6aadd069dbdf2027b782e81a83af66e7e890a\n5d95510edf523ea0be6d417ff69360f77eb610e6\n223522fe8e8011377b663409529bf04eb74cf1da\n96e17cb5f60b07b90c0f62639b5239e7e1e08873\nc9131b49277ad01931aa7fd19e56bf85a4b37e79\na106f645bebbd0f39f073f2dad55bf8ad5733463\n4f23a2e1158d415856c027cc516069d4be7b0b2f\n4859f366f289a1e8aa03d19fcb902cedba2129d7\ne8f8f8c07902241cd86428ab860dde834ba2977a\n8c3d1ec2aec7952602f6c19af5d94648d272257a\nce9efc694149c243243e366d9303f12a6ff21090\na6690af4543a9458baf971404c7d21d03ec19bb3\na89c77066094932562cb1aee1ade444c1335b02b\n16e7d16b9aaf5a230f1b4faa2ad032127befbe6e\ndb26338c391f2b39463967f03ec2f123cc4c1f56\nce3312c9020a8c1e208143ba51ae6ab19fb75fec\nc7d98a3c26f298b4416fe27bb9ed6145a3eed3c3\n77b0d03bf0c0ca0dcc9be4730aff129aaf2a0fca\ndd4564e01ccc4d5bd3ef101a1d1abf368771ce1e\n02207a75314811582d3895d35c2eabab968422e7\n2302d89fd3752bbc6231f9e8131105a04a9f14ac\nf429ed2052552e1d8ea2c8182ce4825b1432a8e0\n40eac0dbad1c95998ca0b5346187b47abceb1f95\n5a5f7558d67477c8b69dd1e23ac4f2ebc65ea2a3\n84d1f13d647cc2d7eeab523587435e9fb36b1688\n1c0b86cc905d6c6a1aa4a11b89eb9361fdd97907\n2fab7686d38b88730594a8cd57d25eaf88076cbb\ne1c2bd0628d34c0e32c48d7645810d29d9296718\ndb746a1ef367383edbc0896f52f31ef82f3e0439\nad5f92b9896911a020caa3971dd9b0a01ea56164\n9c19f242e8a7ad0196f88a2078e0be061ca16cbf\n4ce5a4a0d4125defd1fe11046d72efe8e10b42f0\ndb53233a086170ff69b829ab1a203ce78d404a36\n582aa4c755a129dbaf30fa951232bcfc6b174c48\n029833e37ba7c808c64a2062836edfffb0bf87af\n96ae8e0ad6b59ffcfa5eb5e22071ef01cda3b386\n4c603fe4465e0777bc9ab83f138b24bdc790857c\n9e3fba57211e2d32b1d4cc2abca9d0212e23cc6f\n4fadad93e3aa6411e567a424759a451d053f9a58\nb4c5011cba0f5d39566336fd926158c4c0bcfa3a\n9d1a14335d324a834625d1e922c8de2d4057b649\n103bc6e4a3c22b37c1f52bdfa79e48d90dd86e2e\nba663bd3cc3c16f7c33c8f7977e1b4d5051efc0d\n41a1a512aba83310e57d6c2c4d6e19a35a633900\n325b4c18db320c491021681490d1bc364524b000\n5edf7567a52854a403c21b66c785c4a529b8dd7e\n472c343c957240dcebe500f405e3d818fd65f6fc\n3400b7d680e252f1b193294c24385740e0ea4cf9\n6bfcbbe3ccc56fef2378e7d1305751318ac508e6\n8a9eefe4814db0d20e6d5030f6e9865f47711513\n5737d1f33c99de90e0ef9188ca6c840292896567\ncced1d2cd835ca6d546a22b78dcc6d46f2c1a874\n357eca71bf3767d10f36b6e2db1249048a399c54\ncd0db1a1f6bf45e09e31ff56e6e3541bd617ea70\n8b79cd5e57ee370fe39d8d2234b995c7a0c3667f\n435b3cadb5cdc837343d12c04199ef58ecc88123\nec09a74859312f30a48ec11dd57ed682f4da3868\nedc1122b046b2948c5ccff28940693c0dcae2821\ndeb523e80b4f468c47b0f56e89480b8d19b2c899\n2e52b3173dc0bfec031da450adc0bc06ff51dab2\n2d2491605e0912a54a66a6e141b5f093120f6ee6\nabc7359613387ce22dc82a1f8bf9a336835f505e\nfe0127248147c98388771c02900afa45c17eb565\nf8aecea6bd3c217b2073234c9f6ba3fae4c4e5f8\n4bae4f3d0153a244c1441213327a83884547ea96\nf833e8374efecbc4a9319251db9941f68f331617\n17084e4bb069807f065f847086b5b002ff4d54ed\nfea919d0b4f5184c4ae2d319ed39cfdfb1e3a1dd\n351dc8ef45d4f09072584b1b906bc0b04973df8d\nbda08eb49639f20e85563131cc2356770eaea61f\n3408b9a6c8d68a473bb2b7a53ec31ceff6900f91\n014c30de6ac92ab9ca51e758b74c8563a875d98b\nb46462ea5d8160b69a7999de6116f0b2aa0c42ed\n1c46e8cd1d1cf82cced662b3e6af94b667c77ac9\ndcf2b437e94bf987e60d1c16bf85b331de1ff592\nf0ac44f8041b10a3675f5a2accfe29199a795990\n482bd41f6d5bc599e632c76095c3f0ea09ee7712\n559fec30e82c5553b483acfe7c59d801d65cfd5d\nd2f71046f6eb8b7dabc13cbb88b74d530d686367\n1b8064c9760fd98037e0471373fdc6288f8c2910\n313906939ddd7d64a3ef9562c3e86c7101bdff8c\n1d7bc2d6a526458a943b79cc23b49169cdf4974e\n9b5afa06020b84b598576c56edc09f3fa98be14c\n3da760ed578537f9f39475bb6404d3931af22dfc\n92eefb44fc4b1cb17ad9908a77b7587f5a408baa\ne19f0de2ab42f6630ca65aff48a7cea1b2f656c8\n9560aab94b63edc79181c8e973a226cffaf89aa3\n59a745237cd9586f8dc2224a423365724a90ad21\n8bc13858f70633ec2ec2ade2b878759a3d6715b0\n6c73f8c46eb1477fcbaee6014e2a861e80f04c81\n073aae22bea1191cd548d7ee676d571df4487891\n0a9b0322870cd277aa41e0abb7f95ed026909d4f\n2f42fe9d97523d96b226f74ad9c25f60ff3a992f\n4ef9068ca819def5eb7df6abdd9f799c4baaedd4\na5513488ce466250a812748d7d757af95acdff8e\na10cce22b9a632e0b4a1540b0fc06ed42a4412a3\n52cfbb1f29bb83db1dc2f4d4454d1da048f2a467\nf1d9e0098908132570596a1fa9fd2ceb17978d14\n1ab82314fab1f8521df7b2ec8a9954d92548958a\nee088949886ce13d66ddea732d23b51b90ee4ffb\nc3eb4d56b294866513087636aa96f6c3a748982a\nf1800b77e276cc28469c6f11bf2cd315271d6d17\na183a8c60c6774bb8823b8f73be8a414066ab7fb\n7309bf1dffba660067e70977ec9f636eee86a510\n8a2675034dd8293dcb3b1aa0efdb3641f5c1005b\n5ec677df29e26d2d78251e8d9582202d50665ac5\n8759e869034f9410f2622fed0ab40e5dbb107d97\n75b3c327bbb19116894e0703ca417b8b1654cb4a\n426a4c2b0a5bacc8f4727b2f35127d94237cac40\n6b3ea5bb772a6e0d75eb91368d9a0f0a1e15c9e6\nf13aec39adf65e0d750820e4f7851da645eae6e7\n5a824fe4be28edbef8103b3cd1a8013f91e25492\n9eb470f8a4dd79a2562395324c7d2e60583315bc\n543e2f3a85e2256879d22f7651cd924b6fc47a95\n4e748d9225c0471535e529d428247ec110caaf0d\n05d8a3db546cddf462cbe5ba7072f940acfededd\n7816850f83fad98085c3fec4fe1470c014f6b244\n39fd67f26e204be7f0389d16f7f51bb51411bdda\na0e020d98ca138243813a25931ce2bfd44f856f7\n87a29f49f7e0ef6e456aadfeb58d4c2155665999\n53e56fa76503e703d7fa27170ed403927d33d741\nca380a7b55e3dcb5f1baf08c70dd734b917188d9\nca0d727353041c1fd1ac7a1054dcdeb9c927cb11\n0bf4420aabbefb4d6d2e9e32a5f4ee641edfe683\n2e00a284992b45dbf5d75c4de87f4704fd78395c\n3bee7a4c7e837e0e8a70acf18a65964d6edf0600\nb7a5a67c73f390e26d5d6d912a1a48d41636382c\nd77f068ca08ee2c842a62cbcb58d7038bd138534\n83d6af70e487b84ee55c3b9522410fe30db23ad8\n575cceede455b4809e2da145592ca78b5cbf6156\nfc2aa26bd5fc83eb74ab86f81dc9c630a777f989\nc0a47655b5a2b5739121efbc9a0e03c059a291b4\n10523849ac49dfa56c99eadab438d1f576666966\n7cf4ac77b3693c06d94f6a3ec3c96e44acbaf713\n44ef8c01b6c02603d423425141e0557cf7602a3b\nc1f16fd78645972af29cede0046df2412d0f1aac\n5cc5742704f45c6cffee27f1dda3dad5a4fd2da7\n27dd5e3a87f3bac5942a454cd13dce5f5f60cfe4\n32face946d43cbc0df179550f6d612786808230a\n6b6e1b6dc489867566e398ca50913ef4be733c59\n8804887bf59e6317ccea7e4e64d95d092825d9fc\nbca5ab613fbb009b4b835e2903290888ba7f3574\n17f6768ba9b4d3f58f20a2239f88b8728c67eaef\n1d13139952f73f9ff9dfca79789c0ad1e36a2577\n3882f40b0177622d09e4b1267a007c3911cae25d\nbcdcf178a97f27ddcb129d830f794c5a3fe0c717\n692157482f7971a2ce2c90fe75184b3e51f23734\nb1f784d5484b7a53e64ffc94200149500529d3b9\n64640ba1e2bf8ad5a2ffefba4ff9c00682954af8\nc0f2604cbe4bed4a9681348ea7cd2ce4067fbc06\n8630f8cee5b28b0ac9eaebf5751c1df3b17409fb\n6fc903b9086178ddd02322d17b5999719df733bc\n13989cbf1e16ee377db7ab6a4d1138aad614417b\na7f092254269a5a33732667ab0cae56c4c562bcc\nc041cfd3e048e48df24d927680e38d383da9c126\n14e8619eab2e549ea5522cc70f65ffb629d4dfc5\n17bad27719f91cc0ba496a31453be46816224546\nfbf026c7e76dad7a0f6e17947c9200b73b98c985\nf87aa6653d56da4c29bef3c80aa1b94100ae46ab\nde0aa6126df9eccf60d9cb1d921d7454f90e4aec\n9e1a4649f51c94ff9f187a3f7ac31e2a8f616d64\n7652a2c8b52da1097b5b2bf736f655547e13c175\nd2d63c7bfc9a96a447e3d3d6e293be630d117985\n1fe2f3ee3f8bfb2bfc1bed9a2a60e4b0c1b40b83\nbd478e44de1b6a3a53018b761db19fa2b3de4e9f\n5eac4d78b45be6c85cba5bcb4b0e18a3434be119\nd755ccb6bde29d678adcc0b5373be9e8b65866d1\nd906755d15830244286b5c7ae84168777e57b7da\n65eb074c0d896d2fee33ff5424763187d4219cde\n0a1d91c4ba6061fd95f1218556e5c8bd6bee8b2d\nd4e003b62a7e28af4047926dc5c01019e7564167\n70a9734b9d723d8384bf483c509c5d911f6a1688\n25ebab6b83380c069e94c322049135363073bd05\nd6af461097c8d15b052bcd478c618b6ff5e10c6a\n61917b2d0fa7da63d671d9ca81d0c23953af5427\n400837f00c4758b2344aec005c93f44d7819d3d6\n1108bb9f9266eacb3b49a9ac3d733c76731d68af\n8df5e8f8dcaacb911e03448982ec10f470dbd18c\nfbc8e958f4c431dc26b91f95196f77013a74f4b3\n80f143450a09cd7eae6e081d62a96d7eca14b3b3\nda8e89e81c280e09035b82e7dfa349522c7b8e85\nc66b16684b0206e1bd71907882bb64cfa95195b2\na53cd57604f817365dbbccde3741dbd83e87e722\n78920dded1b5cd7afe0c8dd916b780b18e284374\nbb168ab0f564235717dece05cd43a625e5da85b0\nac530a50b2f7c2e3e4c7135dbaef019ca00645bd\n09d219da3dfa0c44ab928953c3a88bb92a4fc19c\n5515922c7c178bdabb4d159ed167f21c63615b5c\n60cbe341aaec7bc0447dc667a569bc205a1f06aa\naa2789a6cb688d9e9bfa30f4972a883dbd92145d\n529685e23ec84d6da5fa8b945b87b17386cadfbb\n0e272fbbd64c5ab1855e8d9ffa500205da93cf99\n64b1947c7da99eecfb02530cf75c211ddc74c2b0\nbe105c9f5058d5b57470e15136dde89363353156\nc1d7e509bedfbf6a8e4814ce021bbccf58a5b705\n4dc7317a42efd4e33f341cd1992ea2249841116e\naddbc3dea61239991370950c3f106d1a73a3ff55\n618f43763704f8e5782258a50fd974ebec02cad1\n1e603d96e885d05dd7c6a8008f34f7cf051e0756\n13b4377ee5658eede8dca625e1a07705a3d6ecfc\na89a3bff193bb6c0b69807082fe475fae043fafe\n8bd961101b57c5fa7254c6be4f81dc49977a3555\n03a79b1b952fc523b208c381315d58a001fcc69f\nb7e435108650eb13d77f95ced6e01fd4d9a3c871\na593cfa496a38bc552e047eba9b61174ea2647e1\n0b6e53cda06ab5496ab7e9197c28cc9311d34b87\n86f0006ee519a0e7a3a5863a7c781b824bfb9b19\n8d4b1cc918e6efe4543fe693002c4f31a2d47fb6\nc529665900eccccc6cb0eeaa76f12e13a95e7d7a\n0dfe64bd87d43fb425da70b86be312e0f59fa332\n532f85a25b4611e2b3bc98a1a2358891ed04efcb\n1c255d5935685ba5d4ec28e4be3b67da7f993183\n66001fadffe3bb7f231a2f2c9968f190293418f5\n39180322dde955544c3ab31b8e0e9689c366ef78\n58c7da52f023c6b5f4e6392c602158d1fe8a073c\n8eebafba384a4604c3a754a2b9c708eae462b3ea\ne856b11068c25bd8a5bfe8ec53629d94aba1c5c7\nc6c4bb239aa4870dd8e24d24e68950ec76ece921\n3df4ea7af6b656a233208f36b3b7558e07d7094e\nb803e61f8f862aaee163bfdeecd587de319761a5\nc38834a439f34e16c6a8370a1609f1c501513826\nb1bb9d026fa0a2c5de1a28ef73f2fbeebdd39a58\n9e71bfb6ef03e5ae7573adabf44578691a1b0402\n239a79a38937b5844b47c2351118155642419320\nfad1ddc79f5567760d323634fe09adb5ba8cf27d\n5cee36cfb362c6fceb14348494749a93cdb1a2d2\n28bf73e59979e32ba4952f8923db4aee3f25f87c\nd6c45d81e09759f98a897d36fe73754c261f9d4a\n29d9de5df30bdb2dbe0fb1d1de74c3e654d6920b\n4b835bbc8546269a75c79fe7b81a6313af2536b1\n5caff2334b4dbc19e315b98e9e3b5978d1a60a30\n8796289a7fbb2a88a44883baae68259b944de22c\n91223e1f6fac5d29ffc064745080839dc6a0892b\n89ab6283a42c62d625f9a0cf4d48fe76c8c0049a\nc9596877cfa5d559efe756718bb24370605c5b67\nbed7432a0883791f9cca6913ab03b708f10f0b85\n0f94ab7184a1e81feab9d5154226630de54a720c\ne32711d41e34c6e07ef5bfdf48eb3b14cc2f5e4b\nec3d3696c2c77ef8c2715c32a3cb001422e5ecfa\n9d11a5062fafabc3f39573c583552e6caee2aa51\n10f4deb3e10de44df76acb637d7002fb00907231\n1fa981a268f0a17f1d3ec33ad05739130dbf3315\n5b9cbccd2d522d713434ec0da90e10c9b26b665e\n8c5e8907aba41c1b92adbc0f19c6084e0502a08d\ncb9112446174e67633d26ec6b71956060a8531ab\n8b1ccf2e53f1da403341957b57bbb4c318f64330\nca9e5da8c80b3f9fb09752ebd8e1d2662a55f59a\nd663e2342ac65b1d559376fe3e7db6582648efec\n71f2577294ad7dddf7206fe30a362d29880a124d\n768793973e59ce600a909a29d744e5edaee3f934\n0e7e43b0471528a7dc9175c0939bb93bf9f34c85\n997f911ecce2f4df036a30bed2e488ca983c3fba\ne1f402ac7faf5604be924bc693cd389fd4ebd3fb\n6d2aa297487b38829ba832aec10ddc1e3b3d7f10\n8adead794dcb8fcea85b07819c2738ada08771ae\n55ac4519606a16f54c383c6de10d34725261c8a7\nd4a6a915bc7a4ef12c14899214df01a840c4fe01\n51eb9e2614dae7f49dca658e10c268433cf19702\n350e777e101b3e0b70134f8571a598348ede7d44\n904879953e2217394b364f7464101dafcdce5e98\n94f1469f2ed8b7412f8bf2f8b4d6e644415ffefd\nb69eb35a5b634101080a43c52ab323fd1ac3faa0\ne4e43e45f6d1c02c0ffa91a77719a2436dab66d5\n5f3fc87b08437f11bcd55d92618f5cc8129c8931\n8e0fae1a415b8e13427bf66489fd33f362054785\ne188ee0611c594e582f7b3c18adb2c65ce2fe3a9\n5d5c37dd97e4337a71782b42878114c1ba391dd7\n49c5034d79504c2558b57d751ad1bc63006ad2d2\nfc1fc199a1859d5f09454010c6e5d9e91d6ad68c\n84c412b8cd2e945e630120f567402f311defd46e\n473fb813f9d5ccd80270d63d67a02d31a95ce124\nb51c7a8db53f623a2dff1f84ac004da9de06b4d8\ne2a6e5fc12e9ce7015a7e73c4a93627a24fa93ce\n709d07e8f693bd7fb2899735f3ac0a37024f211b\n035438eb210cc122a233eb1c1543946f0bb890f6\n242f6999566658e2cc385639da0fd5d169bd1bac\n626da8c3d4947651afcbb7061e6cf07e2bd3739c\n816f78bd4adc634bd31ff95086f27813b78dcc41\n2a54a13ce90f9ec2b0907631e9995457d7f778ef\n3209ac195e1b47e42811a44ce5affb4764d4072b\n6758cb002b0bbc88e1e8c736f4a770fb190accc7\nd8c889ecc66770e3d388c91860bad31a68fa0803\n25b049f577776529d1e6eb1b651c895eeb8363e2\neeb1eaad119a1ee5809f88b9af203956fc7f02eb\n86cb8df0f93f1a6cd07f560396de031e4a7abe08\n91d62644a07fc9b3693fffbb3ccfdcbddb4db794\n7ccb9c615e62c349a67e2509eed9741d5d064cae\n47c73b18c4d1eed30af136cdcb36f44fa8f0229f\n28309c1a2d37d8ec3b3e3bfdbce1afda1c33ca28\ne0bbbfa860e57f9549736149142c9df8be72433b\na659fedb44ef2a844282d3de0d5503c567837948\nbaa6415f1d630b695cd28de742ed609eb65f1399\nbc560f0fd419cbe271e22c0801e64e92a9469397\n0209c81fa9302517ac674725cefc1f4799d70d28\nc850b78988eed29591fcacbc01d4c26f2f49c22a\n36297d60599a1f214e8d183f978d2688236b4479\nfbeeb172c70957d7397f66a612811e75fd31dd6c\n2ec6da12a319bb378df586332c56261484ae1ace\nd2b1e55b3a9ea5743f68389a1ef864b4c06642a5\n4e717ae72b7d49bd35dbb9d6bef4b37b3fdb66ae\n1c939ac8a7fc954a21354672fab2cb0e8f877ce4\n8351d90c77289f126aad66c145eef2dc753591e9\ncbea100db0f75e72d39ae6eeeaaf6164221cdc4e\n7b883cf29bc9ae40ad5cc09a58f9830a4d7a8942\n5674c7f6191b002b43d9f2e89736b26c890e1ea7\n852f7412f7ad9c0502eaab76579a6e0f71f51f60\n80ae06ac5d83a3d219cbcca7432cae4de6403fc5\n669a4e115aad6efc4027cf9bb989e0fc2ea52729\n898f6dc5d884c92a049e06bbf4b3aac1f2a01f01\n5f7bec3e9b4802429701f3cdbc159ac379370f48\ndda4c22b944c3a1972fff24cd112a377b92f8d64\n9ee06fb3c6c2f1eed7b2db7bcd750ed152bc8895\n5522370d4d6f461c03c1caf31d6c7bebb90d9712\n98e5622375544c0eb7ae3f8233dc676f01de80e2\nd43dd36b6fcd625fcf80b9ce7cd380cc699c4666\nb599ffed2123ac4a8db6e572d71c540e44031ca3\n4b5f72a3b6d30d2bc660e947bcb2f924785979b7\n8258df375bc73cfc17b19cc92cc1e371295b00e9\nfb34d3bc071c54e92d5840c5c818bdb79f8209cc\n1d46d255fed53ec1511b103af5a99d64edf82edb\na1738ef6ecf934e6775afcc7045adac7b1e31819\n34cd79c63c171b3bcb8b37a6857bb386b0aad127\na54465a970ab50e701cf5a8c44ddb2fd55f6acee\n2953f326f02116afc1dc4fb5acbb458c63dba834\n6843ead09ca062c856a7c199a247aaf817b88306\nb935436bb85ffc97b3abb14458ff6d8de657f204\n96799b3245092cc24ed082212edd023802ad60b8\nc08a7f914caf43d37ffeff09bb8f9bf75dd442dd\n05f1eca4837d1676905d882a44d112de5c92562b\n6e931ce7092aecb7ce511059b3e40d4f6b1c9fda\naaf31fce475ee60b5dccce765c34d9208a3af280\n3b341f4168e65f653fdf7191a08978e967e86237\nb926ea7f70d26818a04a8c75b68f295cdb8f4978\n02ea31c21261e6c9c374cf51e67484b0d358409f\n3c29315838afcc70648170e0cd12d8bcde3cb6ae\ncce7d0412b19159e65436c667241a7580b384263\n89450c0d2b9cbff0f9c5360b3301811bde4a17c1\n41736eb05ed2c9469622d49d9f41b7bc5b8125d0\na7b22b34d458ba0b53af1bf452064caa61897078\ne415705f275a8aa4ec19f595c156338ecbed1075\ne24d53894f2f613e8d76d61b0065fce3fe63adcb\n2c80df409e6cb9e0782d6f7544096041ebfa225b\n69b735d9bf020d111d5251e70281767fb4e94faa\n7dbe2558f6c045311ef043f2c5859c711345491b\n33c3bee02578082f7c3192d3496b0067af992f9c\nf0f02584af775a85e723c6ccdb09db2c6141a145\nd7f7da68512e560bd9bc5f2dbb35b07e6630b7f9\neb720e2961d73a826fbd6fae55a0b7e93eef25bf\nbbcfaeeb0d88324f3dc8a9a85b3875e7ff3e76dd\n050f5fbb39bdb1e2073fa98fb471acf01777f0f1\n94bfc589ae9777163855e52b511fc90f3f4adf1d\n13fcb33a140bba7391d9ad8b52a1aa7f25a09955\n72cd4261677d9de449577489edb50fafce56b662\nb7d9a30838189b0e240a590f610b66505d85d0ec\n8e19d470d3fff85329e78239c84e54998f39f247\na1a55ec01f46433fb7ac64eae58089cc8e506a19\n5a34993875502fc1cd9f21dec686a3035a8dcb92\n8cac48d464f70bd5da4c8227abfc7025a23a82a0\n63ac499d92fac90f8bb1c3fabc7c03136abd505f\n2bba54fc6e7061893b205149d7c9172c97239cbf\n50348be3339f4d3f9be532dcd4164964992bdf17\na1174d1e922ffd85eb767b7047fb84e3e3e0745e\nda72adbd503e5fce324c8a801d90c1e0b38ca054\n2a19a64551c0a191adc60a956111f25b2a6a340c\n6dfb127bd0e7f504522e80a4ad60ac01118f2c6f\nbb58600d9294fbb5ce39c2e273155602d7981638\ncc8a46218083c85894e090228f3cd55eb5a238c7\n681c2c432b735a3f67d6732264f50638b47b1552\n799d68b0365eeda8c2d17811575b925a0023ae2a\n9a82a5efab8b2d6a3bed6c0611888b0a191119c5\ne354fbeb2467854770650aec659d98b27dc8d464\n98270d267c77f991b3aecf572d38c097fecac68b\n16a5a16cf1ef8a7aad44ac3d9da1e4c8706fac35\n4082e01348c3279f71685ff9eeaccaee25a6e53d\n92d323dc7d6d82645ae7c25a4134679009dba4a6\n0b3ed50bbf81577558e79b77dd8b2c65f61ad034\n203f8ea5953044b05b047a5565c36c23155cff43\nd2144a88b8ccba4dba32265d69782e65635b4997\naf39173450198616f9e1f959f233f980a65a4d47\n2f31989a6b34be6d213a891c3725dde6c168f2fb\n581d8ed65958db142c9c64e3d6f973327a626fcc\n3b02dc02eeaee8d4e8ca13741489c2e6f72de230\n63dfa5ae605542cb998a8b03155bdddb75bb1472\na787ec5760bf147c755950116a9e440d181a2e9d\n35b4d7233eeda44587c59f0593f5599f82898f65\n1fc9ae71deb1b5191c68a3171bf9b5269e198b80\n200cd34e29349b99b6da38065cee4102e916d98c\ncf5c5200f68b97d88ad2dec4331d04db3e189a51\nee072c3650e9a008900cf00132804e83af3fbf56\n90affe1dbac937b1fd09e2d5ba90ae07491948e6\n0a4cf6a2b0a8352659dfd8145692ed6147ba3694\nb26b91e04a541685d0d5a78277c4ab7bf564669d\nf321abf3809cc8b6696be3b6423a073a76f81365\n84bd75cd3f05b01b765ad8be4d4a096681ac681d\n4452960d95d0bf8e6c9e9c8d60b51b376f6551d9\n3341101669b42a7341643d13c284f696da4a0caf\nf53bfa21e447f53f22ff20a2a64b7b859835e8e6\n591aeeb225c1f542bb49a078d6353a7083ee8fc5\nc1e0be12cf158786064bc1f1d1c4bdf3db2159f3\nb3a9b851f5b0c095eede8d5913f102b4f62b04f0\ncdd8aac9f751a212fcca7f01e7a5159d6ab749f2\na1cba27cee11cd0de0a46ebec89c65eeb86a302a\nc3fa6d356292be28d495bdd1a32973880bb31068\nb3f6d0264ce15c28953af1f995738d0d4b335f8b\n3238a10fb81d182766c38caaee646e3f79e98e71\na8185ebd21e1e7b26aeadda1e39c36c8d9278052\n858d692c20fb15b7e897c58e428ed094423a136a\n53d99e58631b8304aeb76ca3e0b62143dd445f68\n224667b7be2a2416e73475803fd164f99d1081da\n6721e7478d45d27d4af231c34c270a87f6d98b75\nf2a5fd1d4363af84a8bbe880bc807563bc00ca0a\nc854fb813aabca89cd859016f9b6c9d24d2988f4\n7087d856b39143ba0c9ed14b4ce5c199152a9687\n6e25bc7cd582d62cfa688c2203be577804624435\nbfdc34035cb07a2272672d964b855c64b81a63c7\nc121e97d6aba9f566a53f017f8695903bee03556\na1738694639365fc09a7f4c49ea4f94bb45d5d2a\nfff6275b60335280698dbb6a609a1ab34229542e\n2cec15e1f3d8d856511225cbd305a35ffc7d33ba\n921d13593df8d28e981991d9a5e4aabe5ca0fffa\nc5e6aa71ce58895c10091db1c7acfac7bb53cdef\n273915422552702005f6ad070f3c726ef4836335\n0b07c0332e0153766bc273f29979cf2beec337e8\nae07741723edb6b01165ece997a1da2d172fb50e\n3e684eef5ec045020461fbafe4ce58245a096de2\nbab65db299f3412abc5fa28b2dffc16aff574c55\nc0478fe2ab8142e746a703956b89a3f5ec69f524\nfe84f3a2e63bc5190493b469fa2524ddbc585cb7\na1d72b29d902e17d92a80db3eeb00b7d0e2d7123\n1c4ffe41812941046a1cf1c1cb869b5021273c88\n94b70f189ccb0199130e2abfc9a87cf9c9b193a9\nf1bd6c453827140e70170b7cd86de21bf5e8aeed\nfe91ef431b330c83f063b8251cbd3fc0bcbb7d4a\n287e9f6759b3d18f48dbf6ca170fa48be2b00c82\n460f0c26cd2bb10b4e44f1f62024525d7b1eadf6\nbb710c9d8412c556055e4f49a406bab60142dee9\n93ae11d1afb8dc6b94739a56bcf278ae3ba8c112\nd5b61a29a7bf53a7ad91810072e4737d8939c642\n3323aabfeea77579cf71ce92bc6e615332e5390b\nd32faad6427e6fe36d3f643fdf8585d01f6452ea\nc028ac3999a88df3c2c62a342e1918f31ea9f0b8\n6b1eec58bb6b77641f782b5414f0ba2980fb4317\n6fa0a5b3ae7d5ac1d267cfbb7937a283ec3c3598\ne61d6d6e9ff125381d4813a5db0782eff5066e66\n466bdfbc9a916580d2e70950af6fb54ef4cf8bb5\nb5e8df8a4d4b82994d6be2b6a0dbbd26dcb34e5a\n431f37ffd6cb1daba38e2a6f654da35dbd81c3a2\nd50f15b4c4eb65069dbaae31e3bc5b86075bd57f\n56c39e111ed66326b50447bb6ed12826e5cd74cd\n96221e574830aea41cfc23c9237cbc6a336a8e95\nccea19047f2b1959fe0bbc0e6cf70a66576e5d15\n1e029d26dcbbc3ce204cfe49d25502d1ec23355c\n37b20891477f13d5f7d4bd166cf461329d733e32\n781eff6363f2efb44e47ddb863651be491421106\n9f3515967f6a05789cce7aba8b03bb107ae5582f\n99fed4a8840a27dfe1e4de27b95374787874f71f\ndffdbab53a21c0b96b6fca16f5bf772f0921dcf8\n000e370c48da86b40a2d68259c8f42238ca83df7\nd3b28a7ce77894f343e51cd6a9bbd78cea91c1b5\n62080422649deb5cda3c5ea671c1deb34e256b1a\n0391b278289365f96fc6508b6c7da7e49e01706d\n48cd1ec6288a0ff4eee7a80b8008f2cd9cd05aa2\n06ca6ce25a2c753d46405b198407cc7db62d2b03\n1e1bf37e1dafaf7c366702da190a778a0736ea02\n11bc68b1196f2232e074eb965496c77182d2155a\n383a57bb4bc9ded9ad28942670dcb7a2022fdaf5\nd71a50bd712c066bbd538ecee5a3b089d1e4d829\nb4b5cc7f8f826c07956bff054d30c84ff5e8de49\n9c62c9ed74006af75e9c679498f5e91a39cf9200\n6f44e913e57a62e8f4d4ecca83fa84cbf35cbd60\n5e305d87a1466106c6658c64d1454a0b1e44ad36\n912aa73b0dc8f3c8bc684b22e92f0af878ba1a85\n362c144d2ab056c0bfd03d8221ace09767ba5841\n08458ff1258fa668ed48edf0e00b9bf6aa38f07a\n42681d1ec76c6b4b64acec9fa44c1c1c8821be56\nc40702e443e6c3fece31cc66338d051527067d88\n6fa6d008767b45244978e9b0c53998f5cb3116d8\n92689753326c9a96f642d5cda16b08378ea96c7d\n7d76b585c66154d3542e92329c53ae313b6d0ae2\n2466ccf4a9638b5428a9e515aa47b52e476b1840\n4a3ad21a37a98251a2412d22294faf1f81df6b1b\n9c2fbcae2aec2174783e03b53e978e7a9fa62a2f\n5cb96769844687f19c809c8b25b37e595256a4bf\n43da59b8384e66f889ae00548fbbca76c55c6bc6\n56d29640a2d9e59628534fc959b7f66c4eae6673\n34e30b6dc3e50e80f8cfa229416d4c409fc49832\n2a362b9331c4ee2e7b5473712ca54d98643586b2\n3ce7eca6f9e31f5351260e085a4e720ede16d066\n1ba6465cfffd6c80d1e0f07314cda1eee1dd25ed\n6a9f9b2de498ad93c7d84e92f7be8c43d7fb146b\nbc32b97a11ff4729a060685a30ae8a019a5491f0\n6fe3c84fec77ae9ec554c2eea18eb74e1982bcff\n2f6b076c27cdb5c324a9ccfeab9960c6b8952ab7\ne9f3dd7059bd73e4a73ebee4e5eabd7bfaffbdc5\n162e3ed4fdc9e48d0f5e2c061cbb1d44db317735\n34e29bcb6fef9fea67de067928e27aa861ee4063\nc879639b5cd8d54da6e96ca9a3f474e734d14594\nc83d719784414c97bbf89b9d6f991582b82dfaa1\n6eb5efb7f4a3be4515745cdbe8951c5ee5a3b14a\nc809094393142acd2ac025f641d920d7182136e5\neab2b7b5092a97bfa14369902d5701ac58313944\n26294003c2dbb0f98ba0fbacd2498eb91fcbf0ec\nf477820eb11f1af06e73b4f022c8059a4e6b78e3\nc71a5ca053c4deecd0843da72208315964c45569\n2a2878e14b3a04aefd74efd015a86ff7d4efacc0\n6b90261785a05f79a7ef429d66e15302409f9d78\na5c9d5b01d592c3d619cf7eac6ac86978a2a159c\nd7733ecb8cefd4f46b082145e7dad84b3d675389\n4d4db52fab645f470698e36b2c3580dcd6f7ad8f\ne2eb9514adda76bfa18b23a58038507d9030f5c5\nc2a9e73dd7a7fb9a04aee38b259f189f5d0bc610\nde7ec6b999347b86b156ef40014234372912dfac\nf8fca5557f1734f828e5f8f876a4f200c7993d37\nf09171cd444f0f5b0c5570d484f18526283e6cdc\n4c0d9220766f3bc7794972a5796ad898ec6ad879\na3b873763dc004f2739099f55161d3e3a5ec671c\n6b717430ef4cc75169b4de8573ba9222dfa32d13\n11659a3b1a6d9c951a61c812e63d73de4a264270\n4ded45556e2c896781186e19c843cc298e0cf140\na7a3355724e86989dbe1d985a63274ccf29251f2\nde781ab923da6bea54d6e3998d63fd8ceac1291d\n74b38a9eaa123bb6e97aebf90d2d186f27bc6a4e\n0303d05981d4e72ff831f582e0cf232ec804d2ae\n2d053086bfd08f11997514781f8949c9fb4a017b\n467e0353d1ed92c2cdd297b4070e833f8f269313\n1398c9f37f6e2aee88fd6d62ab0a1b50ca4529d9\nffa050d4259d859128d75f34368e0430005967a0\n868444e2dcfad98ad93fba8bafced1bb0428ae39\n6f7e10ef918323380c8f2f1147c443608206d92d\nd098638cebee6123d756893963770e245dc96d6a\n60afad7298ae532c4d51a6214fe118b0c0442a0a\ndfafb9953c70e936ccb9a3c6772993d43c9be4cb\nf5ba6be88f6da2d66c52bb039f224b274cb68ca8\nfe8e22a60b89b11d803880aba12a3bbfd43d37a7\n197eb7aa1b572796a9a1b9fc5d8e8bf52c85684b\n5f442e34c294153e1db463c432a0d73464aca3f2\n54d48156e8c9953a74121afa7173cde21f3f466c\nf00b06f414e6708f9381a16a2db46fe4e60e4ab8\nd8fbb36687663b2b5fd474043b63732bae8980fd\n28a6cec3232c02ad1a55b856fb667a17f16e157a\n660a32a135b8bac31aa59c495ae68f9467193bb4\n87d4aed54ceb44e31c22a0cf0e6902d3a09ec84e\n323f38bf35f4078e9896a8f01a0535832c68ecaa\ne8f4ae98679f93c18759fb23153dcc61060356cc\ncd7860138957fad20e630957d3352d31166c42b6\n8049a7005ab453e7d10c9f5cdd1e0831ed5374b7\n9bd7986c4952c2f473c2fae2eee8ff0aaea06a4c\n9355e5408640fa412254ed37ce3de0789cfcbade\n24f390f44aec775aceef7578ff75809c03142b39\nf5017f5afa9e2f2bfea9fa9fb20ee8989a51ab2d\n1254e8e3d5cd29b3ae67ff993a0c110e76fac270\na8c0496fa2525513d8cf81668c57d783eee299d7\n5713f323069a50de8d0dce8bd2953e1d142b46bf\ncdecaf5d747f681ee6addf2b2d6e58479e36da91\nec4219de84f9d346f082fc668d5979aba519c047\nd7d03e0bf8481ce48fc6bc5c8d7c1b56fc46575f\n6c7532ccec112d6faf8ce47f1d89151e4a46f316\n1fb969abeb8eefc633be50e11e3905d688526cdf\n64f141ba6ced44e6a0b9f1b1680a8edb5755415f\n813dcdc3199fa5157a3b4a83dd52580c5570e659\na77ffb5e2988d7ebb01f59464707cebf33a0adb4\n863343903605d610f5b42f72805f993c401cc862\n26f16646507d81a840cce049069872c431905ca9\n3b41901eb9baad1139d245b6220fb502c7c546ca\ne44d74fb9848cb5812512da7e216e73c6bbbdad3\n9203d051d69e9176f2bb659c70c29cceab90b084\n7e4b557bccfd166ac746c73e98e7c76e6c2c9d57\n6757683347f947e730ff668bac4a3d578b8044db\n1bf0cc47ceaa158aa24880be5e7f9b66dd67c956\n063aa3646d4e6a39baeaa5fa26bc329139f05914\na1f4a720a824ed2e11d64e7fa22bfaa6a2da7eab\n01042e110ecc152ce807322aeb53baba5c06bc2f\na495613e9c6d52bdf921c9d005d4b7206ed6feb2\nf59ff1f05c8742ceabc0903bf453732976268ca2\n500a9c4cbe3531658e7b6fad0c9298dc38e504ca\nda5a1a010e9c2950043daeca6766b5c1e5f01e9a\n9bd78475c38ea4e4a2e91fbdd3d35e79ec292cf2\n00512092012a8774bd3e8bb1d98da705f3ec6515\nfb295b01df70c24b149e0ba5e4dbe2078acd2c46\n302f209612217053c796a452b904062542326532\n8ac9a1d9a2c1966591ab441bbc75958c09949a0c\nda57ae2e35af3f04ea35987b06bb5becc0801f9f\n55ad40f4723e2ac65f197f32e7f00983ee0a464a\n34b7d1942f56355684c8dc5b0cc3d0100c72184b\ne4539774b3e07c22694fc62b945868db06ce4458\neff0125c1711790734301fa41e4c4d9fa7bb9d85\nb62c3850ef95ec00a9ee8b3fb27686002999c8fb\nab11ffdb7eaf4219bd35fc99e2c62029736991fc\n7105186a5f9a22d9a990f683ce7774ce2c585674\nc3f5eb7aaa55c037dc5b466cdd1aab3a6fd6b98f\n2a2757d6a09801fd30a29af2e0b05bb8352c1f1b\n3c66751cd46caf6e8744767793d15ede88a9f5f2\n046abc390bf18c6bb5fcbd178ed711d32e507e77\nbd4c682017cf089392a47c3f0976649c11ba39d4\n9d0f4034155f29da792dad6cfe72365fdca2ba1c\n1c3fbd9772d7e68cf002a41646dc519343a180e0\na5ecbd575c741b310e885f2c83e4027a934e6f64\n0bf43b396c17bffd42906533d61fdd0d4e8711fc\n6ad575e55af96adcf468272a6c6387db9f866505\n9be69cb27221c22a01dc51da7640edd5ccac0ef6\n8e471cae6e6bf94b9d4feb1675242458bade2b5a\nabf8f8e90d573342aa0e8ea3bb13d507fc31ee07\n9d65dde56b73b840e1091ecbcfd9c52d3905359d\n63d11cedc8133bbc611e4943463ba8c8bff88951\n4f4a0f41d268ce510724e61ed4c9f77a1e23d666\nd6332da4c26f86cba02a24ba1cf9085a6ac49a77\n2f3505cebb8048a3ad9eb3aa238a4527ee8a354d\neac246c60ddf8f940dc40da34fa8b458ede41004\n6b8cf1705666660973df8888803d5f0c132227c1\n13ede23fedae0ca8a411d7adb75bffbe4d2e9dd0\n8871255c04351ed846eda1ecbdada488c32b6d38\n4d2d5345c335af336b00653a4b8f727bac942837\n4ffe5caab5d0835e7783d7c552b03c4b55d15524\nfcd92513a0a2aee5c64764aa5caec08946459fdf\n1b213a4b393285804fb16547590ff56e8f573544\nf34291c8c07d1a11226e6d0d6261b32954cb4779\nb2facf0143258ffabd83998501dc324ea0d70b2e\n056aa54a151d5dd84b0dbba14764dd9689db4bb4\nef947db13678ba04d0871075388ce2ddbd29d3e8\nb94d77600ff45d7546888426cab34a326f0c4d45\ne36f27762b584aace9c899de266e8a9e9cfcd4a6\nc5a6ef31fab26d2bd21351959a0527bde9540736\n257da44dfc069b910558e7fdc365ca204e44f9a0\n1957202b3c57103ff6a2ecf9dba1b7caa59741ec\n24d2dd3c20990f6c38362e96cd31b7905ed72c60\ne7c81b1ddb71e72ae3f9fbe77c3447020240e308\n59d25e84e0d747cb7e8fd10cd60df6689fe0b3bc\nf80ef93a71dff7ae697bff7264a3cebf74c28163\nf4a00e67519f6fbd9399db44a880634489676e5e\n537136db933b369ff0dadd0a35b0f8483fd1f5dc\ne40a3e109cbaff6d5f4ae9b1cdae253f96b3d407\nd6d27ab8bb68fe69433a04eb5854656dc88e7ef3\n5a193675fb7907390631c620e2f98a12266347e8\n7ddc87fff9a6e124d2ccb1ec772352c4932446f9\n48df26485429a3c1e785dfa82a1d64a4f3adda73\n2ffa80464ecb90ee77856939114833c074327cb0\n0dc523c62986c94ec14f59cf522b4d539420f5f5\n646167fb8dc29f3b080ddcf112b5e75dfbdbb9f4\n191cbc48d3813ccbf1d12681fe1084fab3c36696\nd4dbff15c352ceded8459413be316c62bdcb58ad\nf8c73f821435298e7e6c388b26a98028b03d9ce3\na77d93c7735658c3a04a7b6650607bb126ee2b53\nb72dc72adcf40fb62553db673ff70eef39220c96\n17f2e8702863666dd08403219099347711724310\n8f0d229dd89916ebe800228e94518a6ff2beda3f\n58df9f2d81b8094ff3f29a1c22b4de1b6d7f9eeb\neeeb6cf0977ed3ada7ae47fd5607d0e22536b191\nc92729b89c5365414978f70453704d099e491eb9\neff1a89624a28f73f080b92d3569a746933518b0\nb52e1ef20efd0bae3bb2749d28ce01bc59f8c84d\n32581310a59093e773751cd4753a3e156e89e7e8\n33535467399804add0ea69b5e7faf71c695014be\n21798511e6d33e40efea39d6ab68c640693e81cb\n886a9550c56d9317dd85494e1719a3229348fb3d\nd877f331b9bc248b9880c2edb099104cea420fac\nd585223c6a69512f4cb56d522d460296599a69e6\n2a2131710df199e88189091ab16f307bb1089988\n7da872d4edc18917deeedc4dde12f9bbe57a500d\n025e5ec18575f08d2306a2c9ca99cc87981e9ee9\ne809c5c57ffdf627915db95e5f94e895f90291bb\n3db150d5c0f6dd107902b3656026e18711f7b959\n2371850c27408aea29c0e98b792db5b548a32eb9\n03493c243c12f606f1a5716841c2e7eb4185f32c\n916e120ec3bbf7ea7089e93b8386b0facf00c4a0\n76c7c679a947122e08cc5985623bbefbeb0681c7\n085931328e9be893c65b674c9e491924afa88ca4\nc569928e35f25ab269b0aeab25423093f9db96b5\n195bd8e7cc20468054182bb08fac2e80b80a0bea\n14d26b51fa983c686b95a60afdfb145ea61c1bef\ncff105fd47bada8ce5bafe07a5e6f85ac2b8579e\n85874fade92d329fb0f6d7f824e06035f83eee97\n681a80c258d2011826c12d52874924843f17728e\n9fc1b9c95e1c930caa9a711477e29f8ac94f8534\n78bb2e6ffb5d7e3d4d78ef3128bddb5f1f83104f\nf74db4f6542d0f008fef614440e195ff724a7879\na409f4049744053e5958ea885b5d68524f15c6c4\nd182417156ac4af142138abe951aefd6dea35259\n9e45548eef592b97967607829e844913b6ced577\n86f9fc51b25f749858999ded032aebda17e7aa14\n24a5c2115889e874b9c95a9ca1ffc59c80932d13\n3333a6ec58d3c2530f33cf8132c18862c29d74ce\n702a0ac1811870197e64aa3dbaa04b9d9afea29f\n349e8b733e16eebb7ed693ff5bc0588a3b12cce4\n04f2417b5cdc29bc08e217f8bf6f2a0cf07fa3a8\n5793453bc1f2476b4361c762611da08325fded96\n62ba43bb6af91568e53e5ddcfe29f4740ae41788\n9ef7eabc2075f31510565bf75226e46d30511cf6\nba3af5641e1244e52b148dd008e544d4631c86f5\n24b32c7b053e73a0a3646854c998a2fc6c653ee4\na39fe323baf2b64f2b79c021b2767e05b2f98f7a\n383bc1773a09bd9d2356e788fcf7bebadee489d8\n0487fdf7ecc14fb4eea84f2ca8dd77d62191df0d\ne29447636011bce47e75b8943840e87ef4db9ee3\n442594103964d1441361071ae1f367dce714bda9\nd39844eda0194d17dc47d205c5bc80ac1b4bb296\na6c9c5a3fd93382c758002354e5e362516e70be7\nb883b914b2ca6c0e6d9908c4611e9c4251c2eb0a\n348d856841f14014714bc55206cdbbfb4c849445\ne17b362a94175aaab6ffc9e711e95fa80df08acc\nb04ed0097dd4ba5867062c16ec2d654e72ac6d6e\n315cbdcde908c5e94fc7f283c2b9d0c76e63f4a6\n7f50034e56da5e6562eb3e03c813263a5c13fbb7\n52f64d507d7c5d867f3d8fbeb97aa52ad664ec36\n302a6275995e2398f18aa59ad871554456d4b68c\n979e7f28b2c2efbf113f898b689d6603bfd274e0\n7507cd91dffe48de1b52cd1060610332753dc890\nbad92e9378ef8164cd37f1c190f92e08ec4c5836\ne8480a8c7036b9965c4a7424fc438aa35e4d423b\na2e9980ec557656d248823fc4366c7fa0f8edb24\n7b65855389c4be45c90bfb866eddc9451a8e1d9b\n16d2a8ea03bd62fbce4078450c714bab9625175d\n3fd6ffb00ebd98f4e93fe0e8db7a8c4121a6c670\n46667fae98f6c8b5ac627d4e0ab717413c9abb08\n31515bd149e609f6f676bb357d461d6e83a01608\n373b3b573a04bc6dfa83cab99bd9d881e799f741\na4dff281c4621b70e411a6013293396a1b673c70\naeb39781690a8641f1164d35fe1651f1fbfe355c\n07639d4e4af2b8232ad4835ca890f2c7e1ad7414\n9c97ea3d520064dde2bd147946e9181b3145dcc2\ndd45f9e65664bc3f4bf324e6bfbd5053f4d72e99\n5b6ff83b93434abffcc48d57b1b716d5194cb2a2\nbf8e7fc878dd464ca797d2eea74dad58332261ca\n3f3bb7416475875a443b77aa98f7f4886458c207\n73ed951374c89b5aacc089e88710118f4fdfc203\ne2c2b6d70dcad60da704a37e9800948dbbe469e2\n9cf083098084c9e450154138455c57cc2843952f\n2687815c353e29a9251ff41bdf334757d5376daf\nd72d5a8a07999ad84d0a1197db00e979b588f054\n22cab0ab976068d4b49ecd7d4fa02e65bea4849a\n10c95ab999173de41a257a626d3cd89017b52b1f\na1d693a7da90ba8eaaca8134f36260485f2f051c\n9243cade3cdb3c1cbbe77a3ca0a46ef8cc593d47\ndfcc241369d1b0f5abbbc163493bfbd3273bbc66\n6258abfd7c1817492550798d663e3e5a5c3a54c8\n247eb3fbb0690de397e3283df37eeee8dd53a6ea\n2b6ee47fc7611438ab7f2b0584cce3cf086ca6f4\n20c240e9c512c3d4966f0f7ca4142b80e8de896f\n9c57c6f953bed6e9348cd47ab06a132357ddf573\ndb071da5fc4d62ffcb9e97ab8b5ed4a040c01de7\nc43e91b217077be43e04f48a2d014d096f274d59\n19c08a7c0a935506b65ae130bb5c1edb214cfbbf\n4c4110991857ec7ecbf5f1ce0280d3057b4ed9c7\nf595b16e67adcf5278e9bdcec065422cbe5c9b4b\n0f60305db7c4f3476af529ea7b778540e06a9958\n51efbde55b88dcd80688a6055608daa5fdb30a27\ne7e33f0ca3224bab3d75b40cdce93d3da36ccac6\ned99fde349f38f1f1f5250ff4d9a02faecab9c3b\nb3f1701f70ea9a610e81a4bc08fa5e1ff3ac9a93\n220ec7f1827267091059abd467b88137e717ad94\n81a951367ee99d623c35d9ecad79e57cc24869c0\n52a4b5eb30342d16b5272d769b033ad90cd4d196\n8976863ab2f8aee31a6222a0010e62d9b08a3be2\n2ebaca3cb6894916c37e7746e7f9b4c95473cbe0\ne3673ab836946e34bc759df32499a89bad460878\n5387bc1373db5438e5a06c0c81b80ab717398163\naa88b8fc61ed0bac537901196b0a7b7fedc79ad4\n1693e7fa706fe6c8bb414712ace18f90b86c3585\n662ef335d84401bb58c2cefcd88b41533e359a1d\n2c71440a60bee65291c6a88981857eb86279d958\n3525c446bb0056b05fe6745085ead8ba106403af\n02e128e74c941b83c3f8c1aea14c1387070c1393\n0b27f18583f624ac932f8d5cbabbd3b5407efd9f\n595544e72ea35d3e85034389459821b27716520a\n8697661b71b53e5dbf4e7e3755f3d6daea35248a\n2d69d1b3639238a9082d9dd1befe7b3b97fa8ad5\nfc41bcee84c9a43fc507116dd63d8b62dae7fdb2\nab07be7a8d96b82155c8447d5e73bb7df8c9e6a3\n333d72ebbc833e2b2424cc63a0d19b23be9cc3c7\n0d97d8d35d0cb44c8ff2444964967acfbab8aaf3\nb6c1ba5ef380ab555007b98c54c5af03309c1042\nb89473a34ba4911150e72d636e7660763c6eb62d\nd9039e6c980daefce4e5e3e1acd81fc74864283c\n963a62c8dd0f0dea807bd2ecd041cab385e54daa\nfd2a050cb03bc8a496075f26ae7a8659739b4647\n69e29d707000c0414c64ee0b96530628364f0cc8\ndc31e424924c5e02936096bb158b6ac210979f68\nf73c6e8e4228f1009aa4820521ca638945e9bd20\n62485dd6238c815d2714729fac136dc37a7770b9\nb5d6c624ae39a2fe49d4107dd05af163936b4d35\n61ed44cd0573769b198807e232ebc005561d62a9\n2227edd47210f18a730d6a9abefa8256e09b996c\n79c5b318678348c4e3b3fa7b19f0f784b334f0e1\n3f31a4e61a2aa5e1cca17471bff1073f5652a649\nd4e7eaa96c7270a12b833c6d7430db5f1cd69405\n0aba1d2e1c1885f67df744297f296f8fa1d41202\nebea34b830e20160c547ee399b0a89a42528354d\n97e1f9bdee0f277a10348208ced3c5ecc52fe030\n5292d8fdc00d79026d1ce6e3a08245308f8d60d5\nd26de7235ab9db2370396b262cdb604814aafde9\na2296b5a2bde683ee4d11558e583777fb3e24f23\n514a3ed75f3c8497c6dc15550d1f25ccf79c1734\n1a9f29175a7be590bf6e9af55961cfc1450b3f62\n8f9a792030bebbd18185c86bacafb1e134b34841\n02fc3dc6e85b78e87f8262532ce8dde6c10cdcba\n035535dd27f4661db5b130576828a4dae9246175\n863138e4529b0176a20a2927a33ee1bc9ddde4f0\n3f30c92abead39a75bb0ffcc3b4adf424ceaa792\n5a18865fbce5cfd9dab6db8e86aa0fd75782af5d\n50d4d13b68096b45d1b76b3ced4f18701da68a56\n8618f4f292ecf3547c230744dbd0b5c73192a2c7\na2579b15708e02c1acdf28bc929754b58d9bd41c\n041c87f228eca59819fc9af856d9982f38c4e973\n78d23b58a76940d8f58e6af1550344598110572b\nf04ecbdf9d38ad1d9b796bb56a3cbf981f7da4fe\n0c7c05ff628c54f8b8fecb0c9b807f92905ded70\ncde7d17a518207871133990e4427e2ebe3afb2de\nc16d04c234423f448de65f8a1825658e9d1544de\n19090e57612572aa6007c4ea1e1c752e38ac0fa8\na8055bd9aef058d810836fe24e73e8fe6d8527e2\nd4ed38deb180211c1f6744691a7147a77e63adfd\ndf155e9611f1b685c31e895a17117f098f8b64d8\n963356453cdded1169f9c4adbf5effa8a16caff4\n9f5c7c72eaab5cf9ddb0cead7dca609e60423f3d\n46777afc96ba104bcfffddd7a1406a3c21a80f39\n1d29383b9c8f4c77ed93584b183ba8c1a314c155\nbda4091c8e359bfc3d2ca5f613eac84e37385f05\n16824e42f60e3c2d21e0bc3f81a7244b42a75e06\naf8797fd57a4b341a53644df2f04785175a8096e\n70ab6bea7b72e633ebb43fb979da2ffef6640830\n461d7f7677ad4aaa411cbc66d88b7729a05f8b5b\n52282aff92f1992955beef6fa70417b9b09410c3\n0f337cd8c71673748cfc3272cf5f457053458b47\n0c7714a4f95ac3f8981d44e526e35200e8512425\nf76634964bd6d89dabac201afc71c392db27d5ae\nf68f44037b0ced445391958b21e1d862112aae80\n6c89858eeaafb8a03f709613572ee28a48ff0032\n73e05ee0f38d2f206cd60de6c84a8593371d3e0b\n2612c8303932ff5a8fafad624f39ad220e65e72e\nd0f03270c6429c25f275af3baf0680f4dc69aa10\n8b643f37be453043787bc55b33f1c648a9274fcc\n586cc5d9e7aebfef68491efcdd2d2621ae8b6b4f\nf4a320d63763ef5598ad98eb5f9a4da8648736a5\ne2a8fa552f782c6dfe955ffd66bc3576c55d72d8\nc95f0c44e4c765d890aec8214205e51820224161\nacd96690acb1d5298b429816a5ddf2aa5ad7b8ea\n96f140127d8092590d12f6a0f0474779b780ae44\ndeef4bcbe3f9b1ffdb112370672b49a29a34f6af\n35ddd3d327dff18a151fcdb52767fddd27e9736f\n0121f1cb99c979c68027b16660ccca25a9acc43b\n37aaa51a4282e6843a16f560ee3cae2333564562\n7ee0942d174d95cb9acf7aca092a883c43fc6cdd\nc5bb21e63eecd953a2585a0874f447a43d760aec\n06f03f513ada2fad54be0a113646e44146bb982d\n5cc24e46160f96401ed15304bb893a10fef3ceae\nee98cfaa069f12ca4469caeaea2301c10537df5b\ncb0805ff8bb419be0862283bf395e10eadb32792\naccaa33ba8bde121822be1af560944207b1c1c39\n947ce453dd787b1d398e5935f768abb3034ba42b\nfa64ad6c012d104c56280cfb9799297a7884fca4\n7c7385b77478d340b548fe63613efb6e0dbafdc2\n3259ebdc73fe4256876a2d0652bdcd0ae186629c\n5c76eeb103ca9ee1627db466461f7d428c5763d0\n1b1add09d1e189297a0198fd0189b0a505616106\n03976c746507769e95416b888af101e01d8a3361\nc949e68309974eec224f3709667ab2e73d2edf80\na129fb2eb5e4099d82d15e92ba1f83db88606ee7\n07d2fd16487913f7174e0e7d2e626f50121c344c\nf7f14a6b4c8f1bdba9975f6da08e88385185c229\n885cb512a28f28cf7a9bfbaa294398bfab2044ed\nc67c821d22016544fd0cb52111314ce64e9020a0\nd720a5ed18a74aa50c1e53eb486b8b62ac36c713\n9069634da8b9b8f0aa037702d4ec0394458ded10\n86645a3177b6281d66e1b4e35e1811b52338c11c\n678dd84fab164bc2c7b31437ba1a98a982d50aee\ne05fdf5204a0b9955e4d8f1ed38fab19a2c8b1d9\n5622efa5487a31dce38ca45e71ff7208bba67778\nc250876b72f02c2288d02ae211d5c0b7920bf981\ndd215cc504ee085a031aac6577c81ad8ae753a7f\ne391260463b04eeaed6769f316c95ff9088b3a43\na37c39557a4ba52f53f4e83454a363bd778f1a3f\n34de2c54dcb4b73865783cc24ca0b96969b1e87e\n25b99e1b211e78ecb9ed20cb803594b1959b3000\n5c7816d4ae274af72fadf354bffe70319e4bacdc\nf43cae7dfbb13d56ea9a8c3d1d816b2e4fe1cfc9\n330d69481ece2323d04de2ac8f5b496e503f24cc\nb6e233d04a02cfbee6e25e62d8f033510b08e527\ncd5df131c230e5f47b03dd5a93bcfc9d0b17b019\ncb2428d137a134aff9dbb3bc8a61b9bde38d8f36\n3b91672c6c9c1357190caf3369545b5150fad649\n3f5074c9b8147552ec8e1592d74df60c9e46a50e\n2aed9209cbce02686c61aca1c580907f2b174df4\nb5de18522aa45387382973031ce9c4861cbc8422\n06bbc86f9ac496d95efd98bca3d26a7089d56c94\nc49afc5cca89af0112ce7bcc046a993e37363552\ne73013afd487afb3ea4b564fc29f101d9412d629\n601657baf95fe3ce7884389bb00695bf7bb4330b\n3657bfd59850a0b74e2a94c53c72348513abcf50\n31359590de49b8f5098a8568432929555645689e\n9fab8dbf3eee065837301bec528982ffc32c6089\nb3873d2aad6a8a8d58f65a369499771db8162013\ne4defd9cd3b98b19fd9b2a50c95559a37d26765f\n65b76a75e8c8a28560f096d3ae61df52aae053ca\n87b771ec8ea8ec5fcbf08c24fcbf2c34ae76db9f\n698ab1c7481fad6fedeaa698dcc4d7b00deea0bc\nd57d36e0580f4dc5dc69536825fd7f26dfe9b77d\n8cf4a51e31112475d0e67fe0ff6faa555f59e71a\nf33d6146ddd5dbb8fd4a5f5555aa430788c85c72\n806831ba03e3a5265bed7022d3d723751c2c6eac\n1ee4502f9b109ca2c06d030c1ec12c1b733348a4\ne1523407df5d05d6019b36faac922ca53fc603e5\n682677ce50baefea11903b0c868a4f5efedbefb2\n1d22d38baeffde8996987d5fe2a6fbf91ad3a4b9\n33dc11da36ed72996df70997833e5510e7b45c87\n2f1aca8ae2ee142d3bb185c204ee5c64b52033ea\na958ecfae0c0277bd5b9755e6dacdc480a98d652\n7e3ae78ed13627dc420cac7016921ec85d4aa947\n3d9118d7f619649dd6cbfedf510fc94cc8365ac3\n51d6bafff4fc1e0cbbb8e2f55efcc959da4722a3\n53dbbfe3e7d9e264e3706f3a76bd668b6995f805\n606d7460776dbc264fa3822396f8ea47dd628e36\n52ecf3ad8b0875477bca362c855c22776df30a8b\n47c2b4bf8e54fa56e329a878ad13e001eb83d6dc\n0e4d9929cfcf243a29b0d0da63cffaaf250fe891\n159f5320f9466298e66caccb5b483ef6e5055b77\n958e418f8a950b1ed20d690b8309262d05827a01\nc7deab623214aa39432e4ef483be4e1b5b578dbf\n40e8d85965c90166eafb4425f2159949a2e764ed\n29d156221015801c663e86e15c3923c768d62caf\ncbae76acf8e8549ce8b510ee9e5bf10b9665dce8\n3916bd328bf72d08f372edb7f79a0d6958cc4f6a\n211cb4b9731b40e36b51c4f1e29b0b5491e280ad\nadc817e32355e6f5339c35c0c83dd521cb0ad9c8\n4db9e48d53dc3b098158a30c418a65ce1d095c24\n0fcecf8474d694bbd425388b62c67925786c472d\ncc096b110266d6a71ced8a3b27ba3ad9589147d8\n0e8a5f3c97049b8e101820d33c521215e46c9d28\ne71a9384f9d3b9cb5dcb280655a1fe1c86badb8e\n14dd6be9cd21681f23d8ef63bb77805b24df71e0\ne8668ea90a39405a3518b3a02071ad1f94ae6476\nb617dfc5df3a6d6a93ecafb79fbd1291295392b7\n2f52ca511ff7f9de4ae3aa2142fe55997c1c3b16\n9c184a81ef4d7110ed1f013765fc58306b646fcf\n466caaad9e55c4d3887389203f37935fa9e2e74f\n2c57eaa22af0449cd931b6c2aaa28226f6eeb5dc\n63704b7f57b5225a072d58f2dc84b00cec2f9f6e\naf0745134f8e9d7efa14ff0cbff2bf05e76bb7c5\n371bbf536cbd2982f2f25647e885b061a71b662a\n877e46d801a3a6f47a1cb04231ec22a251c1d05d\n0208e6faccfd390c8948c344869d283bd0612369\n43a3d3db002f3040cb7d4807d65cb4b157ed563e\n6e2961f62120e0784b352802f032a7e023a41705\nab88f69fa9fd45e21854fae16956f47b474595c2\n6117350ddcd962e464318e8757399155507cb48f\n291097a63daf3743365e8a53cdf56e2077802cd8\n883ff5e6ba4d94da068e855ce1fc0f309ea7f382\nd1c0d7f6556cff41c5ef4dc99ce5502a5e48fe7e\n8114297a1d628d8f277dbc21f05e5861c1be9e72\n03fb8aece5a640dc3db4b69e79b0d7413a0aa4db\n246a128be0e5e83a193dc73b8be1400867ceeca4\n8e5192baf3c5772c63bb4ddabebd4a1231950409\ne1d46db6f7bed641bdcbd127e18e5c4c796afb65\n20fcdf84403bab2f83ccca2b7b059b4c5f838764\n73be03970380f6b4ee6bed206672290f2f700f13\n49f7abb47ecc6f5799be16aa057c7382bbbdd1cb\n9f09554d741c879c37b50561ff4610a635328d17\n4d1ca93260487e72f2c9767892e100fcc780127f\n62b3f81321d59bc796f2203a349415e54395ec2a\n1165c6fcf796434b5924a747527a9d31e3e2513f\n4f31943f01eec31d76c18352c3c8da5059d50184\n465acc71768d23ed3f544ede236cd8fa8d107cc7\n4d67b831ad017435924be58ae4b91d34bc302ab1\n9c69fc7899a019d48f63185b1aa1a1aa3cfe36e6\nad576c81c8872ae8f3e2c913bf1a7aa89f2be46f\n792ee6d70184c078a4d0e7eb7a5759daffde9575\na726868f2a0352dd69509f29b228753f066bf0b4\nd9f9802ae6c4fe2f0e81b3bb033d53c948dc44e1\nbad988b5fc9f0656ef94d7f60547b15349f925d7\n82c2cc261fc2899d674cae1faf4c3b3f9ee4482f\na6e0d1325b70c263ef61bac6480a5724cda69d63\ne122a48ff42eb16f07d1e4c60fa5e798c137c133\na57a7d284d6b29d26d661fdcb2fb5a568a3d6399\nbc080a6a5218483c333d8a8d62ad3c9f42effc8a\n8ffa9ff5428a7e90f5946ab178517578cd5dbe7f\n013ef8b186632b8e625a7f70a688291e4fd93204\na297703456ffe8ec046d25411f087d303ee8527b\nef54273ce5a79fc9fa4f268c67f647be21854a6a\n3c19fec781e2176b5135ee5f082c43008f47bdef\nefc63d151901fdcb6dcccca38c723091ed3670e5\nc03761dd6db163fdac549c68dc765f2b1b2de134\n11c48611038c211b9272a39eb1a558beb257ae2a\n15a7af2fdbe19138fc1d279301322941d72df051\n50e4a4832f60cb11953440d6ab5cc318ba6c292f\n71c67854aaf9634bb00d3f0cabf04faa45b1703d\n726439a19dd7ea4ae87b185e68bcfd348cf8fdc5\na7db2e9b74cc69f9862d3216d2c857ebaf57dc14\nf126871d033c243282abc4e572d68f52a6987d8a\nf2b2a9202d15e047129f223bfd1046c240f172d0\neeac604c28012dd95c83af6d9164fed333f5c587\n6deb85b4571275288851976ec031caac1101dc3d\nc1ea635a6606e7800f03813ead94aa660f9d374f\ne244a66db7c5f9bdad8697c4af31e9f33126bcac\n470fbe91ac813f08980a9603b4b3a6d77a903825\n901af97eb7a61588afcb32a7747c167c6736295f\nb734fc096d4e3d6534a21f67b053b4491d67513b\n67d5925d3dedfb92935a3983010dfba6286ea636\n7143eb17c6c1cc9c78c7b6600f30452f921f7f8c\neefe02ba547f850e9da5c56892a9ac45cfbb29c4\na6d7a717a154ae35ae26baaa396978d3d7ba4a68\nb59ada6025e875ea781d2b0f07cd2cab114543d1\n9948823743a42effcd85c3ae147daa1d9d15d31d\n4bcdc842c6a4645265230b921ced574dbe15588f\ncfc1cf6ebf8f8bf9f534b2d640af4d4564213467\n5a493fc61cb370b5c2c0c2487aa18f8ca81ef067\n14fa0a028a6568cc9c660893502917fb960f93d7\ncd8dbc26b3cd8def1aa00e8391bbaf18c01530b1\n3aa8587aae55ce50f5389923521388a467d2a7ea\n91d038e332e1b38799eb7b5e594d4fb7e93675f7\n62dc6be4fc038b2526ccfdd2e9a31e424652571e\n817a8062a283827aa22d90a8d5aaa201310effac\nc2027be6c0363d6a0011bb6df9948ba734021921\nfcd9ef8c7d66d8b2af645be760aa0f99a3c3cbac\nfe4e90763318d06f134b4093e7fec2f9dfd74dcf\nefa8d3fb5a9c3c28325df28cfe4244711020be28\na4875920e015c40c97023e103cdfa28b21282e37\n0a1b04837ef6f9793989fc65d48bad9c7df94bea\nf55a391d94e4da8c5eaf3ffeeab3a2e27ad6adc9\nfe9218330241deaf90871bbdef5a671634bce81e\n2c2a83c592a934a35346e5dd9467fa8c4820dc60\ndf92443f24256ca322a777b2e39623a4373fb681\nc4cd5ad10f765a8afc4f33e31fbf919c765507ec\n5c507275ea83d7164daedc690b3f8c8879b21e83\n0f44d04cf565cc434ad0d76e7a48e41e882acd5a\n50ffb4ad4a36804c57a6897a101a9d4a937104d4\n91ed94f3e7ba639bcbbd97ee5319416bd64890d4\n750a611301607ec8613ffa1e07ce863362a27570\nb183b1e717e37c7243b99ec18b8d697aa8b0a1fc\nc13ff8da9d0e6d04c5f530a992501fd1ff3c3992\na5eac062f964e5485f309248e295d8f0110b2dd7\n224322a9885f4b5b452d96bfc68a226ca0a02bd3\na0824bac4caeee87b7f21e22d362f29c9dc8843a\n6fcc6255f4eb63135e4c714d65638c524f8278bc\ndda982bfbd1c8a2f3879197c08b04775eb95be82\n3db8d9052c1a7e486657de0b16945b9e2f6cec2b\n2d57dbb72c691d6b333499718de50c954ff8acca\nb1e0868ef0edb473718fdfc8494ea0e3fe54722c\n41529c00be9ed9586eec0e9407c83cff258c0c8e\nc88b214251333b7350b10b27fa2dae683ef3b602\nc7bb17fc14cbbc27ac46b7ee34dd8287301a76db\n5defb56428352d12eb1a9ff27755c37cf9c7cac2\na92d2e3aa03be1a7bc634f5ba71e085a8a48fe89\n3988878bb233c3354cb4f3b3aef7d30c73afc465\n586cfb543c7062b0df0d0c5bec468af01cf7fa53\n643c07c7d544ad83df7bb855cd90aab8651adbb1\n17f31180eeac77b4377c5a854bd40ad613e01210\n4dee94abce5cf59e8a14f54a57dd64ee480aab01\n9298bd4d47c9210c8ed8c353d14fc4d06e9b9744\nea4bc9f5aa3517aea230ab662dde65592ec5a0a2\n50b50a73de61b9c6d6482b1baa15c6b8b8483baf\nd2c8da09ff1faa9d405d24c9798c41711cecf996\n3f6b43155a0af4c5dacf7e493eae2c49caeac858\nd9f60de3ad0efb4241b060fb174227ec8700f90f\ndf0dcf4dcdd6bcf3f573c3d6dcc9eae9db6b9bb5\n4eb68a4fc61ea3b88fa0c3361393e240b12a1e71\n74a3b0670179ce95695a9969fd1fb549de988f0f\n9e74d60fb0df2a1b90fd16109e14c1309a2743b7\n2ce8f4bea06100605980b80b424e62e988ae1088\na72dd21df0278f8c70539083ae28dfa1b1fb93e2\n9ce9add4478a6fcb0f4c328ef1424c02fcf082ad\n6d3f5e2e1b6fc5ac57c411d4adb91691b9e96ef6\na60d94f60fd7a9fa869ef1aa4101338e2e982938\n41adfd261f7f4544d0a982a5c09cddf455afa97b\n9a8c7cb2648822d9247d083f80a352f337ff70e0\nd8c506a48ab502f34b44bd173e6e9b836764d6b3\nbc4884df2a3ad22b2ed32eb044b66ab298ad4ffc\n1e1f55b0670ea5b1d03a4d9ac56610bf2845bf4c\ned8786a06b8623938d82c9a1c38982b36df11139\n096c9254dc09665a7f9645281306c25fe9ba257b\nd4747c96ab72b8f2766b5797080d9917363a753f\n97f71a42a0a32e1c93904327ac8c195feabde1ec\nf2eded53fd2b0e9e03af1deb658f0d1315c90f99\neee1276a68eb23b3df81a8f8f0ec0f5ba293ee2e\n807b1a2ef9913086bd2eb27aee47b17d872be14b\n2f960dfd6dda36d6154656d1ee439abfa44db357\n05983fa408408f5f0d5ec0541db5c61c28a55b8c\n6ed938930ea697c7ed89332b610fd4b057da66d4\n74f3517bc57aade400a68b128da983acf4b0c11a\n6a0ca8f77b62006bbd58a5d6ff0897e3c24ee99c\n9e487891682a7f7b00211139e64e069e146e8736\nde5c873f83ac38efb8b9ba0f5b9a0c6109d3178b\n6939b1cdbfb0c98d611fefbaa935462dddeabea7\nab513ab3a8c9e9e74e0aee9e404b33c6cbad8cd9\n3c6a47ce1599c071b84814e64cc22bfe70f4e74a\na5498c61858525d9781dfbe7c3dea27194ef0b04\n8531cbfd4662a25a66a14e73c4f914adeb71145c\n3111739f6130a87ac9c379617732a3ae040b590f\n0da71dd8bf4cd3919d035f1039baedbd228f684e\n56a77e02cec99cd19a00e54e9eddea62040af0cf\nb47ec8a86d55548b785676fd1ba431386b4e4dae\nfcd85f373341fd8421801364e362acd30ef2ebe4\na0dd65902d3535e222b9a259744401f54df2c95e\n550c842e6a2e45e1eb3493c65aeec3fc3977bb0b\n6585ed0b8a888855de07660cbd45e5ddbffde34e\na2355994144a5319aaba781c68ccec9cc1341b61\n35bd7a2206f730e73e49b74b623d115e5be330bc\nfa4ae601d9472a0d7fe9e0d45529ec6f56c28147\n40a3da0adf309c1f7831f10abcd512275771a9ca\n0469c1961ed0b19ba46027db7d537446a9ba2fca\n8d6defe515f156e06444884592426c62871ac95f\ne0d1cb3accc65ccf3f0279e576eabb2ab4712e84\n76f93a93638bbb8fb8bc0e5c0a714b0f01e480bd\nc1007d77f7c4539a299657fbefc14b5a87d527a1\nb0d6fac4b564797f4f356f9dc81372df9c61e92f\n4d74524706b5b3ee9765518a616079174e10f017\nedd4cbca59a1c748928ab7ee81b8caeccc628e8d\n7e905c1c721018cd7a0336d2b630ed273fe9b6b3\n91b08445116f6b25b51ee1fcd1d27d262e0a5309\n7c99508394adb92cb499e62312db871e21a09fc6\nbde414d405a8051901fddf90415cb91f82a444da\n73fb41db8345debc967eb50dcf1f1c461a9ac499\necc3f8ca6dc41d000495851de0631b1cc64ce344\n04094157b805103337eaca75891ce1121c63cb44\n05e699c9f4bc2a91241038e16b80b426320d005a\n43300c6e1639679564a12f7aafdb577edfcc20da\n2490692a8f8b5913d529a81a70f5f05e11871085\n4d31a7f92e1ad98f3309708dc23194c9c001d3b3\nd270313ca80adfae3b66015a59a40eb65c3bf498\n5eebed34cd7055bece9fb0bd46c7689c1927dcf0\nf8d363e6e24e942988aaa48a6fcdbf092a65f936\n1a93a8d468627bddba6244aa27a210ded5fe232a\n2b647fd576e2c58129686b480b5b32f76f9c0a2d\ndec8a4c87057cc31951b297e0796bfea37751e17\n7ec9412c035ffda8374d701956805021f8ba1bd0\n22d40857924ef8d7a4e0e61c9e527891500d3d7c\n1dfaa1f43594c6139c2ffcfe76c33af85e596e5d\n91cb29f477cb92b35ee77870445f225c798a1cde\nfd2fd068e5b3d537ccc027d1c14330660cd4a030\nb7d2c765ff46acd6c9df645f97b9b181715257d2\n18e679561335e0fdf259c8c21b664724ebc03e84\nd2b0eb4eafdef691e71691cf6c1da85ac07b9fd9\nf6137cc2fc34a4ae334c710c0c237c8f089d7e20\naea0913b894ef74d3935df3ff3198ccb86dc89e6\nd3d4628ac44663d42115e68c3195cc57cf48d4c7\ndcb2af1cdd32868cc3c900defddea926bb85be0f\n786ff9b649a0a9d6f836c4eda390165034573d44\n403543b8e778f048dbef3238e38ddca4707b38a2\n82f0cbe7e117718aaf97860090bc01edfd939a93\ncce099319717c5fa6335cedab8512035b0d2e6b1\nf0b772396303c122ef9ebaad2b4c17d34f15f2b2\n428b1d2c2324fda6afd6ad43f3d0ff697350ae9f\n47cc12a4f06fb26c611be63bf340624cc80e955b\nc59f76096c9ade54041cdfc8d4319683ddf1f133\n5e4b7f22a106a7562cdfc6328ac6349b6eaa317f\n77d625cf97b5d206a64809153a85ae6052c1797a\nd131a82d6ade3c8f05713f7445b9452826d25d72\n3971a4df0b2e68e7826c8850edfa74d0678c2120\n83e79d97322144cadd49e99c4d568455c00e3fd7\n4b037caa02962d80745655b5e1ef6858ad04ea2b\n0bd4654bb6b58218dff7e1a437c38f23860c17f1\ne0d64dcc93e0fad10bd2f47c62c4b2a39fdee7ad\neea03615c43deaad5bdf2989e4dbf4dc6209575c\nf0adeca772448524d7e14aecece343a4a22fb36a\na15d049d16a32896d73267da550d8f2826518526\n42dbd37b3d0ebdf11991af51142ce4938d4bce99\nc8756c929ccce9793a1e1717439a257c97f22203\nedfe59b7a42884380acc1344d17663000f75eff2\n5d739ba1d7f82408c41d858329ff9234f1ccd439\n6513f4410cb8923286d35492d48458efd5eb1d22\n7ffc2a2a3d8727cfec8d298e85a81a5c3fb57bcb\n2bdd089bffb74d6c25e5f171748d16ebaea7c200\naf9dbcda856289029402349ed003efbe0f10e95e\n6a82177b68c97bf88752b5dfb6ecf85d9abbd100\nb1dd12b4f0a9d8727d561a1a6b3395ae9c684211\ne04b046b75d3a0a31e7eebbcf30e36f10edb55e2\ne665ba2b11285ecac9ee987fd1bc4a1ab6b68978\nb2da46c1581c79c6d176dacf7d17f622d36e7b23\n4c8d56dffb72abe22830054d8936f750578f91b2\n702cf8a46262a9eb2e4848faaa34697cfa83bd52\n8ce7b4dab224dd31a175f1284cae93b91204eab0\n743d40d88ffc853de5daef780fcb8297222a7d54\nae30e7972b8a261a7ce0d4f1a9d908e5547bf70d\n2cfc720178153e19c8d537a5aa8ea2cdab0975f9\n1d43d2ec96bfefb7132cb6b236dcf8ca06f6a09c\n07d8e0f85b48c54ca8da920ec3153fcf02ff7a17\n740e37bc645b340a2093c85114b1dcf00c7d5057\n7afa13f6302b3793a7ad04bb492f031c84d80f55\nad06c1cfaa2de146df9dab483f00fb43a2295f6b\ne7a573fa5ae649a379a6385557a42a7112ef095e\nc8f1c13f26238b9ce8ff5fe9b161210ba70ed48e\n7e9de543000ce6b6633b33eec1d82d6854f3dee7\nc3b788bdc8b2f59208a0c1f9f74cedd3513a75b0\nb666e3fa075ea5245a6aa43d9cbfe1007097c340\n6afb800cba67485ac08beb712307b45ea95b64c7\n2c8dd08c247cb459e0803564e21a8dc5841ed9d9\n257d9be36a877688a002ee68473ba0fdcf47ea12\n1a6cddb03a08c61e26b2dd32b9773e020b8f589a\n09313aa08dc8cccc7f27e03d5f6e6661d5e2b73d\n5697532b99955d48dd8bdc38db85b86616bcd781\nc2d3945ca539d59fb3533774c99718ed8529c121\na3a3fc5a7e4b658806e0bba562e6b703cf0f25f7\n6880c7142972aabcdb864fc48a786478ffb8be49\n1fb03173971da31cdc9a60cf8f8823cfe0eb6a4d\n46d70a337e287f93b05db5d9bf57c08eaa41e535\nb9ce7647ba8f7e4d49f8824ca24b7914c1ef4404\n3e78e39922364d8022190c8e188e07744e35b8ff\n00900d28e831b38c5dc78aa3d66b2a4938d0d3a8\n32d04837b45a8d15a6d0ef3fba608383f4a67197\ne562ab294d1654152a4b8bad1e64590212d6e26d\n9997109f3b7c7d397b5af4203765da6d582f9e83\n2eebe2e8e8fa3dd9924d95bcf2725f4f58146337\n156950f559f1c3cfc05cf1b69396fd39fc4343bc\nb1bd959c9dd83b36c94f1fe2ae7ddf038e58f8ca\nfae5015141acbff318d770f2dfa73f031c5fcbf8\n2d32a49fe117828144eb2f0503429b7c411816eb\n2b9d82e43ce6c4d1bc50d1a7f90b6657c2549633\n61a20614269616a9a3f212eee62c988a6e98d531\n75a875d51968f5c1e13ad65ab41e67da6e979c72\n8bb17331350a9111b5aca33e0869177d47562ed2\n87811202be34c5202ee3525cb5c2fda1c06f9d28\ndaa6bbd9c0543d2687d9d5166f1a8b0b1c29ecc8\n4456d9b0414e7458bfa977830d14b32a312de50d\nc2cab69eadc89cacaef04837c068830abf219cd1\n7ac6f555eb29477b3cb60d39f576ecceaf35859d\n42a866d4c75503843b0a7471d1f3ec6c4065b1d8\nff11825a7e043d30879b81b6e9436ad248e068ef\n36db3ecf92117d20a83581c018e83c09b89ef3a7\n3b15194e64db505f314e1e6f980eca588fb1aafd\n6f6d1ea3a0b3bb1089e59b710357e83abd331287\nb534497026ee08ca01f7a3d15c4fde24376cdb4a\n2e8a0fd7cdea999ae4e90f9f1db1bf36aad95e00\n00fc1334ea6dcb933d56e9231a98f97b6976afa4\nf8d704d99cce7f00696f0aa9ca6272fb7f04c005\nc469f0ba9886092a80dcb7b317a0fceecd9ce82d\n0c20533d57f9a7b501c48a7e7d91b046a4cf1454\n107f39e5db77df3f782b1a45d233e4f2b028dc2c\n3bc03c149ff90926257ff722f82ef9bdcdd16d20\na1dd9044d6dc834354326a1cb23d30e018f7921e\na1f1377d8f50f44d6635b3eec779da2aa53ee279\nb8916a7cb5330c098b9d207305bd75656c98bc95\nc190908d01f3d219cc55add4d95d5416eaa7b332\nc394864619651a0fd186f0d12d520a25a669e3bb\na35858ff9118b1e851b49132eea9581d532537da\n8ebf5fba21f99bf904f38569e3bda3306ae8fa9e\n75d76e36326e649ff354d6848dd07b34f8456dd9\nfa1964b57de7a486694a979d393625c4224ae34c\nf79dc044b38d7cb50bedc6c8ab5860d0c6c361eb\n6204fdb79d99d8a6ec90185d14fffb2ec624693b\nc6640ff2ecf78225565be2778b9447bad0415571\nc855821012ed71159c14759768f173a0fa754495\n9da93ccdb5043c45a03ea9efab26d69dda6df426\ne3cf3de293dbf4361f384539ef9248b495464953\n5929359151b00d7eda040110ab236d2a529137b8\n8667c490c5f269cc10e1574e24ba940c7016630e\ned9162b21b4851a8443c2fc9e4f86f24da57f536\n309020346d1d02532ba6479eb38c5b46fcbe1422\n13996bebd47fe648fb374aae3517c6e8ae828478\na711d7e9597a015d2fed25a7a0bd10df04bdc9ab\n20cbff25f55c0a305e3ef1528d785c581988e7d5\n886179285c09f239edbd573b620257fcad3f3c01\n8ea6bb8ccc48d688e73d0ef614f33ee28d0c8a77\nb168f8ede2ed17483fa46c52baec81acdcbdbca5\n63647ef1e00134c14307c65d29b2539d482607a3\nf653ce88a5fe3c38de000f0287eb44023067ab6c\ncd6fb18aa0d9c911a71e02a430555f4fb27a9bfd\n756b12c6da7d180b2b09d9be3f8873bc93774366\n1c534efabbce6c81be33b28a2fce4bab307fee70\nfb3226afbb6a91e96b04670d362cc18192fa70d4\n93a1ed46a904cf3f15fb12cc75fe4cf8c3d8ecf9\n259f64c4abff05757940c5cef61e152f7296d451\na3df4ce4e01a93797428ecaa5ffd14e8ccfe6d4f\na87041465ba90930947920b9075673ec7884e0cf\na2af37311be5ad693a7d76f859f157095eaa6e4b\neb706a5a3ac8bf004d015a9de54ada8f7063a3fb\nf42eb0e6477757d050c07be2adec952672eaa083\nf22b141b1ad277f14c0e3dcad9506275a024d985\n58cd4786f055db39c15980bf0574f3415e49bd4f\n880278affeb5729cb5f860903337bca3e53eb5f7\n17b35aea596261032c6183327689184e52515363\neb82e5ab73697559cdaa5359ce2eb664d2fabe14\ne55ab3e4eb8ea9a798c761bdf31e3428759cce2e\nd887aa56a5c40c44f67735af585696fdc624a6f5\nd87203edbf363e7e64eb4af1adfc1403d7e93bd9\nf90689cc8579169b4705ba603dad1cda5b427ced\n2014b89f5c3911f4b37bdde167e713217fe9ebd9\nde65083142747932c726e75b451791b50eae56c8\nb22bf3ddef855c9ccd95430cbfaae3099d44540a\n1648791889665b29540d093f2cc38f4ef6a87ff9\nfc0dc02dde3ff3dcdd62d95edcdfeb44e2348f4a\nee97002db899a95857307e328775d2db7064e399\n414109b4d53ef973361d5ceaefade981d1b43eed\n763485c1819485fd5455cd53814a7a5293870a2f\nb0380ce7373a8053058e1dcc3bc436e241bc3717\n6bcb68bb0c651266c30f0e9962f2eec80c771b5c\nb40709212a5dc2820ae9b737a584f291bdd29669\n2dc2a5bd7646b376e9cad23f4f5db917d4068adc\n08e71702a2601e75310cebcb95faa5194ba549d3\n171513f9e25c57b52492b481999681db7e0997ba\na14098100a655d494e48b4e847bb70f4d13d6476\nefd9e88fcf3e19f1ef43ff5879d9c742572b1a47\nb60953ebe93c0577e2ee2ea292301ecf0a090aee\nc3faeda237dc394ece62a71cf701930ff5ff6e9f\n8be2229d5c810a3152573c1a5c5d0c6f93021eb2\n7f5bd28b10d793afb822d01fa2ed8bca6f78b697\nc92d4688407e0173c8e6ad66369a8f15e8400587\n870d44939d3046d8b79f0bd59f5e6d1cda31e97f\n9b3e2cbbd9010213a9e2205408594031807d3eb5\n8111636d42d431e521ffbcc8511f61cacef3be00\n9247eaefdfee193b8766945c6cd2377d48637c2a\ncdbf7233f95ebf0034c664574e5b4afe45209567\n7c7bd555e3dc8e056f90de9b2175959b3443f482\n993a7605687b42aa7c36987e121b70b3e6c6afd3\n83d90093a9f5e63379de1bcca145c51bf8618483\n3534c6ea4bdd91ece5dbc71b85155bf07f9e4cdd\n396c062ec0629ed16a30c714463422979ad83202\nf4008680a09bb4faeb340deaa9b81cdd09ec7216\n56b3c8974e36565744ffdb592fed64811bbae82d\nc0c3444708304739612bab676095883c823ef96b\n755e1e7bd6bd44f358588a248e038826672944a9\n5ac8de10cb3f1fef840d9d2acbe0ecce293712ee\n0c974de72660d7e8ebbcc3f7ce495199837bcd26\n87c55ef12898069dec92fe13ee704dfac649f33d\n6fe68a149f51a757953ca35bcf08591f8a349f67\ncad9957616e80eb216a8269ad552c105da553861\n51ebf27bd815180fe84a7813b4f7160d63e09abd\n9ff4531b0227acd84946a6ebd4f40928036442b2\n045c47dc9118fb22a20579c908d071342d3be8ca\n1b24a410cd70e3c1a09130db33b66ecdc123524c\n6312d09c44a26d0cd989ea283200bdf11bf985bd\n936c72146ac85c3a490de4c3441084c4d403ae29\n3e8f145578cf290075c710ce46dcdcafabb88898\ne4618758b9f80847d459dd053b6c5b60a26bb580\n0842d2135ade287427a82842735f404d09b807dd\n73c0c257d966bae0c310dcd841fb37d997df9483\ndce6af8d9c49166c1619fc762874ef274fc36913\n2442d0d467e47af0e62e1a55059a242b2789ff3c\nc45597efaa5fa9ad6a98deca8c1b9f4ba0c7b388\nf5e2d7155ff1f7d69f9d32fb02fc00c728ca0534\n565bf63ac62792464caaeaf7fef7e12e6ee55424\ne7a5f4f11da1b0219626e642c4183311b745ee3b\n8d5d0bb12e5cd91db0edf18448c5704b5639d4f8\n55f8ec2471c4df074ee237fe1ec5adbba32db24a\n843b6e0db426bacbf21994b137d27fffdc13222d\nf979796b0c2a29a5e77e6c72dd933cc922a3a610\n0b0a6abd67a44d8f3a219e9794ca9fb5dd9c3679\n34e4ff3b7f27b17a0e7b6c0cdbb53bd68e012ea1\neddbd60d7351d594c8f56e46a09f7e878424d9eb\n8336f450225b1088a02a40c62c6de74818681040\ned0782b28b6c89069bd919709e1cf57222bc734e\n2d510c904fe14aa029ee9ea96e98f3aadb9a27a9\n309d520d70d01ca5cb2911bb2b51889ee265f1dd\n1ef917eb3b5c3674142c40c8d9c83a2542a097ee\n3d4490f29327d1eb634369b349edaece42972c6f\n625d0d43defef73588a349fe4c9ae4c4b5b513ef\n371c4ea6c98df00752e0e43ad40c2017c94596b1\nde96347a51d1259827a7feb556d702629363225a\n3559b7e68f7f1ed0c0977ca4dde00c5ba84295a6\neb6d3af6e64c3bcb5e358f3f690aa1e2e68bce4a\n7943f39c2d5d2fe4b4b0e693ab16e1efcc15df14\ne408ca867b6a5af7cdd9804b1adc42a7fc0b428e\nc7d4923d98be9c4a4b9fed8d8e22d72c99a8a66d\n8a2eb618c88996ce96f2b9651945086a9911987b\n15d9cb64ccb64e28c7a8a31e8c70d7b11bb8ca45\n06a2374ea824885faeb8423146f78977e585c921\nd752b7dadc3e93935c4473643ac459501855f69f\n32f6c5185b7017a86944ab21ed861f3cda67ead2\n763f35552506f642325d124def537b738dade694\ne17357b54f0392ae5559328fd404a1c02ad3373f\n494fc460f3533bca4e81fbb3cac52f381f0169ce\n353887e5360d94ce4ff5a0a891814aa2f03c1be0\nf673d614f9955d79b613eb248288d317349c5777\n2e7acf4fe904c02ec796c6e8aebe73aa4364c073\nb6f4f1481ed3a36c3d6ae02cbca9a2877ddf6702\ne892735488702df24d6bc4d9b6847e5c13c57caa\nc23f6f166ea3ee2e3a9c659b5e1763edc823420c\n5c2c32cf69b65865957b495122ea3252a1b74715\n8319ce82acb079246cb64d185d99835bf195a11c\na7b5433d897630789f687d9504b1d58cfaadb6ba\n68f9b1f66a6579f058026aac150edf7d260c1e1b\nfa306ebbebfe90ed57cecbf5d170a5670efc2151\na77115c64ec17be4382284d7e7da7ceffd81b796\n7c7e1cf85df43725857ab626818bb4f464a5fbed\nba0abf4fe1b23978a2479d2e47d38777e88fe8da\n5c9045fce266d3460c68c8a91db8c0bae73928d4\n213dadb37638d3907b081787817dd0bd29c7801d\n5dac9a380d67d645ef6ee855bc8db7bb2fa240f4\n56aadb779fa63e3751a4fe096fbf0f6fe0b8c6ff\n380777056cadabb8b2088bc504764f6f5a988e3a\nf8e12884d2b5b749d7f004a76bff484938ae9949\nff9cfc9e20b5804ed54ffac937af1c1d923c4b52\n11f42d1ca38c9a24d8c86d1e6f3b4f5697b62a50\n8cd24935a6a68bc5da3f95ed7b8098c639b286a8\n44db64adf744fca1005aaad64cb679c8e46480b9\n07a29500e828892fd6f9f3bd32791daa93523c33\n00d60f84e5beb610ad2d414caa16071ee8318c20\n9e6165884c69419cc706edbe695ee44bf594201c\ndb85442c55b983a53acf28b630c26156f5c78a7a\na39c1523d84b91a9fb2153966c01f40545d263a9\ne4c32ffe5e1ae60996f08362524f0a31e156e580\n53f6940f23637c92e9b3c7f2590489f1a04fee8b\na31a5aab83265cae124030e31afb2d8161517a49\n4e66d02f7be46804f4291e52a2913dca11398bb4\nd9c9a4e85c250fdfc52889e313e88bab93361a67\n3d427e156f02e5ae80971c5adedab736e4570218\n2817457d03d14b9b8c919d14a4398ad11a9724f2\ncf8d21c1423bee2d235b7d2997fb08b13cb2576c\n461216e738dfa7790d86d0bf5fa32a94f22f6b30\n6fb37dafdc35a91ac066c8b82598db9a565c5c6a\ndcda7bacf833e3388f96f369bc8e1a713cfff943\n6b7441318d53089d8178c5fd92d47b625836c38c\ned1d68e393d336f3860968921d17a19c080f82ef\n5165fdbdf164623a319c97df2d3a1dda239bc350\nedcd5b42498f1658c88938b38d66ef23b3e997b0\na8f51d584f6de535a545173d2e714c34c9f057d5\n921ad20d8c0baebf6ad19398fca666347e17305b\n44fe70810398f30422f01c23f0b4d5119e5ac269\n04d3d7dbe9aa5b49e4e6edcabaa73de93a99007b\need48aadbcb3a0099d6339b16d0ca256efd5b2eb\n77f487b70306be50873167a363b7cbc455ac7a1e\nbdc2997ed709155a166218a613f78877bc8adf99\ne5f2cd20edad11523f89f70fb5a9a0bbe976e220\nc8f7d9b4118bae93328cd9937a42cf078e12c989\n3255032cd4fa5aaa8339ae3307fcc40e2c1e5526\nfcb9686346d364b4d16eb7bebaebef1c9bbee05b\n453da82b53c59342919230bb97ac103e7d19f5ca\n5df0b5ada91bead8b0c1ce43133ce31bb988721d\n0c92eda77ba173040b0a35da9232869f02e23b31\nc7f3890d56ca343518535df5254586262c26ca14\nf3622aab4b6f207b1ce84f7997dc3650f3f50a4e\nee463965c59f9fd8f31c162345be3784dd08ae92\n0616d64521ad05f27ec02c43467f4246d20439d3\n97fe92ebc2bba8e18fdf11834703cd25a9889e55\n8cc732379d61f12545214351227b96d33e81d6c8\n0817c9ad7c76c8dc9f6c04a5b64230ceac788eb8\ndaab25528053c5430942d3734593046b09c0735a\n6a30501a6e9356518a6d47951b7d201ae7462c00\nfa8222a169bfa56c56addb8cfb8e2782a0b291c1\n609550397bb6bde78991a8eea0701a9380113af6\n3d47aeae457dcaf92ed21b63d134359ed50e1af3\nf3543191335117632af7ee612dc7b1f5e65f92ba\n9dfa4e37ad094fb0c2076e0d03f547700c9a2250\nc6ca8da2268aeb7471a4a7e7ecb0a91b6c1403f7\na2ea8cd20379fda7bde5886f59498235de34ad2d\ncbd26d3ca965a86c1ca286e0291ea0b7cd6faf31\nce8adf1b5273038f21dfc5dd8b7b74249e395a95\n3dc44779f8e0875e678ea03c20e17c9d445024ad\n6237b951c3b52feae6841d91aa8e10a25f583921\nb8346484331f3344c4b5c1f9caa75cdc542861e2\n03adc3156bd16c5fcdbb83619bc0770524930361\n3cb41569351838ab46ff9f533d06d9b4ae3d89fc\nf01c855e592c9851b9c55d7db90b5969fd426868\n57395c0f477f7c865266322d3556c7583a5e72a6\n8aa1830a3e65eb9e093a055d59110c69094d6242\n9f19dcce147e3b82ba5d8a3fea49332c13503366\n746ec4c40b56b3f5cf26d83e5c1bc11a4f21359b\n3c1c4292af8ec1c90c5b070aa9048bd430f1844b\na59a27bda0a5ba38bc4221b9e7855319e7ab36c1\n09bf9da137d19cbb57e15cdece55bff490238364\nf9afd41bcdc50468b2b12333789f3ecaa598443c\n0835acd461d8403200f1dcbdc6840146651d3dc5\n0bc8459a29b18b69b630ff293e1b5cbdfe240353\n0d81ecee5c630442421dd4bef7825de9841e189e\n392844aa937cc2abb498a179debd22f8b48b8072\n92f73f2a4e887e1a18c4fbbf58e6e29f0672990e\n9d43e76dd04598dff4e2d90dc086ce1126c75a8f\n94d678fd6d46196d9c35a0eef6ba84ae9b794b5b\n1c49f296e9184f7e2bcbfada9de19d2dca8d315f\n0434d161c35d7fd70eb1a28a9376fdd55e8af2ea\n4922aad478a5f06bdacec005c741dd4fef76a13f\na490a980257e7fbd63b67accfbecfaeab2ee6150\nc1200780b220e4b1793e392ef033325e1feb371c\na725efdb0090b911f25c5d59c2c80fc2fc4e71f5\n09aa41c30a0f6f4f22f4f10680ddb540acfbe892\nf4f8ab64127207693ac48d3ff6cf3ddfdfa41aea\n8f68f34dfaffc9a8f17f12472a5e5fb8581e5219\n96a104220f748e43a43efd15249e1ee2fe932144\ncda111f1c8a28c8abdc93ce2843b2c5193173b89\n10d61b696779138984cd6393a3de536d90c4102e\n60cebcc2a64b4b9bd3e80cbdeffc0b8ffa6ddd24\n0b6851cee7a0aca9664ec2f09091514374cdd819\n2b7aedc107c5b7e8cf17d71e96ee760f6d0b38f3\n663bc588faa0d3c7773adc1d841bee6379b9d46f\n60f17c1538c2db3020056e6ef23c76456f738b90\n55e5e7ea66be6e2f2b0a2f98e6ea78312ba3dc1c\n922f88419d1403bc1e08e30b49070594081ce187\n08da76f6d09eb31073cc12c71bca1f645a66cec3\na56861ee45b5ff1b1d90fff3f905785ba6484a9e\n2a5e280ea37fdb94fcc120d03369b5ede10e128d\nd1aa338f6885a69c3e695e3cb3efe83d62caaa09\n6ba9817c4b49b3d3d5624d8f28026b4ff4a2cddd\n39f04ba4cee1077a03d713be74b12e1d72040060\ne215925241dd1e34b67b1cc5f948096769ac2ada\n3486102b7ce136fe13d4fcaeba2e8af2b9a15e63\n115df31ed442fdc46f586ad953306fa5ae3d375e\n060b00fc8acc7fc52e3b6d944e5b306170393850\n5069e7ddda117ad784c8124d6d29081ab8ad53a4\n6598db3e26ae44c4ac858e846963c4a24c209dfa\nb39e2701253246c1ddbb41a7c4ace9bdbf88c587\ncf978ec560f604e6d9e2ac00c94af68ab8321620\n8973ac89c3fbb08f72f46fd6785bcdd3f4cac8dd\nb30a139ad0c87fb0f02c90f29481f40f12ca808b\ndb3bc54cf0fee96e609fd94e8714f02a064a91b7\nf3353970df8f5514290769b898a9368d369b7f4a\naf41bfe6cb1e93304b43a2b806ce9de97d0a3677\n1fbcbd0f6fbf67d8f84918621833e1d6e63de439\n20933ae40f783684e90147b154933d86028867f5\n9441c946a1b67c621e7647c0bf93c3ecbfc2ad38\n3ff193d807dfd3b4511a567bcaa5db5ccf717d39\nff658fa38a9f9e038357decb831e35181656b0a7\nbc5465e7b036b3f8b15200103f8006ba54207617\n5928ca15b6f45e8fafd468d09000c4da765b3267\n65afd1f6e7515175ba2ecd3b6d86e34c39a3a143\n7d1c22dc0e55a181f232f58d52ac0adca49f856c\n12b76eb311777c6b1671e6f475b40205d7b804b2\na721002c199cc70bc0c18557cba3e8c537d6744b\n2642451f9b3d61c5bc1f9a247f38cc695e043ff4\n3717ad63180779f1f3119ce38b8b29c0b637c827\ncf20c150ef0acb3c2c61b4a8fbe6015c8a924ea9\n584eb58f48b8ce5365a92c75407db692001878db\n7555cb78caca688287a315ac3ae44e92e2717488\n0f214bed7a7cb4fce58819d54234dc67538f897a\n1c7454dac153ab89baec68da1e74cdff26a5e476\n59181b7efd513109142736c54594559c20b99759\n7132baa5bd7686de6190e63dcd538aa0392db08a\ne2070fe65954daff51a8bc6839b499b3eef6bbe4\nac1a77631ec67159ddffc6009e23606b1803d9f1\n75b3dfb19728dbe76654eb4f633bae7a2dcafe85\n7fa9cee31dd7c3cad756282f7ff64ea8d120c2fa\n1e65dda2670080e312ad1553169b1d3ea4976632\n9a87f3a42bb3f2456840d4d98a9dd51d2e3418f4\n1e7644e62946bf52d247663ff2514226fde46138\n9a6f944d187ce553b84ee511d1ea936e1770d6e9\naadc0547f85cc28ac730b88b809d8b6b58d99f13\n3e99df7b4ccc1fd40a5739257186ea24ac292c4e\n916b4adc6c3af117a9a7992b872d6b95047adc63\ncf311b3f8941a68b155e4190c2f00a5fbd5f53fa\ned16d248270e4aa1a5cab2af65b009c685ee1c65\n050b95e0f8ad833d5233a6691bea89b6fc6168dd\n638fcacb4433104acaf7d59b0c29a34a9f063681\n79e0aa07c41d4f4c94d80184af13f9f8fbbd89a2\n5fd7d73cb7ab0f751e5ecddce95d0f85bcb30bb0\na95423bc1dfb8763f4d702e9e15995e2634a44dc\n33477e607dfcbe3d005b919de124af3ae1b7701c\na3cb688818101f47ef7adc65e0a5aa189c89cf3d\n85494ea42dba92740cad9829b0231e303a5db0d9\n89da2706398db81653a1a5b6509e6c0ec990cc6d\nc72fda263ead57ae45ccf2eb28ab1ce92962bced\n765375ae09d8e3cc08b8c389ec856f75f1f7ba4d\n61c5967288d6a33d938d769c02d25f68d9ba4926\nca7f16cd52c720561b40ccdd6beec26f88cbbe5e\nf23b864a3cb3d5526a99a2d75ca3a69ac795bd3d\n76f8d5f07325ef4942885103453f986ea3782f14\nce8acb42553ccf7eb76360b5f65ce778bd6d099f\nfe9ffc06953ccbe5c65ec134efd7070dbf6310f9\n62c0b4fae5e8b9cc366a7de5d3b349d0fc263065\n9c1b112fbb2b12dfca47b63646b77af1eaecea8e\nac1ebe0c86d4bb1b5d21db0eb29e1313defa8f41\n8a7f7ba79856092d3d1202eb41ed585cd294cc52\naea234264abda28edee90db41d92dd6df1dfcc6b\n148e79032178755f8fbd7dd6dd15840ae02e96fa\n9019969a7522958aab1a4bbc30140a5f1f5df983\n6ceb6692fc981af823fe43e16e59a6fa1410a398\nf15008333f58a883ef8e5caa27ef99bca6de1611\nec07d7c56028a9076dd834a835a76ebcaf570d94\nd97d0cbaa0ca851eec37585bea0e5357e4d6544e\n289886ee032f60c41fee15f3b5bba6e671e327e9\nf5cc37a37fc4b71a37dacdb993ce07b0e0c2ef79\nd6fb1c05de68b63a38282b87d220cb5736d07fcb\nbfc9a572d9ad25e1755002f8d03b31edf7b5fc9b\n6b70420a51f716b3ab5842db978d551dfef88434\n393344f5571828dc6e58888245f3718035bdcae3\nd5ecbcd2b61f92b5cc10a863f6e95ed8b0b96898\nff2a454b25c17ebbd4428ea41992bfdd71096fba\n98046aaaea3c7c203fe80e1c8d2f61618ba7b63e\n667b9288e5ee41e2e7cfcf2d4fb7a428503c549f\ncd06d7a2a94f1abbf466516694c2cfe2149bf56d\nb7178b68ba8542d149578874d7fbd5290786ffb4\nfd2328046e3788c440fa48192a63dbbbc1c79264\ndd099c2ff2f187389bd5ed69a5a6e462490c2c86\n28f686349a9d05b79cf399468027ad5936a20bc1\n9e4033dba0d8728cc8de62cd453027b01af2020e\n16a8b5a62341f818a591d178760850fc8bd283a0\n588cb3c20dfa3ec6ed041cfce7aa391e37b75f16\n5d1fbdfde5a0296973a89f90794e14985e89386a\n344be156abed478f0065bef75c3be54679183204\n8a878d60cfed669c02afb1eda3d0f6cc8c9d4948\n5aab7b87b93d0326fb5e15630ce01aee03c30741\n42c6aea2f3ca18499804d48de4bcb70023f37a72\n74223b3c883d1f7aa53469454f0a1c5e3e5681f5\n47f956b156d70d77dd477dc8e92aa99b4858d4dc\nbb15224b49be4422bac517b6e18f6aa410ee2571\n5d0b75705955ed383f676fe3bb4dc966cecd8583\nf29f670b6a9d4454d42679475c05d5933084344c\n0aeb7ee3f572865c6ed1d00cb7d94830ee435fef\n2daab6b71f884cccd363b6b05a19293cb3fee2b2\n602ad423fccd8eb6d7f4ebd297fbc68cd2b08d01\n983eee72f935a7ce70d28a7b6d35e6c263fc2fdf\n085797c33219df01c930dbe27c29cb11398f03d3\nef332f0ee789177c64c847302f1725cc42d558ec\n0695cd31a90572587772fbc3f45dac8d678c8c4e\n33815785d90440d319fb6efb7866118f310fd419\n21a43c9900c59302cbce48fd2b0d8bf34469cc7c\nbc7d7326829a388463a5a7e41c99c7801269d2ad\ncce8539090b295ca6ae86ca7a0900960354798c9\n9c0fd79d14bdf43cbc69f752624d30e586e9564e\nf501053662ee2c79336012b5cf3eeb86539bbadd\nb879fa2bdc4a4388efc07e8c9e3f4004abe15e98\n79318e3ef70daf15dad5b620857efa857b2066ed\nf8e012438b1cb25abbcc2ff90caf8ebc2ce14ada\n2ec4f03b7976b65d99845fbc0950379a9f6b5a59\n13a73ea3062ca6e8977b6257dc0a0e80e9ff0d2f\ndbb03ebac0d3391ecb09345d7cf45ddc509104ff\n7535484f98c0098f8358cbdb3d7a82834272f566\nae3d0c71561ce754a77e4b958de94cb494c17ba7\n6475fee6ea6534f2e5641a56510bb36256951327\n89cbb67300162eea463f9ac40cf533eed2caa6aa\nca0d3756116e607ea96d3165151f590dbf605d4a\nc81cc5897f336db0c8889c06af72db2b3bca30e7\n1b983f297226e77b2800630ad2335da42ba976f3\n3596c39c740ef83457b75261b774a48894c4420a\n22d83a156b253fc2f203a2e08d0538a7241675cc\n41d9b947ac9c7c7a2bf53d488b84c31ff436953f\n79621819717360f647e693c07704ba1d7c30b16c\ne7d0e8562eb97199081bb7b9f84c68e218d0b557\nb5ebe5ff4f5d9a1e2f5158ee99e1d2bd08f0582f\nb02dbcb157172a3c6ccd5eadfb2a78477557799a\n6098b9f4d04820329d615ec01f8332748132a6de\ndab249698fd794f5c70475ec326165f2733107a6\n7e0faf08240b1026784dab219fc93fbfc88ddab9\n5caadc2dd4f99cdb4055f94e82247ac7b4644c82\neeae319e6fba19122867592373a6cf625ead92a0\n151d86f75adc877b7d11cc35540cc848fce6d4a2\n54b9e6b4096e25aa496d37571a8528afc7f6d586\nc3d0d364edf4d383ff904e23008821396e3cbf28\n202ecb65234c571f146226d3c8421f7ec53011b1\n8f4e543faab2ba15f0609321876286d3d98149b5\nb7e8dff20e515cb91bcc486e3573714d5f53e68b\nd5258fa6c34c0b4d238f6f9c484495b07e42bc70\nd1f5701490e421feacd1f2f83526c343524a126e\n16ba0130857f157b8ca2d25ca970628b2d6e81b6\nb876bb46ad10a9e6b835dbf8017e36716fe566e9\nb286809dea53c2a2ea82ffec535bd4519f5c9e8a\n61f9af2883329b93f18c7878535f9f97a6b9461c\n003a349e1cd36f53ee8c76d0aa7f451b14d09fff\n725ceeb3a0a11c0395f58b8524db7a5dc482f3b1\nb36d14fb3160a67bef18ef1e98f81822d9f97ebe\n04439585a81b86bc2b6efa84ed95c22ed6b4aa2b\n817cdea62dfacc1a2b26d76a5585473bdbdf80b2\na67b89861bdcd4e46669fddaf9c9e7c4447a7c75\n2fe18f670331c8858287495c2440c218a480eaf4\n70fe86b3691b34c50faf1aa9d78f0d7bd29826f4\n176580d083e975678c1cb0204af07ec1f4d1e377\n2031fbe4e584fd9ff239912990afb64bd8465206\n60e2fdab00f8f20d4204e4822909fbf15f2b80a9\nace41e37436366b3b48f28150875d1da8683f971\nc190e513d8762ca6cbd34203d4e7f34409718350\ndbe86a1d276ffe751749d78b7644bf53d727b235\n92f54bd32c64650744622d29e7639e8495298709\n974cede9e228fb9f90ea1f02b56762ca3e343a6a\n1c5f9ef05f2ac1df72a2c2790e0ce4a85196f899\nad56241386f50a1853c92bd6bc9de657d460e228\nea5e2e2a8a5282c92355f12c5c12210eeb7993d6\n14c551b7293d696bfdcafd1c662291f094a1bfaf\n25fc05cbf29e3dd7978c5c3ff98c2af837fe6393\n44dd4bdf4e26c1dd93494f1aff23a31f1f7a9e79\n09821b0afdefd29f0a2d8f1d16d07bfbf4993d65\nb190587fe5a11c9bd30338dca34d8ffc5a3cb9a5\n5b3223187d5d3c66ba4ac176a28d9d445af3a05d\n30711d7e9e349183490fb405f8309d463dfe75c5\n466d3cbed9a7de0143d2c409e39e477c2c437665\n87ffe7fe3b93ee0a27d4d22c89869325573a7386\na825793487ed70e3969d94660ddf17905edd3d78\n127fb5fea981bda64d6d17c298789f1187fcecfd\n63814bd3e27245dcfc5d4ba11f765c6ecd727838\n2461fc8fd838e233301fd6268c73e227ea812483\n39a8cbf850140154611de7842acbe94da5ebf3d5\n23b0452d3f57e7e1f1a04e9b65ac79ebf45fe94c\n63a6cedf489fe45966aa661ba42ef14cfb50af7f\n8861b4b932ea90f9ed891893eecfd4b6e360ad32\ne47c3301943a66984671a3fc238fc8d2c48804d7\nb5551d43be75252f3f08d9ba1984be8af9b0161b\nca51e56e4fad9d83d97dd625061a726c6737a127\ncb77b48b62a1149bbf016cacb19529328700942e\ne0cb8ca5c167da59831038ac75b1896648b844ad\n56ed04546303de03c403ca658c16909edea8e265\n2ebbf629ddc5fbe7141f961c7a5d819a62d5f194\nf5ed526604dda2ea27d924e008fb8822a40b3b40\n68dbc78ea23bc3c094df7de434fb829cf5f71291\nd7ae04bc83484c23119be8595a4b02dc8fca604d\n747a95f491ea34c7e070d8f277b1385df9e4e233\n1e46a6d9596d8f7b37f5fe4b532c45bd534d9341\n042000aa2f4103516b675429826aae27ee630cff\n063cf906081d3894fb0dbe20e2233075d290b259\n9e1a6b9565e264379026199507a5f12e9b18d244\n8805ecf84743bc0cfeae3becde69e9b930a41bbc\n90b4ac84b052fd1eb963ae9ef179de54d17b9339\n20bb8fad54023738be95050b43c584376eb38547\nc764685364cda45cd7f76ee61026fa92b3435e32\n7be3b97d5edc74d655caee605512830105737e9b\n6e258008f3fe925994e072daaa9d75c9dbb7760a\nc7ff8ae6f595b6655eac2bb180b6eb7eb157445a\nab13610276804818c8fc149180fbfe5f4529dbe4\ndabb36bfa7ea0d68ad7f36e2d15a3f603ad86ac9\n66f3b4e7aa86a9192c64fe6d1122ae5a29339b9f\na0d919e4c908cc6ac5720f4b147ed8733ebe9608\na7c1280523780c351501ca0e25bd897de61c81a6\n16e58ef3a16052bdcc77f8f7379dbfa8ce60571a\n66d404c23738071205eba79bf92da94c49f8618d\n29721571915c3304053c2a6564255d40fcb09e5f\nfc6564b7b42dcdc3b00384909c4b21ef8fcb3993\na28988b075950add016891cb1c0d76f28dfbeb21\n3d083afdf332473516db6956c822b751574d059b\n539c024c671d37fa48ab31cbb977af73913825aa\n8be239fc61a6cba28bea5190d8885e1d1e25b598\n69e69bd2d6d42ded7df8e31e78581df9167d35ef\n018693e7ebd8c5cb202964177d5de298de356ec9\n2373ea2a423fbf98ce4dd0c556bf1c7797ad14b2\n6e9f2b2e48b22d581ba1f26630c7f442e9ae5f82\n2358e58442d261aa832a397f09f62d71ad1c9771\n9a156aa2b8e8249e65999f5b08f81d6fe4ca53e2\n6290ec80b465320bbd966c90f4bbc38a55a7bc76\ndab065f980de93e5d53e031a1cb95a980b90b814\n9bc941fa9f3270a5c8db6bad9574f30482f2fc10\naa092df23f44030cc5996d6ccd8df0cffe4e8cfe\n3160ab71dadf831e17091cc7215dd9f90d7d96af\n92811fc87431326cb07f661db5d9ef72d9ce4e9a\n3320d2b23e9e9d8b31d199dbb83c87351a1325a0\na23762fe4157e4cae2715b063624ebb9be06a450\na0a5a63f38340dce64acae8f23ef26ed1e247064\n4e035bebb3622c07f57e392da8c6f2afaebc3261\n22b1a37803ecbb339f016f881f26bbbe5f2f8c46\nc312d8fb2f94a0f765ef40a7085da271ad8f3601\ne83477e70105080f6daa89859888b9c788b64831\nc40947ea2137dd384adb22367ddf1831d293df15\n5625d5f865c3760152f1ec90a35d57c22fdf0c0a\n3e39fd2ac22d59097105e019c60ef1998508dd7d\ndd0049686de8b640ca1123d4dc8f4194021ee887\naf1c60e897334324e3d6dd1a7787d50d2e1ee973\na3f8c6157c4263813331935a2635df856de10c27\n7b2da29bd66fec284a8ae0ed6e7c1fb58d8a75e5\n126d16e9a6419ebc946d76283549d48edf8f577a\n6c2a6d5e2a602bca0e066a4338820cf2520300e5\nb2500941ec0845017e78a79b376305197de374c1\n53d909fc90bed86c508e9602dd3dff0998a87fb4\n95673cb589c08f7b9b91f0565161fee9b89d252b\n283bec8c2dd843bd9f89e4fea6ee6e001877c803\n8ba974019ac339517ab15b654cccd3c9882d3d34\n77635a5a66d8aaf180316c16152ba6aa245f8996\n6dd381f8cd69e953a5598a0c26a1088649507964\n80760c1ddc981f6e94f4e3dba1c98ac109806232\ne045ec2409c0502e9b049e5566d5dbaf7d6d8788\n69bd7624b90d527ed23406b956ec43dc451ccadf\nc371d7af384e8716ef3e0be02e2f8da6551e84dd\n041abb29901cd235202e0f22755b7db6b2596932\n0c2b2794dc45b2566c1baa8b061bd60be3530cbe\naaedb6a45fa3f154aefdd12b68ca3b10daa45d26\n3dfd12b73b4c30337f0f007ba299b6c550667f52\n4a02d86bc5c0cb808bf8b8962fea0425521d3977\neb6c2b821e0f03e4976a073124f91c3025ca2d06\n7859611f7fe33530e181b48b558af5827a4022dd\ne6ab0944d595e85ca3bdb683b42def15d7fa4ddc\nc1955b27adfa21f3ae38b54178c69cb604e73ced\n53ba73e1220feab3a4e6b54265338051fa7c9f91\n60594ed03141479578c6a07d11b5bf76753dece0\n038b5e5b1e4e9bb1e6f7d8d7969ce07279d474ec\neafa71ff350fe9a4cc5b2ef66fa38117bfe094f7\na370bcdbeace0ae48d100a2fcd2801bd4085126c\n0739c358539a0629bfcb26a661610748909efea1\n8c110668e59f3566b16296b91ea496f20aeecb18\naac8b80f73e9b221e7dc4370aefe2e43a16df286\nb935064fa71c603cf65127440c336fef96683426\n8cc818b9927be4dc0091eb9b93d86477d6c62c00\ncc507c7d912a95f3f01b43188b422a6f268454e7\ne4886f3453e9cdb71203ee66f2487d60ca4203ed\n1c6203c29dfa8ad426e201dfdd19c09b21fdc834\n260133601bf69ecf226350be74a5fbfd20c85861\n011becafa6b96e37fc37f9944c63a5b4ce2e4af4\n4c6e863cdc20372bed0b306a76443541989f04cb\n8e3dddd53488518eb2af7aa822408cae39a60ca5\n77f8d5efb19a381b5488a846362aff40ece9a3de\n5eb05ac1c1e72234a30f04a7f52d4604bc8b6c06\na11b72e26dc9a71430fb3fd5c4bfd5591409906c\ne37e821ee0e5e768c60afe17f0ef3959a7136f3f\n81023b24f5a899da647ae7ce00669c040592430f\n2245c608ed6b13c94406c9d8b64626ab9dfd3d98\n77bd401ef2b996e1709999668c2cf3852a691723\n2ed683ae1298506449b325df1ef81334f94bc79f\n4f8689ee5d22cb18bc88f9d583dc1b083adc0cc1\naa6939f2229a12ef75fe06e5a9476a60fc07f066\n902370faf90e28437c058e8dcbba8e54b0c5cadd\nf694ac7f54a186bdf9ddc123879c97fcb85e8e3f\nff660123dca05e16b49e4dc31e55e078dc584110\n2a605297459206702f8f3a713577961f49d68b18\n42be0e853e6e504790761eb655f6028771cee855\ne388cae6f1a14f729f86d57ddf430678ac298f43\n5762ec24ff2ccd9a5731a5204015d347ef3b1dd9\n91e1ea02b1fbf3f0066af5f6161b11298fdf5feb\nb0dae40725f2be1ada240392874748c7f8f5ecb1\n141dacddf82b531a609d64023e44a0b78db94ad5\n90b11bf5c894ecb81c395235ef3b036282c648f6\nc0856b90f4271b5e2281a15dad9fc9edbb75452c\n9e752dfd6a77d89fef094c0b3b852f1a2e945b82\nf6b39da05d9d0c96d73b4b9bb733f70c5a892c1c\nf8b22d9a12a9cc43e70086231388c4bff00a9b77\na95d1faa9b969ffe0dbcc8033e3c0da37840560d\n33433486d2acc9e1aa4dee8c39e2478a2a5596f7\ne3c410eb88521e5453d127fae15cb4ef8e98e3b6\n5aed2da88eef6ca84b46e16f5085b77bfbcb98bb\ned285c33d2b968def5bd3e3e8445d294fa24f244\nc9aee0b1847dbc71fdaaa33be01e7d3a65c5408f\nc4dbb41e7b37e9e0fed01bf48eb4c4698bd6456a\nb7c8216d1611dddd849b19920d1407fe68f3a7e0\n30ec734b5ff705a6b7a18dd7cdd6c5ef0ebd5231\n4e9375a0ab4f304a1fee44b01d68d9e30cf0c02a\n4bf3482f0b7032d8b42df232d85ffaac68d9b0e8\nfb3da587b3194dbc450734af89119ad69bda4456\neff9bf8e4c3b1ddc6619b6b6ce74238cce74bffb\nd8926c0360032655ffb5bab4cd1fb6f34d9025de\n0d3ac2dc82d3d2cbd18381aa7ba3c924114d1650\n17e888fb78f4c94277dd9be49b8913966ce80361\n92e94b9a2f2a4daa7d6174b2a19156ce5e3242f4\n15aeaacc15e86c6fe879d131853c4e2aafd404e2\n9b7a09695d8906d7760c4f8a603ee6834639145a\nd254d07482d160713f4962ee83b2ab3ab0a061a5\nd981beee8af23eb1a88959f273c265893706c36a\n0e829d781bc79e9aafaf07112a252d3989d07ae1\n59b8313b93e359e2e953f5759cfbee35555fc6f3\na927825f41807b8ba3fe834bb4cdf9b19f380160\n956943487a928049235d880e5756392f7b646766\n942b24602598af530f0b853e5e99a06786a870bd\n203831d712b3691c1b9167048326e433475bef56\nb77e88e29d7f10ba18566064095cf43575c7ef33\n9665f3c663cae93a060a8d42345493467b2afaf4\n75e44d879c72f2d5fe03dcfd1be1730d191c34f1\n9241e581a68df76911cb74c0d241cdfb98abb97c\n589fd170829697735036d0baa90cdcc2c9461e2c\n42d9fb33b7cac00cb0bf98dc91556db5978c07b9\n4c46c9c2c6507485b069901b400ff2fbfdeb58a4\n058c2fd0fbe424772c4ac8da958b108b149fe665\n2779721a299d975eb9b449f9fe7d5b933ad4c541\n6dbc1d7982c8727e149a2c026ab1ebe88d4007a5\n8b1b20920acf629cadbe99b4b06769e8e63912d9\n3c602d135d6f170f910c7893c96487db949637af\nce76652240bca2e369fb7d09cf7d2c90a465e53e\nb88fcccee58017d8eca65fca4188a9d8aeef4721\n59044ef87ccf8565906b5fd8a1df49a4d450c621\n4a42678222549258646340aae4e1cde768f7abc1\n13b6151582c0f91a91b3af81a79a5c5b71bdbe82\n16dcdaaf7183e24e0244c572f87567afd3ee43b9\n513a30c8e55f18e73dae972ce10d9db9563352e8\nf59d5160a28f8f4407c2c989f4d41b8b8312b123\n3fa8411d14a191f218d4d13edef3d9858bc2e1f4\nc6976fc95170d3f8ff7ae5b6ffc261b3a0c8ebbf\ne7e5b1a40b91e62a5c4b3407035d6eadde539374\n3bf9b70afc3ba297a2e384a431867f5b0ed00e54\nce56e4d5705b73accffbff60048fb92f75b106bd\nc04f2724aed299780444fe36da320baf4cd58f6b\neb5d6336b9f7acfbdc5110ada01bfd8946fc4d73\n3c014f58a59a8b657652c9c4babd71ff79f3d7c9\n457938c59d0867580e96ae978aab47d10424ab2c\n83ecc170dca6746831ee32deeaa692ec5d97fa74\n793e678e8c54db07602dc70f5d52112e5e186f62\n86b349224e745fbd675d56e113138227e0574e9e\n4fdcf23e77468179dafa74a2850e0ff0b3cc72b7\n9b2c3db2e020b5728ff8047c254b476008f1a4de\nbdf63dd83891ea9cddd40d02c2278cc8f4ed009f\n84cc1f485cdb313c0e8f2085711f4307bcd931dc\n1209735bc4779ed18949ef75074c7f6f2468d51e\nf5a18b7cbe4263f45718a257c2a45f574e8af619\n82eca5ccc5d8b09ce7ab6fe8ebbd95119fdc9484\nab1d6631e5a193b7ef8902ff2201c4f4618043fa\n4d3b1682c6b865b5367ecc13e32b2338e2f15a1e\n8b592bcdcb4b112516600b4be426f20558492029\na60b30d533ddff616416883e153ed1d9fec6a26b\n91030ad3b9e0b39481dc276fe243e55a7ddc219a\n1165bb26023a7b24757c7360657008438ffeed2e\nb2e277da3de0961b6717a7cc08bc23aa49de6960\n1f767694669f7b7484cf951f7057d05386900a49\n778c025c71c48dede13c4da1fe117677a5f9033a\n5f93fa3171f93c0edc84806c5924d6361644852b\na1e1a90f319fb0440c6c321e1ecfc6ea37002891\n75b8116f0e2c8e32e5a1df7efe04582ac9ce46f3\n49a18e01a1dce832861babbd2c15fa2079618389\n99ae7d5df176c47c5ea230f8392728cffbe02030\n66040b97ed790dc5312dfbc9e8592a35e6910c4e\n1583b45b01afe9434db335b92fa314a1e1b32f7f\ned8da72e7c2efe2cab3c103e6e2c2645dc1150c7\n15e06f183af7d908ad11e8b946f575d48323ebec\nbd863c42a007fb2e991d5312b03ab16fca92efc3\n5caf4a6578033a6e34c335b0b9ba6934eb9e0733\nc76115212f20709531696572e531d1808d06eaa7\n4c8b8da65f27daa006e41869fa6dbcea848134fc\n99bed286b6b15f62e27a13e614c00ed6688eab1e\n9a98edc95087411000326c3ef635792bf2399b16\n2cfd118ad8a498a6a59c04964d85c29d9c3bb99e\n5b3805a2715738d8cb999690f3b441c34de25a05\nc5dee805da8ec12b2c1cdeb67d371fe61f673ce6\n7c3d12fb7a2be0e0fe22e04b9947b299a700fc06\nb3e095f30f3b5498926016a3c6825e9982a468eb\n921324ddfa980fcc711de28abc906517ee4c40b6\n2ecea64d78def7c33892f5cea8d2cf28c6d27108\n2593339f116791259429a6ad1cb1e9712bc19310\n7dd8a17b8153ac0622ba98c720ea260b30b6b194\n1750b693ae732cdf6932c1e1b5d5faf57ed4ad94\ncfb00c047adb0dcb0415d1ded819063bc2ea8d3e\n93a98ff4aa0a6275db7ce16c01acabfb4968deb3\n0d5f2f340ce0326222c34c2d8613ff326ffc2345\n099d3584c1f3118b18af460c01fad9f6b432cd32\n882de80116bc399a484d6cd6afbfea244c6e3426\n6157a28a60dc731c02ac8fb31cc8591c04eb918d\n7558d895a2e25800ece0e181ed1b7788e800608b\naedfb0d1ee3b89213984234979f2137b2a59df0b\nfa3e1f7bf740445e180fc574fee2a15265d7c08b\n9d794dd375b30004efa9c5d0301522ba80ceb6b0\n2b15d386650893591c23669c2ecbfb529556d518\n73f6fea1d902e7a9abf341dde9d1725b17e854b9\n2c9d64d46eee85049b66048fa9eef659d826f9a4\n235f903c9b8c4897a03df7bfae30f1bf45ee2f1d\n9e596e2ce492b51c134c3dad22a1ba4ca35c3b02\nb6f2f27fffabc0b0a28774c450000e24d4567371\n7a2199cb5b30f0a8f08a5975aedbb718aae4778c\n66c3fe9e2253315cb6b3150eb23f0ac01f99f6b8\n960074e6bf4996db7b0c73c908aae5fea252623e\n44393a6e59288da83f0611f92e67119cf55b11a4\n7048f82452877ded0f3aa5dd6c51dc3c9fe3078c\nba2fb8652c31e34ed00a7b447bbcbca3e17e0dd6\n5b4c13a16c7ff45c7f7586ae9c2d0d8a9c1cf454\ncada6f3b68096eabc8f62c8a0b6f52f2f2ac181a\n906b33da57e3aacd23f255616057dd9da460f7c1\n6969d79f6b18b40cc065ffbc52191a5bb8e5f786\n20ad3212c883e78ad15895069a6f23b4e5a46ae8\n78b6265a9d3e0a2dc89cbf259b0317e8ebe22762\n349e61f8d63ac3e021b9181f92e49c7503678f5f\n14e6cb436f44a7f265c7ed613a2a9d52d313f056\nf7ef55ffd6b038fa002c795613e37a1c040ea732\n35388acaaa4b66c8f1347d494ec5e3f38f44a4f2\nddcbba0a2c19a2ae786675ecbf2bb40ffdab3a19\n6380ca9f409b6ac30e59fcab658ace2587ab45d4\n8322b7b03295246732ec4334f8c66c9073990faa\n659de73d00997d6ef6fbe9bace3a4c9939a5df3d\ne6ff5cf5cb93dae1a7355a6b495f94bd9c746d55\n0bfb0af828d877f5e65004dc3780bda2649e80f6\n45dccd21c2191002a5bed33c744a397b895c1b0a\nd726380487cef794abd00e00b27d3c535e18d92b\n5357705e425b25400ee584bc84d56ede14667eec\na1409c59deb3714ffab1cba0d486cb3ccb2fa51f\n013023749f30c305652c61d35ed73d4f58795809\nc3f97845900139ba10929105a9c3e8dca34c6672\n3f7f2a13de3b202ed48ecce83c8f093bcf421bd9\n3d133ee7a6ae25903e1cc01eb8e6a9384b34ac44\n4a0337f57b3727affc04511ce9547483986bfb77\n3674dc9a515e798d00a84ac4dbf22f7198d9e0ba\ncef282028674bf43872059c47c179dd4b0170572\n9c8404c23592bc4d5b6d932b55932b981a2f63e9\n67148356831d88c45e6879817903e8c24157b96d\nf7b184afe38ba624148fd86bc3ab5a4da05bd588\n3d275be3ee2013f36441293189b6af4ded7c7a15\n0708b729cacfcad3fd4d28b10f228bc1c542af74\nae02588f5a9b9cd669949895c527aee4c70b2774\nb6936e2311fa65faf0c3a17393d57f8227581f8d\n24306942fc9cc6c9d87fbd8d92be46558af62cb2\n3d81b30cf8af7ba01385391d40474159300a46a2\n645597d5629e3f45fe86c4e3d053aca3733f5d65\n8fcb849ece00cfcddd2c9f605edeff9a64c21ce6\n05c43abafeff549ecbd9e53cc35116bcebde1504\n6119a97e5eda62f0c9930266736db34df40d9ab5\ne6afe014e74faf0727ae620fdff2dd9b2620558f\nd824515e08296521080f0715d5b1a2cecab028bf\ne84029b031eaabf3e7b428cac52edb1026e05f57\n84c26dff4642f9f148a8a4fb3a24d24e4ba4c842\n36adeabb0282fa7aa704023be624704aeaa7737e\n8c5369e76acb622ae7c9e7b0a9f74a288777ee58\nbf65ac599de2482fd4016aba17374dd4365c6ab4\nab0422df0420288dfd62d32b0da5f33abfa95058\n1c309d0d097ce264b2c749277e22f6afb96d31fa\nf3c5d0af8f533c56483cc2c32dd59e55ef979553\n9fe740e810c39f990d8281bc33cf6614d0a12aba\n44345eab11a70553e1aa7c8561fe587279df20a6\n6a67776c44afc44deeb53558b02a76fdc9c176a4\nda8c57e2686d704b3a51357a70176c412816c2f3\nd9e62ccce9363a4e36520ddf53c92023c87fab33\n838d4db662059e1b0a3dcd9895b33acf606b5d35\n1bfdb1e48899bfe083d359d5cf3a1641bde446fa\nec2ffb962fe60b2874cd281f5519ba2226620d88\n087b3022cdd7d6fe2fbd47326f9a9bfdde8105ef\n3f6dec3f751e3625de3027551713e3a04af0940f\n77d4d74989a6cd745cfc20b016fd0cb28515ba1a\n6b77313ce0cacfb1dfaf08bbacd804652b763235\n515cf15dfc88da8999fa60e018bacf4040f2e64b\na56006401802aa6637d781fcb7216ed8d70d1d00\n6e3d80f7d4a3c6ab51e75d07a58944ce16a196f7\ne325034ee2f4ed1a4b7929b7a8100cd287d703ff\n9ae76d535a2fc2e746b914fbf83dc3b097f2855a\nd807063563a63b7451fdbabe27127f786e4b3ad9\nf2d8742473a1630d56bea75ac5dff0d01d9afa99\na26aee01aa92ab38bc22e8b71a9262a4b617a821\nd317f3ee531139dd11203d344e6bbbf9f4487d39\ne4b77fcd3992a422072251bbeae4132848d80216\n2d2d33ee45056b4e43d52d731e47bb59f93c1f3f\n69c3f84f4636dc56fdec90e2b2ab7dc09ad70a10\n871ca3f67956ee0a8911c9378643e4acafdce45f\n47c3c0f1ede32fbe67ae4136eee3a9ba20c6adc5\n3a0ad942a884c78fc01fa88ca4cb46a58c85064c\nfcdff6652036f4ffef6051f1c9454c4527f5e12a\nefb20947688a815fd2fe6a80f195486fb12881d2\n7dfc66fe597230c70477ab11cfdde39769ee4695\ncc9c143fa472cdbd7805fc7f55b30773b67c1bbf\n50987ac70b7e56827635bbb7cdf47728d93f630c\n28cf51f82d6992312b31bb44b6da8ce0524526d0\n20d4b7cf6e1e9985858c4ee2d88980ab01d66808\n808b69afc436d118108029034614c19e52b872db\n0cf97ee957242a8c63993253b38c6907507b413b\nd958c93d8bb186b6bf27b71b215bf5a43162efef\nc51b8d173491c8f031d228178411bc7f3c570be9\nfbe0b0368dbac3168e3b1344149a36f82bce698d\n52e8475c0a63aaf90888339b8fc7c09d33b7d62e\n74d5d64ea961bd4e5c7c0fd3cc08af9702960a12\n1011dee05818fc5fcde8cbd554c495378333df72\nf5c077f3fb4013d56891810f4c6c3a44813d067a\n63d466d3429e1c5e022da92b027ac6f972db6c66\n97e50e2c965a9f51aad3a29ff0a8d3da9098afb6\ncc0e3c88bd89802b937d7e6086a6efe1890fca4d\ned4e3511ad2d7d68dd8dc625c0cf369ee3b860aa\n019ed3d873e8befadbddb1f56c43314e9d2cb4f5\nc99ae5928fcfa2ea35b455e3f9b3ff95aa80ebe7\n4c5061669b68ab639c5c1d541684722e6731e73f\ne1b894edb52211d0f28c06e6f1144cac6b55440a\nea91bf399e03c75ad6c87138eea3974bf7eacb1e\n4cfca2ca106590bfc26cab6d867d9330393106d1\n79fbf8685209d7cd2f796a132a6aa7296106ea65\n62e57d310d794ccae30840af16211fefdc7b92cc\na3bcc40ab1ea1243ffda1e621c15fd8d645998f1\n97d82365edb7cb1c4169223f6046b9e7a9a7dbc4\ne7e33fed434bd06a1c57287865e4187ce9237818\n689259043d83b6e3eb598d138adf6bfbcd18f38a\n998b0be23813de5e4348296f0728e3eed9e51967\n2662c086013f5415753cce2c68e1b1b9af86154c\n50434336d76dd930cda3d78b5c783c5fe1fe9247\n64e257b489b20a21a51825dc26270887ce65d7dc\n8f403c25f510d4214b2b443ab70fda5dcb6e5112\nd1079ea98ed68b616e38d0399df138f32c4603e3\n6a40809978d10ffa91f7be5d4dba309b99843b5e\n083930d4bdcfab64fa0dee2e5dff0f89a0f0cc00\n56ebe63e308dad18280e711792b7b3639a445bdc\nc6ebcbbe2152f441a7d2144378d44b8c241001a7\n55c6d3a8a0bbacbda24099fb569320f0c4e39acd\n505160dc6af62806f7b7c413686ed997e77ff93b\n610834d41f8405322ab34d03cfe980bdc2a56d84\nbdb7edec43840d9dc08ee617189f028a270bae93\ne68e584ba418c5666457e1bc32ac203f79f2cc72\ncde2c99ed797eca9cc70508049de0de621990375\nf4eba5be0915806967fd6e14ed3f2b7e3e43374a\nc5fe042405e48e100513c545191cf4a575dd1164\n8d4c4f9393dc9f301f81265d86383dd56af1ce7a\n84fb9ac5f657b63463d0bafb64cbceefc8f801db\nec2ebaa5bbcba32a227d91376fc372e0dcd51a72\nc4f07ecf7334ebc66c2b74942cd8ad19c9b80bc1\n6a6244ae5ae66857116546a84c64aa70da9fef2c\ne8c3513f99d1f734281c893f2357374893a689d9\nb389459485722e58c508fff3829687f038a14d87\neb75eaf1e8f52081d3ffa19737cf1e386a8e9a69\n5de2b94560963733af93ea01cfb48fb3587c3817\n82d3f51b5a3f6517489599c678f815f19dbee1eb\n425983a27b432824a84b090c08b16ee3f2030659\n22f15c2097c4b895ebc9ee9edcd69dcf4ed055b1\n456b139ff2fdd8227774fd6e49bbf2249873f9ab\ncd6fb1e206dc29f01955896415b447752adc9eed\n34c97dae0dd8e3c52a4736fef455eb5c221f6bcc\ne4176de1db50571fe0704de7ac66ae7b33683391\nb63f23f6797f6b07bef9c3080bcc6049785af918\n6db1e3b0addf8721c2afa088efe6e07550413f6b\nb578812373655cd5ad6afcae139cea32dcb3350e\n15452463de0bc35b8cd8ba9602221cebbaf452d2\n46854615c9d3487716ee21defc65158fe9139998\n6098310670e6198e1dc71da0c19b47402b388f4d\nb7026ba379540cd5dbb1cab29b626e7878d69bd0\nf95551465323eb8990cddce09f0928a3f20dc9fe\neaedd02a8c38094878bec06cccd5820ced6d729f\n20cb405ed58d8ea5797ab611a06589a02ab42472\n341f1bb1b6ebd2f9e2a9e5bc80ecb8e924c9a16e\n74b14d739ff92554a2106839daddf0acd7aa4e37\n7df26ba4d81a09c01aa26660744ff0a011d38086\n9fdf5e42cc004a5aa42d57872456c04b94dba276\nf9713fb262681100a5b9e99222af4ce02ad87295\n658695c588c634cab89ec3a65054f4d89f9af18e\n9e637b7c2da277d8629ab68dab753386d116a233\n60841ddeb6f74b3691a9969c5f20d28058675287\n0c9fb3ef6d12bf27f6c034c929a30e8213caf829\n1c0045b2078d2dba2a87185d3665e8666c5b12b5\n62a95efd8e94072d5fd7a18448e1973407d144a4\n95a067a6f1cbf22c965fe6f015f1946d8247de53\n7c533fa92e67bf759c63705f3a2ba38bf17cade2\nf60054085f8d8395cbf020a86f5aef13d172b696\n067e979aff32592a34afc0daba1d4188ea9046c4\n55020672413646463728c5abf4716302161896f9\nf282dfac1f383786c9e60c4c43974a0dba9c2a5c\n09c22d175e78ea1bb8c08e55fd6b5c23cf798bd7\nce2273f8e4556060f2a349a657ab6e78874b45b9\nba3328fc3e2c5d0fe0aca3ead4d7abe4bcae0520\nc7ea4c6bfec6a648a34525726f804cdf018d8cb0\n6a21a1dd7725ba3f129d20028e5cd79453eb7f6f\n0e82f5cf587a8ca2fac5229af6e1da04fbb3c458\n99c76f0bd6098aa3172bdabc7ac0b4210c683d39\n82b7b1a39a16fdfdf891096aac36bd6ea7a22849\nd68e56df12b70ecf354177549c837deff39a10e4\n61a9dccb2b41ea89350d8c9a7e361952207e5d47\ncdd0d8a4c465e0abea94f38d4c2cf78326a3845f\n05dfa018657d794d8678403fac08f7ac841eba8a\n852d06bbf8e641b8090d645d5744335f51ae8e13\nd3cf85d9f3ed1f2d51c557097800b01735e058f0\nbf9af20b132e0e6c51db41fec889aa501601d9a9\n3388a9d66fa6f1f3a6f6528c4b862c82c53c7f5d\n1b4f27e33f6f20e60ee1604935509db03caa03e3\n42592fea9dbd6d08eecfb82d5b88954ddfe4e31d\n66c07ef48148c9a3ac21b1c34b6cee146dce63f4\n91b954a9ea9cb06507ac7eead1ff24349000bc1d\n210b71f1b902564a4a6d869d9610a580b916c41e\n6e1a4b67aea0c2422a412a481bc999aa29352103\n225f723393d2840c8de81236bc248ffb9d0bf2ef\nc53130200a6ef06497df2296aad1e6c9ed27f292\n5050ffda7bc5977df6e071fe881a163387002a74\n47dcb3be33f32f85f442300fd434f2cb2b19aa88\nb90b65e99ad68133c7b426eeb0972e77b55401d5\na306484ce89e49fc17020e70f1e5485d3d77f07a\na4cd1c7a7b945a9b347e7e7b09518703ad5170cf\nae2bd8ea022973b1d3cf74bffc0e420435aa0084\n65d9b412c8e628d5a59d6227f865e171d4d6af02\na17c6206d8f41cfe18d7fdf53c9394f46a2d7549\nd9136035e52196f9a9443248085a7aaa2ea07177\na1aa7b630ccb45487d329623e182c7bcc41a8149\ncd97478cba66bed7cdb99c7788b29f492acddef3\n89aba6b54851b992020db17c3ab237a5d46ee27d\n656429533dd95fab33de88f043d0cc6480a4883d\neb68428a3b6993fda4f8570e8e5c1866ab7b6df7\n050707569fb1e92d9d90913feca2c5926791fe9f\n4280b0277339b2dfc07456f7e8eb95df01825223\n3f8df7b2e0f6f44d385e36c9efa955cf1d313a5b\neb2a30656434bd2f8c8689980e595c0662215a14\nbec7289e3e52786ebd365ab8dcce90c37535df78\n5f8de2d823a05755459bc1ae151f2be58439e757\n52d2339f02db3402a12198733055efc0b833cc09\nd3e1601c9e5d02c76f65997773a66f69e4c18ac6\n0367725739cf98cdf7fd954656b5f0e5f7dc498b\nd5555b5a88904146f037b1777b1ccbc3fd585976\ndf6c9add73dde8126c0ac98cf21ff569c5a602e0\n7f9fb2325771005478fbf8c10f487149ac28e895\nac93fb29e458a49f2265b0a146165515df4f971a\nae06eeb651ac5ee3f36554102feb02d605726da5\n7cfe90077c726788d79f663143bf1c7dc55d6cb7\nd614b77f9c7b1f712cd2e599688dee294a9bee55\n61074d151d4dd157290d7e7b0210570807a499be\n27c00857bda1ec3dcd1d73d52fed8156be102825\nfbf822d73f3a09e9588cf05c1c79e1deb8b03f3e\n31f9a5d9a334c0af187ddb8fedf3d6613b029351\n8d387c5bc356376b84696ec825e914a8e1eba605\nd06c51b6a55177e102e796aca34750d9f042ffea\n69831efae2d540363e6685adce5599aeffd17e30\na3d252949c0a867abdcdbc2fbeace5813875d50c\n2c7d51a7e874388f3b62f3000b57decc1d906703\n08d10e0e628dda014c9fb1331e2b9700096ea2ca\n525b1f44557c90cbd98440092fb53321c3c0ff91\nfdd79140e22e4802f865460df3a75a0ad50e66b3\n210a2ddc4420e829cf6a2d9c135724e05c1219c4\n25c9393d1160c74c3681b7588eb4f5391da2b806\n25fc7e99bb85bad0970c9d40d3c061c95f4fa04d\n32d177564f89e3267d6ee9f88b32128dc6f66435\n31dd5677af4d9640a92061f0fdf039adb647994d\nea93010271a1bef8480a93a87ea75e3e068957ea\n332c0bb6b79b29fd56a283f6762e72e8133b755b\ne12ab1b48d452f1554142b63c5832e7be1058837\n3983711e773b9f6b41e4ed6df96f881c66a0586e\n2207add74d482c604cf70b68f7e61f72d235a96e\n860ab8ee1a68078c7f5d871fd27ac1348e3d47a6\na1b39356ab30428b96f3815adfc6b6592430dca0\nf50a31c8df66b3ac8cfb46c5fe8f291db60ebc87\n30d36d670bd96ace7dd33132387e01e73750021a\n84cafa37fe15f86334ad6c156aaff6b5fdbd196b\n14d3c26782a92adf62dc26276f43e3b30839fce0\n2b9d89de6ee285b749028ffc6fd2544118ff484c\nce9f450ea71a35b9e08a1b936431d7750c8d0ea3\n79823053f92679a78799d946af4f76021e3d4884\nc02abd219f4f5eaf295fe0da9b552190c68b62c4\n179cae55486547db2fa5efe8101a9820437adc41\n92e804b0f004a91c0e47d88b954b8bc1d882998b\n17680f44b08e8891659c7bc09bdfe637cf632127\nf96c161f8bab289573e1d4a6cf9dc0fbf3212276\n25b110bef1346ae693673afaa2a04553faf99952\n244bda0fd487d67b3d5e42c2b1b6a9f8e53dae97\n194beeaf4cb72efefce02a69c6c48f5e9463419e\nbbe0919f659d081ddaf07890b41a56854bd8660d\n789683f2bedd0a502e76e19ee2f7dce42023dcae\ne39f4cb5aa9266b913b79089b6166a1c586a726d\n1fd189ee59969884f6d0872df1aa30d3f39ce5b2\n43246b5426badd72a5b2ac690f2d78ca14271666\n4556d480be5c0c11d7c91152bbbbe08958b6f02d\n3027ebd0011ebac64e8d8665ae5078500c72e921\n9e9755bde5835ae4d842741b6b4071b8e1c43799\n83936bb0cbfff957a846d55acac27b2a2c1d8cd9\n0cb7af60b073f8ecd78beafdb13cc540e90a2a6d\n1c9d98e912dcd04069c0fed2d7680153a97dc42b\nd0b5c970a246391e175cd1762138a35d35dc74da\nf06d26c7ec49e4cb3e44c3f4ce2696562cea8612\n0aea024cd5bb0fdfbfc9a757c7a95e53977d27d7\n21818f07eadc647bf227fa3ba2ccad0fe81195f5\n0df96f063ed8583a16434c58a09761d65ab1aa45\n74fbcd830f062b37579daedb439cd29f8fc3426e\n5d43060eae83e900f29ed87ec7ecc7c0970ddf36\ne8ca9ef732a81609097a40e66cf4595607dc01ad\na227c1647381c5c70524fa225454e67bbf0eab51\n4482f38fa394bff1d040d7af6ffc3197c8cc2fe8\nae9c8c4c1620295fefa2f6ed6c9204f64e796a5f\n34119ac35b82766fbb66f4a3fad07773c20ee08e\nac97aa65f331a0007146d31343605d7cff480947\n32eb94145b2c89bbcf1ac4059578d99a1905c64b\n8e1cdf4240c9f196cc69263c247ee7815810b5e8\n159d6005413754434b117140ab19ca173a115ab5\n9db51f765cdf90024c8ecabba4031d8e5142717f\nf0c36be712f69718f3757c8bf3a349cc75f0b61d\nd40bb4dea60fcbfae7e84a1e8b2669ab31d04944\n2043e95408976a879bc153e9f72725fa43e79f71\n8a86749753e83b9516beb9224629cba4c142bf9e\nce9ef24adf7c44c087c49b85cd1fb2c1f6fd074b\n34b15687a0ee6fb6e529d3d652b714be9d53c230\na76a8ec50d485591f24f1218773edb766d9feb4e\n5ecec325b90e29462174bf36aec44fb3ac57bf13\n62c1ba5ad7c5b74488f9f8c4707504202ecaa498\n24af07468760a7d58cafadc94ff648a8d279aa34\nfaeec111a401183da874d0feaaa63104da45f07a\neaa5c1d1256c2c700621d7ab9fbefb692217e93c\n604f8b1ed33a7c3fa93889ed77433fa8396348c2\n72e7c725860c2656f2d24cb850b2535aed6c1f7d\n65720cf08d35968c63c7b85b67fe7ad4f7c0b07a\n0244a9e4fd8a186d2a232abe632e75bb33d946ba\nb67161169bbba39abb6327c9cefb1ee28961e743\n057a9c10c0a2da25d237e953f34bb05bf417fc4e\n7cfe35f51f54199b21541dd5d1d171c11d1bea3b\n90033f6fe351f1862ad13e937442c10814f22ca9\nd98c01c94caa8fb1196ecd99a133f147a9d77e32\n533adb9aed63672d170d8e7c75c8ffb694249d10\neb69b92f8964cf16740d8a4c6cc86f7c40e2a613\nfef2d3bf8aeffef583fc9f083e2fa5b7b2fec7b6\n401acdfed4df133a6a02e753def5aa6c5283b630\na737580886187327c2e34c73d21c14fb054f82bc\ne846c2ed168aa164bae78bac7a101d79d2728c0d\n1f4957ca82af70f81849d113ee848db38f37fff0\n40b13f23a7fa7b0a9fc81002619cddc98019d1c2\ncc021b41837dcf733b79d92a2dfb31a63af81451\n4451243dad5172df0253b01c6cd4215859a4fb29\nae4bccd75b75d6bf800cfeb8444788803c3ade2a\nc4f631d185987e98b17243f406af58941b0c0a64\ne51f3bbcc586e59b0e7712448f9e9e1587d76b64\n247a804eef2191cf740e6c01ef03ab7bdaaca0f7\n99eb299b51fa3a3e2d925472900e3c55ea2d545a\nafdbb834d28964461afeb3059b2363088692311a\nc087c50b9350c2a5e38107b9b37b7ea192c618c0\n26b951e359896d96a4ce11cc279d26de9dff854d\nc877002e29b69b5a1e5d54318317124e8a4d66bc\nb8f206a19ca75c25914dd7f7c1a246d9404dc235\ne4413eee1fb2a137ff9628fe4cf7863c34d199e6\nce18ec722cbd8e69d08178ef594768ee10afdc68\n2efb34fed0631b19312fc14e958eb79520e3066a\n68eceda6b12cb556d57d9a37387be9bc566ec353\neb7bce74395a989ffa97021d37ac6876a6bf0211\n4e481da4931c9dc31ddf420a78640910ebc88724\n23d2d38febdcd7c166dbc3e0c54f6af0afe9580d\necada9a3a8c3bfb7f046a529511954787cfb374c\ne9557065aec29b9b749007d8ce2e554bd667e385\ncff11fb0915558adc02688aba563ad741b48bedc\n1e82263c7d8a89158791b024d8b09385a4a14c29\n24e6db8e5157a6398a937aeef764fd3aba2eaad6\n3dbe7b98c7ae4d59ca0228130a5f1628a9bad658\n0695c19827c5c792e8be0e3df05f14c23922a318\neea952b11083b9a651e568605fca196bd92ebe12\ndf392036e356dfe86b4941f576df8d69db7f75ee\n5952b9b095b21b5e7b98375ec0ca9eba63a5c6fc\n92d22847128a8ce2ac8c3e0b202400cf3d4bea16\n685a01df3f8e8b610704515029fc493d11b1b3af\n7ec83e8664298ec47f8c88b4860f31e6c34b7652\nf1dc57887b223f9379e831583629734dd9786eaf\nec42725b41311be22dc6c4834763584a88382a5c\n537a9c2853e72dcade24a8cacbc095a1395ab5ba\nd9bbc6e79b13a20b7f760d337bd8bbc59ca4b7ab\n30a055177292dcc3e3b23c99d469984b478d04d4\n15f0f27b64f9f6bc5331945397d4b59b7fd3567c\n2f087482ff0428518cde1258be49d247180d26a7\n5942ccef53964881d4497570c3d6c902eb906e6b\nbc07bb51a36a32ade8d3ffda0ddec66c04283934\ncb161110adb2fcedcdf9ce0cce875037b1b4cabb\n89ff2191ca0782707302f6978f66261be38e8847\nbdfc4db674b9b4524bf91fcf84ff97f98e7472c6\n77ac196b2a3270f7ad259f1aa5602b02695d3e07\n24a9407b2a713ba31bc76c8958cc92cf993ee98e\n8fe5ea6b04a20ebadbb3be2f439c63a5257c0c07\nb634c0cc239500f8420d84c2751db1b1a44b6fb8\n63da09ec9d97a23a789d99601de00f8445a6ece6\n321e790f57468ae335d2a0fd321c441f3100f38a\n87742ca2d658a60048d1c61f8d410b625021af7d\nfbaa02fd24237c9c4d809f64581696b35784943b\nbc813f49869bc621d16585d7648ae5e2c4bfe81a\n1a6728d9a5a2bc8e6f3febf66414c4dd6562df11\n4389700a37b7c41741ad9e6f25743072c3b41889\n3c3ba20ad9f923cfd9be6fec74a5100fcee432ed\n0cebcae69d28faebb58e1f37f9c9c16ee8b097b1\nf3bd49a0c980055d373007a7eca60aa24aa1464c\na72d5ace87979b2b1647ef627be4ea8cc40d562d\n3f6ea6ed37ee0948cfa574897261f181ab0af0f8\n3f8691614b484f0060b72cf67442bc1a1b79844b\nf19f87d7d4325eedb4a266db47cf9a495f6e1ca2\nfa9806ab3573a9c353a222da85b041fcc1d87a7c\n7f96b230c12f91c3f83238608fb07ff53a32c08c\n461c71c97f1e017f0f8827570d5f95f2c741bcef\n51f8a8cad4f6faae2f98ca47f47de6670b765786\n6b4608c72aa90e30910087cff27b192f18e7c9d1\ne682af00fb9e952212cb0eaf63dd5d9b7d2a420b\n17b89eea8920d9feec83e8ebbcad6c5063a6a608\n864cb5b4b1b86e05dce86410deb6971af9ec9d53\n093f4b1b000707456dd95dca2d21b34aa0090af4\n574f0d60efa09a5f736ab0cc096ba30114ce5b7d\nee9b1c68d5c3770a80c178d0c967ce55f299d096\na5a92ac439283a2876d39bce2e38348441451c99\n6f7ec9398b602574396786567bb735ff00d954f8\n270505741eaf8f7325be7ab3277c76abf58e9045\na8b26db33bfb995c2f8298654c183eac11174b17\nbac4034ad784e796bc013d874a874e0c38777bcd\ne36449722cb15fbf56b65179c8ea6793ec8be906\n47333b5cde16838ba7c4a8c64e99445f53c4e7f3\n4d7d4e84888eeeefd12f3692320badb19040fe4d\n6117b3fc5b30dcec0e3a162d6df5f976db6d1bdb\n0ad302940d9e883763df68fe40fbb560aaaaccd6\nc3f8fe6d4d980230dc9a32b6d20079c86bcfdd51\neec8c7ae6a3c49b1fd7a58fa1edb4e0a4fbdaef8\n97e3ea8d6762476226e00e110d8a33e5630a3a6f\n5f36971c01d9a3bc52ee999454c0199eb27a7ff8\nb07a5e065a16755cba48d638d72b28c2bd111dac\ndbec72854ac57ee4f2937b09eedf373bd49e6e19\n4b5a7621be2f99cb6ba75476ea9299e2155b9d16\n4278d5b249e8f0302866777e10d57ceff32945cc\n5d6cbfff711d6721af44702d9baee487078d77e3\na64b491d710f1258e6b1fd58772726a459ac8e91\n14f87963c736094ad35c8e0908514670b2111774\na8f0df2a88064d31ed445a05e032c69ca490fc82\nbb9adc17826e2b4df40255f3c7efd366541e1795\n088ef02a18ffd0c57fbcc5565358905c624a3dda\n0c689b9e7d08a64c320d4769ff6d312ec61f1b00\n94fc56e69265f0d61d0cb7e896335d017b77747b\n0c5292d265bb33dd3be8b6d217fdfbdb0ad7bcd9\n35989c8bb8226c0d15bf02cd07ab23a5237d6eff\nfb1385b87914ddc040630bbe0d10c5a40bdc8b99\nd9a3d2dc612cd1c5956ae98bb9393c4b58f94653\n9caa328dcaa35b751c48f2e2197ce92104cdf132\n3fb6f45c7a48a148cfc94afd4475e90a8d56f15f\n1bbb6616fbf5393a984bda57e2d7423d15e06d7f\n8c00bda7dbc94eae694acb5e0204c8b12eec633a\nc49f1656c78bfe96ea2c562f3bba3f80e294157f\n9da44b990faf882f0988bb419456fa7a466e3d67\n882cb7a3eac6de74cc913ce9fdc898c8fe0728b2\n2da11a5f987d00e235ec6b7d7ea5d072b6a36198\nafb668cb39cf623379989209a3f6cf4584eae71b\n224b098a0dfc5adf8da010713ae55588d411c011\nb7c3abf5004ccda474507dd39c0c52d579186584\n63496f31a39d0bbfc35460259d90e4c5053c1db5\n52189aced1a306c35cd2ad4af6db8826aa879837\n51e91ee29e272e8d6af2854b46ff54096b942108\n669cf794d179668c1176d0f7b3e9af0cc266187a\n9c8444ae737cab7a94bd62edb0e06a70699cdad1\n03c315ebbbfa321d8494f2ee5d32ad09ed2eb4a5\n7bd1de71a7e6a8a4e2104fe8a92255c957f2094e\n82917923a05f944ff8e283dc3846e06fb4a97b7c\nf1c1f30dd2b9a0720f3c2821c11a65f6908e50c0\nf6912be94e6e4cf913c639398cffac55c5c96fa9\n00538e7cc48ade86b72f17a41c4e5dd28ad6051d\n659dac168fc571efe6872ed61abfce4a5800fb2c\nf7e26d7590574085fe389c8f8a8abfd28cb20499\n89a2de5b935125ab917af7f8107d7f93e80f7cb6\ncdbc3d9be43d180f90d02ba4dce6fb76c6d25774\ne7a7d4b282560f435be995fe0a08f992d944bee8\n7b958fda8aa5befdb950b226214cc89a982f696b\nac333d0828cb9e772799d33a99ee2931fa6800d1\nf8e6dac639b6437f73ee607da87f737911aa9c63\n5152e54cd53dda2261269fe0ecbd4e73b1d54e79\n6a58a375b667a3447bf28527937ddd2cf8ec3a95\nf98711e799efc4ce3799b4ffde9e021a0f2ffded\n0cac2e6e1a0642f65963b2aceb34a38dce332956\nc6d6a174294ac9e6598f4eb7e44114419e083474\n81f0bcc195e06efeee1e1c1f38a5e857aebdbc27\n6ab256bc23ca59ebca0e282732e9bd3e1e8eb41f\n30a0d5b5f6ed421bff78e92320c91841aa7fc5e1\n1970e0617e9671f155b866f7b27c70d91975f153\n0fbfa1e96b069d605d5cb6fe91aa7e88baeb46dd\n9b981ea6e8cbc1c9bab4b23576bb58dd3026ea42\n7b12db3d9acd7a1198dba333387b37aaf7db2ff8\n16773e75599c05f867f44fa622636031f75bd8c8\n00dc8386150c5da6207cd6f59a048ae26e38e136\n25479f2a5fffce7ff304d6b38c979581b32dc801\nfdd69aa8e1d79ef14fb0448b3ee702982e43f9da\n1bc9419eb983a1bb2dd99645476b16670f5a565a\n4ff4d11309aa6d40595ed6ec89d800c39bfcdf9c\n015fce0deaf6b2edc5b4072d0fdddd65096f61c4\n2918708fb475111254a86f6303a0e0fb23ac0c6c\n763434d4868607ea78d5d1d233551f41490f6c4c\n9161f79fc60c3d022be68c1c919168705af970f6\nbdac91314d94d450695afe886069df1def8cd1af\ndd125a19e527939ff5c128856283e0bc4bc0def3\n0ae571409ca712c0e6032ff34a662fea009fde7c\nd720a14a295d5c1f73db93e8b65b9fa459464018\ncb5b188dc0b07d264bbe59f0507a56b1a2a14ea4\ne7427ec396688045c49a33df0bd997c6d0baf078\n3f1075a79528d14ea34c921fab1a79791af034be\n88bf113c19036b381fcc477166af1c2f98bf3e8d\n3d8be7ae7c28d86d733165b135781c8947e3330e\n61df3f49ca44afb570b4c7508238b7ebfebaab01\nb95cb445d214687f20e728012f07606df5a6e9f3\n6f7ebede0ca8a72d6b6f7ccf0a9106bb862cac39\nfeb0bebc498f8bf64ef9f2286c5c1cebb5127c4f\n493840379aa8b915916a6ae6135ed8ccc3038bea\n2fbe8142d19ed8cf0d6399f63214adcff09c1325\n592e3a4b0501cfa5a0dbe85693bd33cab18e5ce9\n4dedc70f7dcecedc52e5f8dd2532ec9666308c96\n1f65b29dd8870d17100089e934c27e3774d136d1\n5cbd3ea1b5a3b584b4fea043c9ecfec6396d9ee8\ndbb626353928f9a130d707c057b038ff38ab9dca\n64a24f404516f0c54222749fe7e924cce6706b40\n0c40065b190b1eabb3ec95eb835a2f8decac81ff\ne029468a13c3e8c0b6030a3276a179990d1de11e\n8c3eb214a98cad32e9798a2082170de6fd040870\n5a08d61c877932e39b997fff9e9980b2456dc348\n1a77d09fac5c722e294a1a8f9846758caa5a7c2a\n918b7ba206cc5e0a97dd86e82695772c25ce8347\n7b433cd386abbb190cbde543708eaaf0dfd567bd\naf7d0f4b31df1bf21bf91cba6946685235ab3ece\n55e4d2a715c262dbdd5df12944e966bef9ee3d72\n25a091ed28521fea47632909651a725fc7eca153\nf46610f88beaa8b937097461b5fb77d2967f7016\n0f56674cf0f18f39e18918c2191417b69ed2ea82\nb5667380011d1f940fd03d2f22ffefd380fe816e\nec411e94da0c4dbcd4078483689d0f409d59d278\n8838b5495fa752aa388c09eb28be989f320269f0\nd0b603a0cd9ef29aaaeecd5094df91a29222a4b6\n18b6620ad991c1e7896cf605377be10fa6569387\nd92edfb0b0d787f072513b68b9aff16f2021a678\n91c0c1881b5186f479db3f1046e207291b077582\n68abc449f309e8b27c18f7e076721736620a1f21\ncce6379b13070616fd186c01dee622366ec3c517\n69195deb45197ffd6a6a78a72df65df3108fff6a\n798f3bb9f5eb16724ad4eef99967155ecd00b602\n6921ff7af74065190741e1a718f8a0551825a095\ne1490bc7b17ae36c6f081b092014754ae3a8c115\n3be4ee51309b570f6617403f24b1c8662f35c487\n6546ded1458f724d33ffe527cfaf453d8979da0a\n74dd6ca3e4d2f88db6d8205b2118a8e2f08475ed\nb127757f8912661d9d9292db7567782e042d464b\n3994d71451bc8596310945e043cf31018212d815\n23e91efb4050638a050125af743862915875bc11\ne90589481e62211a0c57fff537c20db4226c3a47\n9571c5a872379c646a04d0b2701602d3cff93521\n7c71eb8fb1af846c5acb56809d0064a3e1ae25f8\n2e195af1554cf51d9d57ad11b63862e9bc6c64e0\n5d8e7ca80a359c04b1e5839266647245c8b05a0d\nfbdd0b7881be637f8893f8f138805e56d06aa22d\nf852135775123a7d2f4d4338038f97bfda21efd0\n17555a94a9fae749331872c0cff2da8b1aca42d4\n0df719c6bd8d001311418c3b4bcc31d417d184ab\n67e9f09358bfd180d71c19d12377e72b8601a7bf\n38f275621c9727894c80d37922023b1e82a62c13\n57f965d3c06817386be9519a1384f699d7ddf523\n4b3798280cbf3b9735a54a982868e8cc91eb046f\na72121b9551921aa3dced32d943c6034ba318f82\n955add0b09637e7ecdd62411272b2d0ef84d3aa3\nce6c5aac0db5476dc496c34388e4f9ce2c4b86e5\nb46b1e64f06f448bde78b98e3ae8228ce5f96067\nEND_SECTION COMMITS)\n[//]: # (START_SECTION 7c86d935be95d9838f45727025d442f55a8456a4)\n### Error fixes (#677)\n\n> Commit: [7c86d935be95d9838f45727025d442f55a8456a4](https://github.com/dOpensource/dsiprouter/commit/7c86d935be95d9838f45727025d442f55a8456a4)  \n> Date: Fri, 18 Jul 2025 10:33:42 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- * Update v0.78 Upgrade With DB Changes\n- * Fix \"sdp_transport_helper(): malformed or non AVP $dlg_var(src_media_tp)\"\n- `sdp_transport` does not support dlg_var, switch to pv\n- remove inaccurate dlg_var on MANAGE_REPLY\n- set default for rtpengine to enabled when dlg flag is not set\n- * Revert Media Handling Improvements\n- not enough time to revalidate, will revisit in next release\n- fix rtpengine enabled on direct media\n\n\n---\n\n[//]: # (END_SECTION 7c86d935be95d9838f45727025d442f55a8456a4)\n[//]: # (START_SECTION 844fdfd70c526e3e3f4b94ca91de5a74bab12213)\n### Fixed error message when configuresslcert is executed\n\n> Commit: [844fdfd70c526e3e3f4b94ca91de5a74bab12213](https://github.com/dOpensource/dsiprouter/commit/844fdfd70c526e3e3f4b94ca91de5a74bab12213)  \n> Date: Fri, 18 Jul 2025 13:24:20 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 844fdfd70c526e3e3f4b94ca91de5a74bab12213)\n[//]: # (START_SECTION 8261be6d3e1772d94acb1d8c8de194a0b4afbdd6)\n### Update README.md\n\n> Commit: [8261be6d3e1772d94acb1d8c8de194a0b4afbdd6](https://github.com/dOpensource/dsiprouter/commit/8261be6d3e1772d94acb1d8c8de194a0b4afbdd6)  \n> Date: Fri, 18 Jul 2025 07:59:55 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8261be6d3e1772d94acb1d8c8de194a0b4afbdd6)\n[//]: # (START_SECTION c1b270c5efb9f1640ed0a32a7d9c0d7f96b1a02d)\n### WebRTC Fixes to handle a more compliant WebRTC client called Jambonz, which is based on JsSIP\n\n> Commit: [c1b270c5efb9f1640ed0a32a7d9c0d7f96b1a02d](https://github.com/dOpensource/dsiprouter/commit/c1b270c5efb9f1640ed0a32a7d9c0d7f96b1a02d)  \n> Date: Fri, 18 Jul 2025 11:52:25 +0000  \n> Author: root (root@mack.test.dsiprouter.net)  \n> Committer: root (root@mack.test.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c1b270c5efb9f1640ed0a32a7d9c0d7f96b1a02d)\n[//]: # (START_SECTION 58096c74533221acc325c101627b8f8a8ada1282)\n### Update of login page and logo\n\n> Commit: [58096c74533221acc325c101627b8f8a8ada1282](https://github.com/dOpensource/dsiprouter/commit/58096c74533221acc325c101627b8f8a8ada1282)  \n> Date: Thu, 17 Jul 2025 04:26:23 +0000  \n> Author: root (root@mack.test.dsiprouter.net)  \n> Committer: root (root@mack.test.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 58096c74533221acc325c101627b8f8a8ada1282)\n[//]: # (START_SECTION c8c6ba9ded2fc109099a8fd3c9e7775ea61560c5)\n### Update v0.78 Upgrade With DB Changes\n\n> Commit: [c8c6ba9ded2fc109099a8fd3c9e7775ea61560c5](https://github.com/dOpensource/dsiprouter/commit/c8c6ba9ded2fc109099a8fd3c9e7775ea61560c5)  \n> Date: Mon, 14 Jul 2025 12:16:13 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c8c6ba9ded2fc109099a8fd3c9e7775ea61560c5)\n[//]: # (START_SECTION a252b7e6646a684a44946f48bc93f78335d559f4)\n### Fix Failure Routing\n\n> Commit: [a252b7e6646a684a44946f48bc93f78335d559f4](https://github.com/dOpensource/dsiprouter/commit/a252b7e6646a684a44946f48bc93f78335d559f4)  \n> Date: Mon, 14 Jul 2025 10:53:06 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- move 3xx blocking up in faliure route\n- move timeout check to after failover/hard fwd checks\n- dsip_prefix_mapping view only includes inbound routes now\n\n\n---\n\n[//]: # (END_SECTION a252b7e6646a684a44946f48bc93f78335d559f4)\n[//]: # (START_SECTION 31d9a61fe5e5698a83ba460a347ca2d9d681904f)\n### Cluster Fixes\n\n> Commit: [31d9a61fe5e5698a83ba460a347ca2d9d681904f](https://github.com/dOpensource/dsiprouter/commit/31d9a61fe5e5698a83ba460a347ca2d9d681904f)  \n> Date: Mon, 14 Jul 2025 09:44:50 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- do not allow debian images without systemd-resolved to install with `-dns`\n- fix return code of each install on `clusterinstall` subcommand\n\n\n---\n\n[//]: # (END_SECTION 31d9a61fe5e5698a83ba460a347ca2d9d681904f)\n[//]: # (START_SECTION 902c53e6ea1b56ba293362fd7147f954fe5b87f0)\n### Extra Check for Carrier Load Balancing\n\n> Commit: [902c53e6ea1b56ba293362fd7147f954fe5b87f0](https://github.com/dOpensource/dsiprouter/commit/902c53e6ea1b56ba293362fd7147f954fe5b87f0)  \n> Date: Fri, 11 Jul 2025 08:25:06 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- if dispatcher entry does not yet exist when updating, create it\n\n\n---\n\n[//]: # (END_SECTION 902c53e6ea1b56ba293362fd7147f954fe5b87f0)\n[//]: # (START_SECTION 10cdc797dddb3053dfa574dcd10e5fe0696cffe8)\n### Load Balancing Fixes\n\n> Commit: [10cdc797dddb3053dfa574dcd10e5fe0696cffe8](https://github.com/dOpensource/dsiprouter/commit/10cdc797dddb3053dfa574dcd10e5fe0696cffe8)  \n> Date: Thu, 10 Jul 2025 11:53:24 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix load balancing regression\n- set more sane defaults for rweight/keepalive\n\n\n---\n\n[//]: # (END_SECTION 10cdc797dddb3053dfa574dcd10e5fe0696cffe8)\n[//]: # (START_SECTION 2485a964ccf7d5eab5c719d9fe1d24221e79ff41)\n### Fix CLI Debug Handling\n\n> Commit: [2485a964ccf7d5eab5c719d9fe1d24221e79ff41](https://github.com/dOpensource/dsiprouter/commit/2485a964ccf7d5eab5c719d9fe1d24221e79ff41)  \n> Date: Wed, 9 Jul 2025 11:00:59 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- `dsiprouter start -debug` now works as intended (blocking)\n- allow environment variables being passed when started in debug mode\n\n\n---\n\n[//]: # (END_SECTION 2485a964ccf7d5eab5c719d9fe1d24221e79ff41)\n[//]: # (START_SECTION 37fe6c03ed7e798119732367dc1896fa93d3182f)\n### Fix Record Routing for Some NAT Scenarios\n\n> Commit: [37fe6c03ed7e798119732367dc1896fa93d3182f](https://github.com/dOpensource/dsiprouter/commit/37fe6c03ed7e798119732367dc1896fa93d3182f)  \n> Date: Tue, 8 Jul 2025 13:16:09 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 37fe6c03ed7e798119732367dc1896fa93d3182f)\n[//]: # (START_SECTION 49ba3a7b761ef8f7c71de501c66bdadeeddc70ac)\n### Resolves #640\n\n> Commit: [49ba3a7b761ef8f7c71de501c66bdadeeddc70ac](https://github.com/dOpensource/dsiprouter/commit/49ba3a7b761ef8f7c71de501c66bdadeeddc70ac)  \n> Date: Tue, 11 Mar 2025 11:51:05 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 49ba3a7b761ef8f7c71de501c66bdadeeddc70ac)\n[//]: # (START_SECTION 5995b5b7296506b72e5863aba07d1343acb9e113)\n### Resolves #637\n\n> Commit: [5995b5b7296506b72e5863aba07d1343acb9e113](https://github.com/dOpensource/dsiprouter/commit/5995b5b7296506b72e5863aba07d1343acb9e113)  \n> Date: Thu, 13 Mar 2025 13:21:33 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5995b5b7296506b72e5863aba07d1343acb9e113)\n[//]: # (START_SECTION 23b1395497f61ec9a8f60ab63dd394397e1866b6)\n### Fix Typos in Deb12 DnsMasq Install\n\n> Commit: [23b1395497f61ec9a8f60ab63dd394397e1866b6](https://github.com/dOpensource/dsiprouter/commit/23b1395497f61ec9a8f60ab63dd394397e1866b6)  \n> Date: Thu, 20 Mar 2025 12:04:50 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 23b1395497f61ec9a8f60ab63dd394397e1866b6)\n[//]: # (START_SECTION 3007c0e8499cc95673704a420dee938dd424ef9e)\n### Fix Amzn2 RTPEngine Install\n\n> Commit: [3007c0e8499cc95673704a420dee938dd424ef9e](https://github.com/dOpensource/dsiprouter/commit/3007c0e8499cc95673704a420dee938dd424ef9e)  \n> Date: Wed, 9 Apr 2025 08:24:31 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- bump rtpengine to mr11.5.1.11\n- update dependencies\n\n\n---\n\n[//]: # (END_SECTION 3007c0e8499cc95673704a420dee938dd424ef9e)\n[//]: # (START_SECTION e1e287dd579ff03c0407b90d047d76e10a2ad52f)\n### Fix Amzn2 Kamailio Install\n\n> Commit: [e1e287dd579ff03c0407b90d047d76e10a2ad52f](https://github.com/dOpensource/dsiprouter/commit/e1e287dd579ff03c0407b90d047d76e10a2ad52f)  \n> Date: Mon, 7 Apr 2025 13:56:51 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e1e287dd579ff03c0407b90d047d76e10a2ad52f)\n[//]: # (START_SECTION 8f427d4f46d64c4ead9c32402d56f509f63850c9)\n### Fix AWS Image Creation\n\n> Commit: [8f427d4f46d64c4ead9c32402d56f509f63850c9](https://github.com/dOpensource/dsiprouter/commit/8f427d4f46d64c4ead9c32402d56f509f63850c9)  \n> Date: Tue, 1 Apr 2025 14:54:49 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- various updates to cleanup logic for golden images\n- add support for `dnf` in cloud build scripts\n\n\n---\n\n[//]: # (END_SECTION 8f427d4f46d64c4ead9c32402d56f509f63850c9)\n[//]: # (START_SECTION 155f1594e37d88e14dc9699cc75cba04f3d6b2f0)\n### Update Demo API Token\n\n> Commit: [155f1594e37d88e14dc9699cc75cba04f3d6b2f0](https://github.com/dOpensource/dsiprouter/commit/155f1594e37d88e14dc9699cc75cba04f3d6b2f0)  \n> Date: Tue, 11 Mar 2025 13:30:00 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 155f1594e37d88e14dc9699cc75cba04f3d6b2f0)\n[//]: # (START_SECTION ababcdd35782a7234b14e9a8c9630e2127bfc52e)\n### Add Support for Upgrading to v0.78 (#674)\n\n> Commit: [ababcdd35782a7234b14e9a8c9630e2127bfc52e](https://github.com/dOpensource/dsiprouter/commit/ababcdd35782a7234b14e9a8c9630e2127bfc52e)  \n> Date: Thu, 3 Jul 2025 11:17:04 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- * Add Support for Upgrading to v0.78\n- * Fix Multiple Settings Effected by Updates\n- fixes `setConfigAttrib()` matching other settings partially\n\n\n---\n\n[//]: # (END_SECTION ababcdd35782a7234b14e9a8c9630e2127bfc52e)\n[//]: # (START_SECTION 73cb9b23631150873b18c39696018a4f599738a9)\n### Add FusionPBX Provisioning Permissions to chown Subcommand\n\n> Commit: [73cb9b23631150873b18c39696018a4f599738a9](https://github.com/dOpensource/dsiprouter/commit/73cb9b23631150873b18c39696018a4f599738a9)  \n> Date: Thu, 3 Jul 2025 07:50:02 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 73cb9b23631150873b18c39696018a4f599738a9)\n[//]: # (START_SECTION 303bf86be04320131b8fce613e19aa71dfd7ce75)\n### Version and logo update\n\n> Commit: [303bf86be04320131b8fce613e19aa71dfd7ce75](https://github.com/dOpensource/dsiprouter/commit/303bf86be04320131b8fce613e19aa71dfd7ce75)  \n> Date: Wed, 2 Jul 2025 11:16:53 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 303bf86be04320131b8fce613e19aa71dfd7ce75)\n[//]: # (START_SECTION 52e07933e0845f85c95c6c05af3e4a7b9ad3f2c2)\n### Fix from merge\n\n> Commit: [52e07933e0845f85c95c6c05af3e4a7b9ad3f2c2](https://github.com/dOpensource/dsiprouter/commit/52e07933e0845f85c95c6c05af3e4a7b9ad3f2c2)  \n> Date: Sun, 29 Jun 2025 01:28:43 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 52e07933e0845f85c95c6c05af3e4a7b9ad3f2c2)\n[//]: # (START_SECTION a39ef41fa5eb058903ca03f8b923570aac7ff15a)\n### Added changes for WebRTC fixes: #666\n\n> Commit: [a39ef41fa5eb058903ca03f8b923570aac7ff15a](https://github.com/dOpensource/dsiprouter/commit/a39ef41fa5eb058903ca03f8b923570aac7ff15a)  \n> Date: Sun, 29 Jun 2025 01:16:03 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a39ef41fa5eb058903ca03f8b923570aac7ff15a)\n[//]: # (START_SECTION 3dd1156097fefd98be296dc312bbc03eb3c58002)\n### Added a commit to allow WebRTC clients using PassThru auth to automatically translate secure SDP into non-secure SDP if the Media setting on the endpoint is proxy\n\n> Commit: [3dd1156097fefd98be296dc312bbc03eb3c58002](https://github.com/dOpensource/dsiprouter/commit/3dd1156097fefd98be296dc312bbc03eb3c58002)  \n> Date: Sat, 28 Jun 2025 00:38:07 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3dd1156097fefd98be296dc312bbc03eb3c58002)\n[//]: # (START_SECTION fd1696c6f460afdac86e5e56414b975ecc3c37ec)\n### Added support for OnHold\n\n> Commit: [fd1696c6f460afdac86e5e56414b975ecc3c37ec](https://github.com/dOpensource/dsiprouter/commit/fd1696c6f460afdac86e5e56414b975ecc3c37ec)  \n> Date: Fri, 23 May 2025 04:58:38 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fd1696c6f460afdac86e5e56414b975ecc3c37ec)\n[//]: # (START_SECTION 0717ce38b28066de5749decbc3aa65bf65051956)\n### Added logic to handle BYE's and ACK's for WebRTC\n\n> Commit: [0717ce38b28066de5749decbc3aa65bf65051956](https://github.com/dOpensource/dsiprouter/commit/0717ce38b28066de5749decbc3aa65bf65051956)  \n> Date: Thu, 19 Jun 2025 03:10:46 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0717ce38b28066de5749decbc3aa65bf65051956)\n[//]: # (START_SECTION b580cc323974d7aba05932392940050f14350858)\n### Fixed ACK issue\n\n> Commit: [b580cc323974d7aba05932392940050f14350858](https://github.com/dOpensource/dsiprouter/commit/b580cc323974d7aba05932392940050f14350858)  \n> Date: Fri, 13 Jun 2025 22:36:03 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b580cc323974d7aba05932392940050f14350858)\n[//]: # (START_SECTION 5c5eb60127bc8b176e933c39f30a60a56d08a312)\n### Added logic in the LOCATION route to make it transport and signaling aware so that calls are properly routed and translated via the RTPENGINE\n\n> Commit: [5c5eb60127bc8b176e933c39f30a60a56d08a312](https://github.com/dOpensource/dsiprouter/commit/5c5eb60127bc8b176e933c39f30a60a56d08a312)  \n> Date: Wed, 11 Jun 2025 21:24:35 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5c5eb60127bc8b176e933c39f30a60a56d08a312)\n[//]: # (START_SECTION 49e9c8c9f4ab16572259a178182437a5b8ef44d8)\n### Added support for OnHold\n\n> Commit: [49e9c8c9f4ab16572259a178182437a5b8ef44d8](https://github.com/dOpensource/dsiprouter/commit/49e9c8c9f4ab16572259a178182437a5b8ef44d8)  \n> Date: Fri, 23 May 2025 04:58:38 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 49e9c8c9f4ab16572259a178182437a5b8ef44d8)\n[//]: # (START_SECTION b46787c059a00d4a8b17a957df3f696f3dad682d)\n### Added a flag that would trigger RTPENGINEANSER\n\n> Commit: [b46787c059a00d4a8b17a957df3f696f3dad682d](https://github.com/dOpensource/dsiprouter/commit/b46787c059a00d4a8b17a957df3f696f3dad682d)  \n> Date: Fri, 16 May 2025 22:02:40 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b46787c059a00d4a8b17a957df3f696f3dad682d)\n[//]: # (START_SECTION e606c4765084386cf03740c99cb8916bcc78f7f3)\n### Added a commit to allow WebRTC clients using PassThru auth to automatically translate secure SDP into non-secure SDP if the Media setting on the endpoint is proxy\n\n> Commit: [e606c4765084386cf03740c99cb8916bcc78f7f3](https://github.com/dOpensource/dsiprouter/commit/e606c4765084386cf03740c99cb8916bcc78f7f3)  \n> Date: Sat, 28 Jun 2025 00:38:07 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e606c4765084386cf03740c99cb8916bcc78f7f3)\n[//]: # (START_SECTION 17a10889ddc23eee8f9392319fee27e6401bcb70)\n### Added support for OnHold\n\n> Commit: [17a10889ddc23eee8f9392319fee27e6401bcb70](https://github.com/dOpensource/dsiprouter/commit/17a10889ddc23eee8f9392319fee27e6401bcb70)  \n> Date: Fri, 23 May 2025 04:58:38 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 17a10889ddc23eee8f9392319fee27e6401bcb70)\n[//]: # (START_SECTION 08ea093039491b124b09bd4b21f63fd629019afd)\n### Revert \"Location Lookup Improvements\"\n\n> Commit: [08ea093039491b124b09bd4b21f63fd629019afd](https://github.com/dOpensource/dsiprouter/commit/08ea093039491b124b09bd4b21f63fd629019afd)  \n> Date: Fri, 27 Jun 2025 01:14:36 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- This reverts commit a85ae0c02f89bcf0539c56201a58e5d01585f062.\n\n\n---\n\n[//]: # (END_SECTION 08ea093039491b124b09bd4b21f63fd629019afd)\n[//]: # (START_SECTION 9e2e812346a002b11e66ef6bd87ce72e130b198d)\n### Revert \"Revert Carrier Redial Location Lookup\"\n\n> Commit: [9e2e812346a002b11e66ef6bd87ce72e130b198d](https://github.com/dOpensource/dsiprouter/commit/9e2e812346a002b11e66ef6bd87ce72e130b198d)  \n> Date: Fri, 27 Jun 2025 01:14:33 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- This reverts commit f298cc3ba4118807586579090c473df2035189e5.\n\n\n---\n\n[//]: # (END_SECTION 9e2e812346a002b11e66ef6bd87ce72e130b198d)\n[//]: # (START_SECTION f298cc3ba4118807586579090c473df2035189e5)\n### Revert Carrier Redial Location Lookup\n\n> Commit: [f298cc3ba4118807586579090c473df2035189e5](https://github.com/dOpensource/dsiprouter/commit/f298cc3ba4118807586579090c473df2035189e5)  \n> Date: Thu, 26 Jun 2025 13:14:15 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- too close to release, reverting this extra feature\n\n\n---\n\n[//]: # (END_SECTION f298cc3ba4118807586579090c473df2035189e5)\n[//]: # (START_SECTION a85ae0c02f89bcf0539c56201a58e5d01585f062)\n### Location Lookup Improvements\n\n> Commit: [a85ae0c02f89bcf0539c56201a58e5d01585f062](https://github.com/dOpensource/dsiprouter/commit/a85ae0c02f89bcf0539c56201a58e5d01585f062)  \n> Date: Thu, 26 Jun 2025 13:03:51 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #669\n- allow phones on other side of carrier to redial a registered device\n- remove local exten length check and continue routing if no device found\n\n\n---\n\n[//]: # (END_SECTION a85ae0c02f89bcf0539c56201a58e5d01585f062)\n[//]: # (START_SECTION fbd0f7c84e4cd66e7f0d694a965abc259f333683)\n### - Added logic to properly enable the RTPEngine on 200OK's - fixed #661 - Added logic to correctly process Extension to Extension calls that use Domain Routing\n\n> Commit: [fbd0f7c84e4cd66e7f0d694a965abc259f333683](https://github.com/dOpensource/dsiprouter/commit/fbd0f7c84e4cd66e7f0d694a965abc259f333683)  \n> Date: Thu, 26 Jun 2025 18:11:06 +0000  \n> Author: root (root@ip-172-31-53-209)  \n> Committer: root (root@ip-172-31-53-209)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fbd0f7c84e4cd66e7f0d694a965abc259f333683)\n[//]: # (START_SECTION a638bc71661c89be5881a9d776b9f73a6ad0af97)\n### Updated device provisioning documentation\n\n> Commit: [a638bc71661c89be5881a9d776b9f73a6ad0af97](https://github.com/dOpensource/dsiprouter/commit/a638bc71661c89be5881a9d776b9f73a6ad0af97)  \n> Date: Sun, 22 Jun 2025 07:59:26 -0400  \n> Author: Mack Hendricks (mhendricks@sangoma.com)  \n> Committer: Mack Hendricks (mhendricks@sangoma.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a638bc71661c89be5881a9d776b9f73a6ad0af97)\n[//]: # (START_SECTION 952f0655d49ad7ab60c106b4f108945070b7c8eb)\n### Moves nginx changes to the FusionPBX module install script\n\n> Commit: [952f0655d49ad7ab60c106b4f108945070b7c8eb](https://github.com/dOpensource/dsiprouter/commit/952f0655d49ad7ab60c106b4f108945070b7c8eb)  \n> Date: Thu, 19 Jun 2025 21:50:24 +0000  \n> Author: root (root@mack.test.dsiprouter.net)  \n> Committer: root (root@mack.test.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 952f0655d49ad7ab60c106b4f108945070b7c8eb)\n[//]: # (START_SECTION a058f7f1054f5c65c330f0424e732ce6861793bc)\n### Added logic to write out the Nginx configuration for FusionPBX Provisioning within the /etc/nginx/sites-enabled directory\n\n> Commit: [a058f7f1054f5c65c330f0424e732ce6861793bc](https://github.com/dOpensource/dsiprouter/commit/a058f7f1054f5c65c330f0424e732ce6861793bc)  \n> Date: Thu, 19 Jun 2025 21:13:05 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a058f7f1054f5c65c330f0424e732ce6861793bc)\n[//]: # (START_SECTION f3993379b9ba18b9b4a26eba2611c03ac64b568f)\n### Added logic to handle BYE's and ACK's for WebRTC\n\n> Commit: [f3993379b9ba18b9b4a26eba2611c03ac64b568f](https://github.com/dOpensource/dsiprouter/commit/f3993379b9ba18b9b4a26eba2611c03ac64b568f)  \n> Date: Thu, 19 Jun 2025 03:10:46 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f3993379b9ba18b9b4a26eba2611c03ac64b568f)\n[//]: # (START_SECTION febc0d9073bf1834e0fc13dbaf893cb9179a82d6)\n### Fixed ACK issue\n\n> Commit: [febc0d9073bf1834e0fc13dbaf893cb9179a82d6](https://github.com/dOpensource/dsiprouter/commit/febc0d9073bf1834e0fc13dbaf893cb9179a82d6)  \n> Date: Fri, 13 Jun 2025 22:36:03 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION febc0d9073bf1834e0fc13dbaf893cb9179a82d6)\n[//]: # (START_SECTION 01171453f8bfe861f37aceb332ada09057ad4b59)\n### Added logic in the LOCATION route to make it transport and signaling aware so that calls are properly routed and translated via the RTPENGINE\n\n> Commit: [01171453f8bfe861f37aceb332ada09057ad4b59](https://github.com/dOpensource/dsiprouter/commit/01171453f8bfe861f37aceb332ada09057ad4b59)  \n> Date: Wed, 11 Jun 2025 21:24:35 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 01171453f8bfe861f37aceb332ada09057ad4b59)\n[//]: # (START_SECTION d61ec9564deeca1999c61f0683af50bf8b74730d)\n### Cleaned up xlog comments\n\n> Commit: [d61ec9564deeca1999c61f0683af50bf8b74730d](https://github.com/dOpensource/dsiprouter/commit/d61ec9564deeca1999c61f0683af50bf8b74730d)  \n> Date: Tue, 10 Jun 2025 03:57:09 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d61ec9564deeca1999c61f0683af50bf8b74730d)\n[//]: # (START_SECTION cdfda0c7319e70217b1d6f5570ba7d242e181b4b)\n### Fixing broken edit/delete links on domains\n\n> Commit: [cdfda0c7319e70217b1d6f5570ba7d242e181b4b](https://github.com/dOpensource/dsiprouter/commit/cdfda0c7319e70217b1d6f5570ba7d242e181b4b)  \n> Date: Fri, 6 Jun 2025 23:03:26 -0500  \n> Author: Micah Quinn (micah.quinn@sipiq.com)  \n> Committer: Micah Quinn (micah.quinn@sipiq.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cdfda0c7319e70217b1d6f5570ba7d242e181b4b)\n[//]: # (START_SECTION fe6dee1e3bb7f94a87dca23d8f78512fbd7c03b7)\n### Added logic to route NOTIFY messages when the RURI contains it's private ip address.  We use the TO header to lookup the real contact address in the location table\n\n> Commit: [fe6dee1e3bb7f94a87dca23d8f78512fbd7c03b7](https://github.com/dOpensource/dsiprouter/commit/fe6dee1e3bb7f94a87dca23d8f78512fbd7c03b7)  \n> Date: Sun, 1 Jun 2025 01:49:55 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fe6dee1e3bb7f94a87dca23d8f78512fbd7c03b7)\n[//]: # (START_SECTION 43d053b938a14191b97ce44048a8751021fc6255)\n### Added support for OnHold\n\n> Commit: [43d053b938a14191b97ce44048a8751021fc6255](https://github.com/dOpensource/dsiprouter/commit/43d053b938a14191b97ce44048a8751021fc6255)  \n> Date: Fri, 23 May 2025 04:58:38 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 43d053b938a14191b97ce44048a8751021fc6255)\n[//]: # (START_SECTION 68a2dd685172e2b5521f1b698456b2f0dde077b4)\n### Added some notes around setting up Phone Provisioning when dSIPRouter is being used to proxy FusionPBX UI requests\n\n> Commit: [68a2dd685172e2b5521f1b698456b2f0dde077b4](https://github.com/dOpensource/dsiprouter/commit/68a2dd685172e2b5521f1b698456b2f0dde077b4)  \n> Date: Wed, 21 May 2025 00:16:42 -0400  \n> Author: Mack Hendricks (mhendricks@sangoma.com)  \n> Committer: Mack Hendricks (mhendricks@sangoma.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 68a2dd685172e2b5521f1b698456b2f0dde077b4)\n[//]: # (START_SECTION 4f2a9f8ad407110852092ce543461972687588df)\n### Added a flag that would trigger RTPENGINEANSER\n\n> Commit: [4f2a9f8ad407110852092ce543461972687588df](https://github.com/dOpensource/dsiprouter/commit/4f2a9f8ad407110852092ce543461972687588df)  \n> Date: Fri, 16 May 2025 22:02:40 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4f2a9f8ad407110852092ce543461972687588df)\n[//]: # (START_SECTION 656fbc1c50d503d7f14d13674da597ebc4e1f117)\n### Converted back the commit\n\n> Commit: [656fbc1c50d503d7f14d13674da597ebc4e1f117](https://github.com/dOpensource/dsiprouter/commit/656fbc1c50d503d7f14d13674da597ebc4e1f117)  \n> Date: Wed, 14 May 2025 20:45:37 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 656fbc1c50d503d7f14d13674da597ebc4e1f117)\n[//]: # (START_SECTION 186f48f0d295f28527564a3dc3103cab8993f65c)\n### Fixes #660 - Added logic to test if the SIP Dialog was already created.  If so, don't create a new SIP Dialog \" Fixes #554 - Confirmed that it's fixed Fixes #545 - Confirmed that it's fixed Fixes #520 - Confirmed that it's fixed\n\n> Commit: [186f48f0d295f28527564a3dc3103cab8993f65c](https://github.com/dOpensource/dsiprouter/commit/186f48f0d295f28527564a3dc3103cab8993f65c)  \n> Date: Wed, 14 May 2025 20:35:30 +0000  \n> Author: root (root@demo.test.dsiprouter.net)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 186f48f0d295f28527564a3dc3103cab8993f65c)\n[//]: # (START_SECTION 2b76d501e66b0b22acc911e944f24ca09588cbd7)\n### Fixes #660 - Added logic to test if the SIP Dialog was already created.  If so, don't create a new SIP Dialog \" Fixes #554 - Confirmed that it's fixed Fixes #545 - Confirmed that it's fixed Fixes #520 - Confirmed that it's fixed\n\n> Commit: [2b76d501e66b0b22acc911e944f24ca09588cbd7](https://github.com/dOpensource/dsiprouter/commit/2b76d501e66b0b22acc911e944f24ca09588cbd7)  \n> Date: Wed, 14 May 2025 20:35:30 +0000  \n> Author: root (root@demo.test.dsiprouter.net)  \n> Committer: root (root@demo.test.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2b76d501e66b0b22acc911e944f24ca09588cbd7)\n[//]: # (START_SECTION 9b1cab3e12f978ae79c7317b530881a1c6cbe8ae)\n### Fixes: - Fixed the contact so that the transport parameter is changed based on the destination - Fixed the Path header so that the transport parameter is defined based on the destination\n\n> Commit: [9b1cab3e12f978ae79c7317b530881a1c6cbe8ae](https://github.com/dOpensource/dsiprouter/commit/9b1cab3e12f978ae79c7317b530881a1c6cbe8ae)  \n> Date: Tue, 22 Apr 2025 02:06:23 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9b1cab3e12f978ae79c7317b530881a1c6cbe8ae)\n[//]: # (START_SECTION 6fc2548389b0c6672f9141baf4e53e71bc297184)\n### Fixed issue with RE-INVITES from a Carrier to MSTeam causes a 488 from MSTeams with this error message: 'CannotChangeRtcpMultiplexing, InternalErrorPhrase: Cannot change RTCP multiplexing from existing active media'\n\n> Commit: [6fc2548389b0c6672f9141baf4e53e71bc297184](https://github.com/dOpensource/dsiprouter/commit/6fc2548389b0c6672f9141baf4e53e71bc297184)  \n> Date: Fri, 18 Apr 2025 00:29:55 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6fc2548389b0c6672f9141baf4e53e71bc297184)\n[//]: # (START_SECTION 3f73914f1225b2836cab1f13e366582a61c183b4)\n### Fixed REMOVE_REFER\n\n> Commit: [3f73914f1225b2836cab1f13e366582a61c183b4](https://github.com/dOpensource/dsiprouter/commit/3f73914f1225b2836cab1f13e366582a61c183b4)  \n> Date: Mon, 7 Apr 2025 13:15:07 -0400  \n> Author: Mack Hendricks (mhendricks@sangoma.com)  \n> Committer: Mack Hendricks (mhendricks@sangoma.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3f73914f1225b2836cab1f13e366582a61c183b4)\n[//]: # (START_SECTION 6265abc736c8c08348a951da48b643bd67034b0e)\n### Fixed Internal Transfers\n\n> Commit: [6265abc736c8c08348a951da48b643bd67034b0e](https://github.com/dOpensource/dsiprouter/commit/6265abc736c8c08348a951da48b643bd67034b0e)  \n> Date: Mon, 7 Apr 2025 11:25:21 -0400  \n> Author: Mack Hendricks (mhendricks@sangoma.com)  \n> Committer: Mack Hendricks (mhendricks@sangoma.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6265abc736c8c08348a951da48b643bd67034b0e)\n[//]: # (START_SECTION b57a2a3670df1bf0005a4992f2af073e4d85738a)\n### Fixed issue with MSTeams calls not hanging up properly\n\n> Commit: [b57a2a3670df1bf0005a4992f2af073e4d85738a](https://github.com/dOpensource/dsiprouter/commit/b57a2a3670df1bf0005a4992f2af073e4d85738a)  \n> Date: Sat, 5 Apr 2025 16:50:25 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b57a2a3670df1bf0005a4992f2af073e4d85738a)\n[//]: # (START_SECTION a977b58eee52a00f10ea32f7e9a05f2222037a3d)\n### Trigger Jenkins build\n\n> Commit: [a977b58eee52a00f10ea32f7e9a05f2222037a3d](https://github.com/dOpensource/dsiprouter/commit/a977b58eee52a00f10ea32f7e9a05f2222037a3d)  \n> Date: Fri, 28 Mar 2025 14:36:48 -0400  \n> Author: chelseatcarter (chelseatcarter@gmail.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a977b58eee52a00f10ea32f7e9a05f2222037a3d)\n[//]: # (START_SECTION f47832ae5061628cef09e3198b7a33525f2cd0d9)\n### Trigger Jenkins build\n\n> Commit: [f47832ae5061628cef09e3198b7a33525f2cd0d9](https://github.com/dOpensource/dsiprouter/commit/f47832ae5061628cef09e3198b7a33525f2cd0d9)  \n> Date: Fri, 28 Mar 2025 14:25:21 -0400  \n> Author: chelseatcarter (chelseatcarter@gmail.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f47832ae5061628cef09e3198b7a33525f2cd0d9)\n[//]: # (START_SECTION f2a522d40e62c96bd1e55de7e098cfda9c98fbfe)\n### Fixed Issues - REGISTER will now follow the signal settings in the endpoint group for domain routing.  Fixes #656 - Fixed an issue with the REPLACE_CONTACT_DOMAIN route, the semi-colon was missing in the protocal portion of the contact\n\n> Commit: [f2a522d40e62c96bd1e55de7e098cfda9c98fbfe](https://github.com/dOpensource/dsiprouter/commit/f2a522d40e62c96bd1e55de7e098cfda9c98fbfe)  \n> Date: Fri, 28 Mar 2025 16:03:43 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f2a522d40e62c96bd1e55de7e098cfda9c98fbfe)\n[//]: # (START_SECTION 92176ca0a908d5a778cb9a1081d7bd3a80df719c)\n### WIP on SUBSCRIBE Interdomain Messaging\n\n> Commit: [92176ca0a908d5a778cb9a1081d7bd3a80df719c](https://github.com/dOpensource/dsiprouter/commit/92176ca0a908d5a778cb9a1081d7bd3a80df719c)  \n> Date: Wed, 12 Mar 2025 15:13:32 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- fix POSIX REGEX check on Contact\n- remove non-useful domain code on PUBLISH\n\n\n---\n\n[//]: # (END_SECTION 92176ca0a908d5a778cb9a1081d7bd3a80df719c)\n[//]: # (START_SECTION 50380c10d0fbeb07295d13cde20aa6ac0a221def)\n### Fixes for Domain Routing Use Cases\n\n> Commit: [50380c10d0fbeb07295d13cde20aa6ac0a221def](https://github.com/dOpensource/dsiprouter/commit/50380c10d0fbeb07295d13cde20aa6ac0a221def)  \n> Date: Tue, 11 Mar 2025 14:25:41 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- resolves #622\n- fix Contact on REGISTER, SUBSCRIBE, and PUBLISH\n- use dispatcher routes for presence when a domain is matched\n\n\n---\n\n[//]: # (END_SECTION 50380c10d0fbeb07295d13cde20aa6ac0a221def)\n[//]: # (START_SECTION e6cbcdbf74426a74a890a97cd5a1ef93813eda99)\n### Fixed issues for MSTeams: - Fixed internal transfers - Fixed issue with hangups when initiated from the carrier side\n\n> Commit: [e6cbcdbf74426a74a890a97cd5a1ef93813eda99](https://github.com/dOpensource/dsiprouter/commit/e6cbcdbf74426a74a890a97cd5a1ef93813eda99)  \n> Date: Thu, 3 Apr 2025 09:10:31 -0400  \n> Author: Mack Hendricks (mhendricks@sangoma.com)  \n> Committer: Mack Hendricks (mhendricks@sangoma.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e6cbcdbf74426a74a890a97cd5a1ef93813eda99)\n[//]: # (START_SECTION 14712ee1e39628bc178d8591e22d8fc6b355a21d)\n### Trigger Jenkins build\n\n> Commit: [14712ee1e39628bc178d8591e22d8fc6b355a21d](https://github.com/dOpensource/dsiprouter/commit/14712ee1e39628bc178d8591e22d8fc6b355a21d)  \n> Date: Fri, 28 Mar 2025 14:36:48 -0400  \n> Author: chelseatcarter (chelseatcarter@gmail.com)  \n> Committer: chelseatcarter (chelseatcarter@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 14712ee1e39628bc178d8591e22d8fc6b355a21d)\n[//]: # (START_SECTION 7a264c878a5440330ee03bccc501ea83c4fa33bb)\n### Trigger Jenkins build\n\n> Commit: [7a264c878a5440330ee03bccc501ea83c4fa33bb](https://github.com/dOpensource/dsiprouter/commit/7a264c878a5440330ee03bccc501ea83c4fa33bb)  \n> Date: Fri, 28 Mar 2025 14:25:21 -0400  \n> Author: chelseatcarter (chelseatcarter@gmail.com)  \n> Committer: chelseatcarter (chelseatcarter@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7a264c878a5440330ee03bccc501ea83c4fa33bb)\n[//]: # (START_SECTION 2ab2a009610830601e70476cea99ec7d867ae181)\n### Fixed Issues - REGISTER will now follow the signal settings in the endpoint group for domain routing.  Fixes #656 - Fixed an issue with the REPLACE_CONTACT_DOMAIN route, the semi-colon was missing in the protocal portion of the contact\n\n> Commit: [2ab2a009610830601e70476cea99ec7d867ae181](https://github.com/dOpensource/dsiprouter/commit/2ab2a009610830601e70476cea99ec7d867ae181)  \n> Date: Fri, 28 Mar 2025 16:03:43 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2ab2a009610830601e70476cea99ec7d867ae181)\n[//]: # (START_SECTION 36eb14bf4f109b1db9e11497f036f7768669a8bd)\n### WIP on SUBSCRIBE Interdomain Messaging\n\n> Commit: [36eb14bf4f109b1db9e11497f036f7768669a8bd](https://github.com/dOpensource/dsiprouter/commit/36eb14bf4f109b1db9e11497f036f7768669a8bd)  \n> Date: Wed, 12 Mar 2025 15:13:32 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- fix POSIX REGEX check on Contact\n- remove non-useful domain code on PUBLISH\n\n\n---\n\n[//]: # (END_SECTION 36eb14bf4f109b1db9e11497f036f7768669a8bd)\n[//]: # (START_SECTION bda6aadd069dbdf2027b782e81a83af66e7e890a)\n### Fixes for Domain Routing Use Cases\n\n> Commit: [bda6aadd069dbdf2027b782e81a83af66e7e890a](https://github.com/dOpensource/dsiprouter/commit/bda6aadd069dbdf2027b782e81a83af66e7e890a)  \n> Date: Tue, 11 Mar 2025 14:25:41 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- resolves #622\n- fix Contact on REGISTER, SUBSCRIBE, and PUBLISH\n- use dispatcher routes for presence when a domain is matched\n\n\n---\n\n[//]: # (END_SECTION bda6aadd069dbdf2027b782e81a83af66e7e890a)\n[//]: # (START_SECTION 5d95510edf523ea0be6d417ff69360f77eb610e6)\n### Fix DNSmasq Install for Debian\n\n> Commit: [5d95510edf523ea0be6d417ff69360f77eb610e6](https://github.com/dOpensource/dsiprouter/commit/5d95510edf523ea0be6d417ff69360f77eb610e6)  \n> Date: Wed, 5 Mar 2025 15:59:35 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 5d95510edf523ea0be6d417ff69360f77eb610e6)\n[//]: # (START_SECTION 223522fe8e8011377b663409529bf04eb74cf1da)\n### Fixes #646: Set a default value of 1 for rweight when an endpoint is created\n\n> Commit: [223522fe8e8011377b663409529bf04eb74cf1da](https://github.com/dOpensource/dsiprouter/commit/223522fe8e8011377b663409529bf04eb74cf1da)  \n> Date: Mon, 17 Mar 2025 23:15:18 +0000  \n> Author: root (root@sbc1.customers.dsiprouter.net)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 223522fe8e8011377b663409529bf04eb74cf1da)\n[//]: # (START_SECTION 96e17cb5f60b07b90c0f62639b5239e7e1e08873)\n### Added DLGURI back into the config\n\n> Commit: [96e17cb5f60b07b90c0f62639b5239e7e1e08873](https://github.com/dOpensource/dsiprouter/commit/96e17cb5f60b07b90c0f62639b5239e7e1e08873)  \n> Date: Mon, 3 Mar 2025 23:17:28 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 96e17cb5f60b07b90c0f62639b5239e7e1e08873)\n[//]: # (START_SECTION c9131b49277ad01931aa7fd19e56bf85a4b37e79)\n### Revert \"Removed duplicate route that was added in during merge from QA\"\n\n> Commit: [c9131b49277ad01931aa7fd19e56bf85a4b37e79](https://github.com/dOpensource/dsiprouter/commit/c9131b49277ad01931aa7fd19e56bf85a4b37e79)  \n> Date: Mon, 3 Mar 2025 22:45:27 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- This reverts commit 4f23a2e1158d415856c027cc516069d4be7b0b2f.\n\n\n---\n\n[//]: # (END_SECTION c9131b49277ad01931aa7fd19e56bf85a4b37e79)\n[//]: # (START_SECTION a106f645bebbd0f39f073f2dad55bf8ad5733463)\n### Revert \"Merge branch 'master' into qa\"\n\n> Commit: [a106f645bebbd0f39f073f2dad55bf8ad5733463](https://github.com/dOpensource/dsiprouter/commit/a106f645bebbd0f39f073f2dad55bf8ad5733463)  \n> Date: Mon, 3 Mar 2025 22:38:45 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- This reverts commit ffbc368d783e83ae47809ef7cadad68e7496640f, reversing\n- changes made to e8f8f8c07902241cd86428ab860dde834ba2977a.\n\n\n---\n\n[//]: # (END_SECTION a106f645bebbd0f39f073f2dad55bf8ad5733463)\n[//]: # (START_SECTION 4f23a2e1158d415856c027cc516069d4be7b0b2f)\n### Removed duplicate route that was added in during merge from QA\n\n> Commit: [4f23a2e1158d415856c027cc516069d4be7b0b2f](https://github.com/dOpensource/dsiprouter/commit/4f23a2e1158d415856c027cc516069d4be7b0b2f)  \n> Date: Sun, 2 Mar 2025 04:06:15 +0000  \n> Author: root (root@demo.dsiprouter.net)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4f23a2e1158d415856c027cc516069d4be7b0b2f)\n[//]: # (START_SECTION 4859f366f289a1e8aa03d19fcb902cedba2129d7)\n### Removed duplicate route that was added in during merge from QA\n\n> Commit: [4859f366f289a1e8aa03d19fcb902cedba2129d7](https://github.com/dOpensource/dsiprouter/commit/4859f366f289a1e8aa03d19fcb902cedba2129d7)  \n> Date: Sun, 2 Mar 2025 04:06:15 +0000  \n> Author: root (root@demo.dsiprouter.net)  \n> Committer: root (root@demo.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4859f366f289a1e8aa03d19fcb902cedba2129d7)\n[//]: # (START_SECTION e8f8f8c07902241cd86428ab860dde834ba2977a)\n### Resolves #633\n\n> Commit: [e8f8f8c07902241cd86428ab860dde834ba2977a](https://github.com/dOpensource/dsiprouter/commit/e8f8f8c07902241cd86428ab860dde834ba2977a)  \n> Date: Fri, 28 Feb 2025 20:14:40 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION e8f8f8c07902241cd86428ab860dde834ba2977a)\n[//]: # (START_SECTION 8c3d1ec2aec7952602f6c19af5d94648d272257a)\n### PIDF-LO Support: - Fixed the Kamailio configuration so that SIP packets with PIDF-LO messages are properly re-written\n\n> Commit: [8c3d1ec2aec7952602f6c19af5d94648d272257a](https://github.com/dOpensource/dsiprouter/commit/8c3d1ec2aec7952602f6c19af5d94648d272257a)  \n> Date: Fri, 28 Feb 2025 14:12:28 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8c3d1ec2aec7952602f6c19af5d94648d272257a)\n[//]: # (START_SECTION ce9efc694149c243243e366d9303f12a6ff21090)\n### Improved CDR Handling\n\n> Commit: [ce9efc694149c243243e366d9303f12a6ff21090](https://github.com/dOpensource/dsiprouter/commit/ce9efc694149c243243e366d9303f12a6ff21090)  \n> Date: Thu, 27 Feb 2025 16:37:35 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add selector for billable calls\n- move pagination to serverside\n- move searching to serverside\n- improve performance of queries\n- update changelog\n\n\n---\n\n[//]: # (END_SECTION ce9efc694149c243243e366d9303f12a6ff21090)\n[//]: # (START_SECTION a6690af4543a9458baf971404c7d21d03ec19bb3)\n### Upgrade from 0.76 to 0.77 (#631)\n\n> Commit: [a6690af4543a9458baf971404c7d21d03ec19bb3](https://github.com/dOpensource/dsiprouter/commit/a6690af4543a9458baf971404c7d21d03ec19bb3)  \n> Date: Wed, 26 Feb 2025 17:08:49 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a6690af4543a9458baf971404c7d21d03ec19bb3)\n[//]: # (START_SECTION a89c77066094932562cb1aee1ade444c1335b02b)\n### Fixes: - Removed the Microsoft Teams Subscriptions Link from Dashboard - Fixed the Teleblock Screen so that the Enable/Disable button works properly\n\n> Commit: [a89c77066094932562cb1aee1ade444c1335b02b](https://github.com/dOpensource/dsiprouter/commit/a89c77066094932562cb1aee1ade444c1335b02b)  \n> Date: Wed, 26 Feb 2025 11:48:01 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a89c77066094932562cb1aee1ade444c1335b02b)\n[//]: # (START_SECTION 16e7d16b9aaf5a230f1b4faa2ad032127befbe6e)\n### Fixes: - Fixed BYE on non-MSTeams Calls - Fixed BYE on MSTeams to Carrier calls with the Carrier hanging up\n\n> Commit: [16e7d16b9aaf5a230f1b4faa2ad032127befbe6e](https://github.com/dOpensource/dsiprouter/commit/16e7d16b9aaf5a230f1b4faa2ad032127befbe6e)  \n> Date: Wed, 26 Feb 2025 11:44:39 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 16e7d16b9aaf5a230f1b4faa2ad032127befbe6e)\n[//]: # (START_SECTION db26338c391f2b39463967f03ec2f123cc4c1f56)\n### Upgrade from 0.76 to 0.77\n\n> Commit: [db26338c391f2b39463967f03ec2f123cc4c1f56](https://github.com/dOpensource/dsiprouter/commit/db26338c391f2b39463967f03ec2f123cc4c1f56)  \n> Date: Mon, 24 Feb 2025 10:54:49 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION db26338c391f2b39463967f03ec2f123cc4c1f56)\n[//]: # (START_SECTION ce3312c9020a8c1e208143ba51ae6ab19fb75fec)\n### Fixed issue with the media type being copied to the other leg of the call when using MSTeams\n\n> Commit: [ce3312c9020a8c1e208143ba51ae6ab19fb75fec](https://github.com/dOpensource/dsiprouter/commit/ce3312c9020a8c1e208143ba51ae6ab19fb75fec)  \n> Date: Mon, 24 Feb 2025 23:02:53 +0000  \n> Author: root (root@sbc1.customers.dsiprouter.net)  \n> Committer: root (root@sbc1.customers.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ce3312c9020a8c1e208143ba51ae6ab19fb75fec)\n[//]: # (START_SECTION c7d98a3c26f298b4416fe27bb9ed6145a3eed3c3)\n### Revert 02207a7\n\n> Commit: [c7d98a3c26f298b4416fe27bb9ed6145a3eed3c3](https://github.com/dOpensource/dsiprouter/commit/c7d98a3c26f298b4416fe27bb9ed6145a3eed3c3)  \n> Date: Mon, 24 Feb 2025 07:39:33 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- this was resolved in 2302d89\n\n\n---\n\n[//]: # (END_SECTION c7d98a3c26f298b4416fe27bb9ed6145a3eed3c3)\n[//]: # (START_SECTION 77b0d03bf0c0ca0dcc9be4730aff129aaf2a0fca)\n### Fixes: - Fixed the BYE for initial request from MSTeams to Carrier and from Carrier to MSTeams - Removed 2 Flowroute gateways from the DRouting Gateway List\n\n> Commit: [77b0d03bf0c0ca0dcc9be4730aff129aaf2a0fca](https://github.com/dOpensource/dsiprouter/commit/77b0d03bf0c0ca0dcc9be4730aff129aaf2a0fca)  \n> Date: Mon, 24 Feb 2025 04:45:10 +0000  \n> Author: root (root@sbc1.customers.dsiprouter.net)  \n> Committer: root (root@sbc1.customers.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 77b0d03bf0c0ca0dcc9be4730aff129aaf2a0fca)\n[//]: # (START_SECTION dd4564e01ccc4d5bd3ef101a1d1abf368771ce1e)\n### Updated Flowroute POP's and configured the address table to allow inbound traffic from all Flowroute PoP's\n\n> Commit: [dd4564e01ccc4d5bd3ef101a1d1abf368771ce1e](https://github.com/dOpensource/dsiprouter/commit/dd4564e01ccc4d5bd3ef101a1d1abf368771ce1e)  \n> Date: Sun, 23 Feb 2025 22:54:01 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dd4564e01ccc4d5bd3ef101a1d1abf368771ce1e)\n[//]: # (START_SECTION 02207a75314811582d3895d35c2eabab968422e7)\n### Added a fix to allow DigitalOcean Dropets to ignore the Internal IP address because the DO Networking doesn't NAT the address, which makes it not routable from the Internet\n\n> Commit: [02207a75314811582d3895d35c2eabab968422e7](https://github.com/dOpensource/dsiprouter/commit/02207a75314811582d3895d35c2eabab968422e7)  \n> Date: Sun, 23 Feb 2025 22:40:23 +0000  \n> Author: root (root@sbc1.customers.dsiprouter.net)  \n> Committer: root (root@sbc1.customers.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 02207a75314811582d3895d35c2eabab968422e7)\n[//]: # (START_SECTION 2302d89fd3752bbc6231f9e8131105a04a9f14ac)\n### Revert dsip_lib.sh Internal Network Resolution\n\n> Commit: [2302d89fd3752bbc6231f9e8131105a04a9f14ac](https://github.com/dOpensource/dsiprouter/commit/2302d89fd3752bbc6231f9e8131105a04a9f14ac)  \n> Date: Fri, 21 Feb 2025 15:29:33 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 2302d89fd3752bbc6231f9e8131105a04a9f14ac)\n[//]: # (START_SECTION f429ed2052552e1d8ea2c8182ce4825b1432a8e0)\n### Revert \"Feature/sbcsignalhandling\"\n\n> Commit: [f429ed2052552e1d8ea2c8182ce4825b1432a8e0](https://github.com/dOpensource/dsiprouter/commit/f429ed2052552e1d8ea2c8182ce4825b1432a8e0)  \n> Date: Fri, 21 Feb 2025 16:16:54 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f429ed2052552e1d8ea2c8182ce4825b1432a8e0)\n[//]: # (START_SECTION 40eac0dbad1c95998ca0b5346187b47abceb1f95)\n### Fixed issue(s) with MSTeams, which include: - Adding logic to improve REINVITES for MSTeam - Improved signal handling\n\n> Commit: [40eac0dbad1c95998ca0b5346187b47abceb1f95](https://github.com/dOpensource/dsiprouter/commit/40eac0dbad1c95998ca0b5346187b47abceb1f95)  \n> Date: Fri, 21 Feb 2025 21:12:04 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 40eac0dbad1c95998ca0b5346187b47abceb1f95)\n[//]: # (START_SECTION 5a5f7558d67477c8b69dd1e23ac4f2ebc65ea2a3)\n### Update DIDs When Exist On Import\n\n> Commit: [5a5f7558d67477c8b69dd1e23ac4f2ebc65ea2a3](https://github.com/dOpensource/dsiprouter/commit/5a5f7558d67477c8b69dd1e23ac4f2ebc65ea2a3)  \n> Date: Fri, 21 Feb 2025 11:10:17 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 5a5f7558d67477c8b69dd1e23ac4f2ebc65ea2a3)\n[//]: # (START_SECTION 84d1f13d647cc2d7eeab523587435e9fb36b1688)\n### Added logic to fix BYES\n\n> Commit: [84d1f13d647cc2d7eeab523587435e9fb36b1688](https://github.com/dOpensource/dsiprouter/commit/84d1f13d647cc2d7eeab523587435e9fb36b1688)  \n> Date: Fri, 21 Feb 2025 10:24:28 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 84d1f13d647cc2d7eeab523587435e9fb36b1688)\n[//]: # (START_SECTION 1c0b86cc905d6c6a1aa4a11b89eb9361fdd97907)\n### Added port-latch parameter to RTPEngine flags\n\n> Commit: [1c0b86cc905d6c6a1aa4a11b89eb9361fdd97907](https://github.com/dOpensource/dsiprouter/commit/1c0b86cc905d6c6a1aa4a11b89eb9361fdd97907)  \n> Date: Thu, 20 Feb 2025 22:17:10 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1c0b86cc905d6c6a1aa4a11b89eb9361fdd97907)\n[//]: # (START_SECTION 2fab7686d38b88730594a8cd57d25eaf88076cbb)\n### Fix Typo WITH_CDRS\n\n> Commit: [2fab7686d38b88730594a8cd57d25eaf88076cbb](https://github.com/dOpensource/dsiprouter/commit/2fab7686d38b88730594a8cd57d25eaf88076cbb)  \n> Date: Wed, 12 Feb 2025 12:17:14 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2fab7686d38b88730594a8cd57d25eaf88076cbb)\n[//]: # (START_SECTION e1c2bd0628d34c0e32c48d7645810d29d9296718)\n### Latest Progress on Translations\n\n> Commit: [e1c2bd0628d34c0e32c48d7645810d29d9296718](https://github.com/dOpensource/dsiprouter/commit/e1c2bd0628d34c0e32c48d7645810d29d9296718)  \n> Date: Fri, 31 Jan 2025 12:51:08 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e1c2bd0628d34c0e32c48d7645810d29d9296718)\n[//]: # (START_SECTION db746a1ef367383edbc0896f52f31ef82f3e0439)\n### Fix Missing ftag\n\n> Commit: [db746a1ef367383edbc0896f52f31ef82f3e0439](https://github.com/dOpensource/dsiprouter/commit/db746a1ef367383edbc0896f52f31ef82f3e0439)  \n> Date: Fri, 27 Sep 2024 10:15:47 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- fix missing tag in From after SBC translations\n\n\n---\n\n[//]: # (END_SECTION db746a1ef367383edbc0896f52f31ef82f3e0439)\n[//]: # (START_SECTION ad5f92b9896911a020caa3971dd9b0a01ea56164)\n### Better Call Teardown\n\n> Commit: [ad5f92b9896911a020caa3971dd9b0a01ea56164](https://github.com/dOpensource/dsiprouter/commit/ad5f92b9896911a020caa3971dd9b0a01ea56164)  \n> Date: Thu, 19 Sep 2024 17:36:29 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- update cfg to ensure rtpengine sessions are always torn down immediately\n- fix dlg_ctx parameters not being applied to dialog\n\n\n---\n\n[//]: # (END_SECTION ad5f92b9896911a020caa3971dd9b0a01ea56164)\n[//]: # (START_SECTION 9c19f242e8a7ad0196f88a2078e0be061ca16cbf)\n### Fix Endpoint Group Call Limit\n\n> Commit: [9c19f242e8a7ad0196f88a2078e0be061ca16cbf](https://github.com/dOpensource/dsiprouter/commit/9c19f242e8a7ad0196f88a2078e0be061ca16cbf)  \n> Date: Wed, 18 Sep 2024 16:22:18 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- fix in dialog requests forgetting gwid/gwgroupid\n\n\n---\n\n[//]: # (END_SECTION 9c19f242e8a7ad0196f88a2078e0be061ca16cbf)\n[//]: # (START_SECTION 4ce5a4a0d4125defd1fe11046d72efe8e10b42f0)\n### Handle Null Request Username\n\n> Commit: [4ce5a4a0d4125defd1fe11046d72efe8e10b42f0](https://github.com/dOpensource/dsiprouter/commit/4ce5a4a0d4125defd1fe11046d72efe8e10b42f0)  \n> Date: Wed, 11 Sep 2024 10:45:09 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- default null req username to To username to support non-compliant UACs\n\n\n---\n\n[//]: # (END_SECTION 4ce5a4a0d4125defd1fe11046d72efe8e10b42f0)\n[//]: # (START_SECTION db53233a086170ff69b829ab1a203ce78d404a36)\n### Added MSTeams Fixes and SBC Signaling Handling - Cherry Picked 52e5931 - Added Logic to Fix MSTeams\n\n> Commit: [db53233a086170ff69b829ab1a203ce78d404a36](https://github.com/dOpensource/dsiprouter/commit/db53233a086170ff69b829ab1a203ce78d404a36)  \n> Date: Sun, 16 Feb 2025 22:01:33 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION db53233a086170ff69b829ab1a203ce78d404a36)\n[//]: # (START_SECTION 582aa4c755a129dbaf30fa951232bcfc6b174c48)\n### Improved SBC Signal Handling\n\n> Commit: [582aa4c755a129dbaf30fa951232bcfc6b174c48](https://github.com/dOpensource/dsiprouter/commit/582aa4c755a129dbaf30fa951232bcfc6b174c48)  \n> Date: Wed, 28 Aug 2024 09:45:33 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- handle the translation between A and B leg signalling within every dialog\n- update in dialog requests back to UA to use the Contact they initiated call with\n- fix dialog tracking not matching properly\n- fix To number not reformatted when drouting strips from the request URI\n\n\n---\n\n[//]: # (END_SECTION 582aa4c755a129dbaf30fa951232bcfc6b174c48)\n[//]: # (START_SECTION 029833e37ba7c808c64a2062836edfffb0bf87af)\n### Fixed typo\n\n> Commit: [029833e37ba7c808c64a2062836edfffb0bf87af](https://github.com/dOpensource/dsiprouter/commit/029833e37ba7c808c64a2062836edfffb0bf87af)  \n> Date: Wed, 12 Feb 2025 20:53:34 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 029833e37ba7c808c64a2062836edfffb0bf87af)\n[//]: # (START_SECTION 96ae8e0ad6b59ffcfa5eb5e22071ef01cda3b386)\n### Fixed Carrier to MSTeams call flow with audio working properly.  Hanging the call up from the carrier sends a proper BYE to MSTeams\n\n> Commit: [96ae8e0ad6b59ffcfa5eb5e22071ef01cda3b386](https://github.com/dOpensource/dsiprouter/commit/96ae8e0ad6b59ffcfa5eb5e22071ef01cda3b386)  \n> Date: Wed, 12 Feb 2025 05:14:37 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 96ae8e0ad6b59ffcfa5eb5e22071ef01cda3b386)\n[//]: # (START_SECTION 4c603fe4465e0777bc9ab83f138b24bdc790857c)\n### Fix Backup and Restore Feature\n\n> Commit: [4c603fe4465e0777bc9ab83f138b24bdc790857c](https://github.com/dOpensource/dsiprouter/commit/4c603fe4465e0777bc9ab83f138b24bdc790857c)  \n> Date: Mon, 3 Feb 2025 06:10:13 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- use root DB creds for backup/restore\n- add backup/restore subcmmands to CLI\n- add new subcommands to sudoers\n- add CLI completion for new subcommands\n- add docs for new subcommands\n- allow backup / restore with DB proxy auth\n- update API to use CLI interface for backup/restore\n- fixup browser history / links in backup code path\n- add loading spinner to backup/restore features\n\n\n---\n\n[//]: # (END_SECTION 4c603fe4465e0777bc9ab83f138b24bdc790857c)\n[//]: # (START_SECTION 9e3fba57211e2d32b1d4cc2abca9d0212e23cc6f)\n### Fixed #591 - Fixed the backup so that it excluded database views - Added logic to propagate mysql errors when trying to perform a restore\n\n> Commit: [9e3fba57211e2d32b1d4cc2abca9d0212e23cc6f](https://github.com/dOpensource/dsiprouter/commit/9e3fba57211e2d32b1d4cc2abca9d0212e23cc6f)  \n> Date: Mon, 2 Sep 2024 03:47:32 +0000  \n> Author: root (root@ip-172-31-13-77)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9e3fba57211e2d32b1d4cc2abca9d0212e23cc6f)\n[//]: # (START_SECTION 4fadad93e3aa6411e567a424759a451d053f9a58)\n### Resolves #602\n\n> Commit: [4fadad93e3aa6411e567a424759a451d053f9a58](https://github.com/dOpensource/dsiprouter/commit/4fadad93e3aa6411e567a424759a451d053f9a58)  \n> Date: Thu, 30 Jan 2025 11:08:07 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fix fall through condition when building regex for epg dispatcher query\n\n\n---\n\n[//]: # (END_SECTION 4fadad93e3aa6411e567a424759a451d053f9a58)\n[//]: # (START_SECTION b4c5011cba0f5d39566336fd926158c4c0bcfa3a)\n### Bump jinja2 from 3.1.2 to 3.1.3 in /docs\n\n> Commit: [b4c5011cba0f5d39566336fd926158c4c0bcfa3a](https://github.com/dOpensource/dsiprouter/commit/b4c5011cba0f5d39566336fd926158c4c0bcfa3a)  \n> Date: Thu, 11 Jan 2024 21:44:15 +0000  \n> Author: dependabot[bot] (49699333+dependabot[bot]@users.noreply.github.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.2 to 3.1.3.\n- - [Release notes](https://github.com/pallets/jinja/releases)\n- - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)\n- - [Commits](https://github.com/pallets/jinja/compare/3.1.2...3.1.3)\n- ---\n- updated-dependencies:\n- - dependency-name: jinja2\n- dependency-type: indirect\n- ...\n- Signed-off-by: dependabot[bot] <support@github.com>\n\n\n---\n\n[//]: # (END_SECTION b4c5011cba0f5d39566336fd926158c4c0bcfa3a)\n[//]: # (START_SECTION 9d1a14335d324a834625d1e922c8de2d4057b649)\n### Bump jinja2 from 3.0.3 to 3.1.3 in /gui\n\n> Commit: [9d1a14335d324a834625d1e922c8de2d4057b649](https://github.com/dOpensource/dsiprouter/commit/9d1a14335d324a834625d1e922c8de2d4057b649)  \n> Date: Thu, 11 Jan 2024 19:21:16 +0000  \n> Author: dependabot[bot] (49699333+dependabot[bot]@users.noreply.github.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Bumps [jinja2](https://github.com/pallets/jinja) from 3.0.3 to 3.1.3.\n- - [Release notes](https://github.com/pallets/jinja/releases)\n- - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)\n- - [Commits](https://github.com/pallets/jinja/compare/3.0.3...3.1.3)\n- ---\n- updated-dependencies:\n- - dependency-name: jinja2\n- dependency-type: direct:production\n- ...\n- Signed-off-by: dependabot[bot] <support@github.com>\n\n\n---\n\n[//]: # (END_SECTION 9d1a14335d324a834625d1e922c8de2d4057b649)\n[//]: # (START_SECTION 103bc6e4a3c22b37c1f52bdfa79e48d90dd86e2e)\n### Fix Standalone Run of generateCDRS()\n\n> Commit: [103bc6e4a3c22b37c1f52bdfa79e48d90dd86e2e](https://github.com/dOpensource/dsiprouter/commit/103bc6e4a3c22b37c1f52bdfa79e48d90dd86e2e)  \n> Date: Mon, 20 Jan 2025 09:23:26 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 103bc6e4a3c22b37c1f52bdfa79e48d90dd86e2e)\n[//]: # (START_SECTION ba663bd3cc3c16f7c33c8f7977e1b4d5051efc0d)\n### Cronjob Edge Cases\n\n> Commit: [ba663bd3cc3c16f7c33c8f7977e1b4d5051efc0d](https://github.com/dOpensource/dsiprouter/commit/ba663bd3cc3c16f7c33c8f7977e1b4d5051efc0d)  \n> Date: Wed, 28 Aug 2024 09:10:25 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- detect if CDR cron entry missing on update and add it\n- update default python paths to venv on shebang lines\n\n\n---\n\n[//]: # (END_SECTION ba663bd3cc3c16f7c33c8f7977e1b4d5051efc0d)\n[//]: # (START_SECTION 41a1a512aba83310e57d6c2c4d6e19a35a633900)\n### Set Defaults For restart\n\n> Commit: [41a1a512aba83310e57d6c2c4d6e19a35a633900](https://github.com/dOpensource/dsiprouter/commit/41a1a512aba83310e57d6c2c4d6e19a35a633900)  \n> Date: Fri, 17 Jan 2025 11:27:41 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- give `RESTART_DAEMONIZE` a default value so the func is more reusable\n\n\n---\n\n[//]: # (END_SECTION 41a1a512aba83310e57d6c2c4d6e19a35a633900)\n[//]: # (START_SECTION 325b4c18db320c491021681490d1bc364524b000)\n### Networking Updates\n\n> Commit: [325b4c18db320c491021681490d1bc364524b000](https://github.com/dOpensource/dsiprouter/commit/325b4c18db320c491021681490d1bc364524b000)  \n> Date: Wed, 15 Jan 2025 14:40:57 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fix dsip-net-cfg reverted when system network mgmt service reloads\n- improve output / parsing of `-debug` options\n- fix swapfile not written in fstab\n- fix null returned when no default route in `getInternalIP()`\n- fix comment typo in debian install script\n\n\n---\n\n[//]: # (END_SECTION 325b4c18db320c491021681490d1bc364524b000)\n[//]: # (START_SECTION 5edf7567a52854a403c21b66c785c4a529b8dd7e)\n### Fix Some Dependent Sequencing\n\n> Commit: [5edf7567a52854a403c21b66c785c4a529b8dd7e](https://github.com/dOpensource/dsiprouter/commit/5edf7567a52854a403c21b66c785c4a529b8dd7e)  \n> Date: Tue, 14 Jan 2025 11:40:39 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- move system path config to `install` / `uninstall` subcommands\n- move `setStaticScriptSettings()` up before funcs expecting dsip config dir\n\n\n---\n\n[//]: # (END_SECTION 5edf7567a52854a403c21b66c785c4a529b8dd7e)\n[//]: # (START_SECTION 472c343c957240dcebe500f405e3d818fd65f6fc)\n### Dynamic Network Mode Improvements\n\n> Commit: [472c343c957240dcebe500f405e3d818fd65f6fc](https://github.com/dOpensource/dsiprouter/commit/472c343c957240dcebe500f405e3d818fd65f6fc)  \n> Date: Wed, 8 Jan 2025 12:05:33 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- improve IP selection algo for internal addresses\n- make floating IP the default route on supported deployments\n- add hook in dsip-init for network updates prior cfg updates\n\n\n---\n\n[//]: # (END_SECTION 472c343c957240dcebe500f405e3d818fd65f6fc)\n[//]: # (START_SECTION 3400b7d680e252f1b193294c24385740e0ea4cf9)\n### Fix Call Settings Htable\n\n> Commit: [3400b7d680e252f1b193294c24385740e0ea4cf9](https://github.com/dOpensource/dsiprouter/commit/3400b7d680e252f1b193294c24385740e0ea4cf9)  \n> Date: Wed, 11 Sep 2024 07:30:08 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fix issue where call settings htable would not load\n- remove unused drouting htable and replace with prefix_to_route\n\n\n---\n\n[//]: # (END_SECTION 3400b7d680e252f1b193294c24385740e0ea4cf9)\n[//]: # (START_SECTION 6bfcbbe3ccc56fef2378e7d1305751318ac508e6)\n### Fix Concurrent Call Limits\n\n> Commit: [6bfcbbe3ccc56fef2378e7d1305751318ac508e6](https://github.com/dOpensource/dsiprouter/commit/6bfcbbe3ccc56fef2378e7d1305751318ac508e6)  \n> Date: Mon, 6 Jan 2025 15:04:59 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- use dialog var so dialog event routes can access the src/dst gwgroupid\n\n\n---\n\n[//]: # (END_SECTION 6bfcbbe3ccc56fef2378e7d1305751318ac508e6)\n[//]: # (START_SECTION 8a9eefe4814db0d20e6d5030f6e9865f47711513)\n### Fix CloudInit Networking\n\n> Commit: [8a9eefe4814db0d20e6d5030f6e9865f47711513](https://github.com/dOpensource/dsiprouter/commit/8a9eefe4814db0d20e6d5030f6e9865f47711513)  \n> Date: Thu, 3 Oct 2024 09:24:55 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fix images created from instances failing to configure new ifaces\n\n\n---\n\n[//]: # (END_SECTION 8a9eefe4814db0d20e6d5030f6e9865f47711513)\n[//]: # (START_SECTION 5737d1f33c99de90e0ef9188ca6c840292896567)\n### Fix Apt Variable Sequencing\n\n> Commit: [5737d1f33c99de90e0ef9188ca6c840292896567](https://github.com/dOpensource/dsiprouter/commit/5737d1f33c99de90e0ef9188ca6c840292896567)  \n> Date: Tue, 14 Jan 2025 10:42:59 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- move the APT config variables into `setStaticScriptSettings()`\n\n\n---\n\n[//]: # (END_SECTION 5737d1f33c99de90e0ef9188ca6c840292896567)\n[//]: # (START_SECTION cced1d2cd835ca6d546a22b78dcc6d46f2c1a874)\n### Fix TLS Cert Renewal Check\n\n> Commit: [cced1d2cd835ca6d546a22b78dcc6d46f2c1a874](https://github.com/dOpensource/dsiprouter/commit/cced1d2cd835ca6d546a22b78dcc6d46f2c1a874)  \n> Date: Thu, 21 Nov 2024 12:22:11 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fix typo in cli function\n\n\n---\n\n[//]: # (END_SECTION cced1d2cd835ca6d546a22b78dcc6d46f2c1a874)\n[//]: # (START_SECTION 357eca71bf3767d10f36b6e2db1249048a399c54)\n### Backport UltraDict Build Fix to Debian-based OS\n\n> Commit: [357eca71bf3767d10f36b6e2db1249048a399c54](https://github.com/dOpensource/dsiprouter/commit/357eca71bf3767d10f36b6e2db1249048a399c54)  \n> Date: Mon, 13 Jan 2025 10:25:40 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 357eca71bf3767d10f36b6e2db1249048a399c54)\n[//]: # (START_SECTION cd0db1a1f6bf45e09e31ff56e6e3541bd617ea70)\n### Debian12 UltraDict Dependency Fix\n\n> Commit: [cd0db1a1f6bf45e09e31ff56e6e3541bd617ea70](https://github.com/dOpensource/dsiprouter/commit/cd0db1a1f6bf45e09e31ff56e6e3541bd617ea70)  \n> Date: Fri, 8 Nov 2024 13:50:00 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- run UltraDict install separate to workaround hanging install\n\n\n---\n\n[//]: # (END_SECTION cd0db1a1f6bf45e09e31ff56e6e3541bd617ea70)\n[//]: # (START_SECTION 8b79cd5e57ee370fe39d8d2234b995c7a0c3667f)\n### Backport UltraDict Build Fix to Debian-based OS\n\n> Commit: [8b79cd5e57ee370fe39d8d2234b995c7a0c3667f](https://github.com/dOpensource/dsiprouter/commit/8b79cd5e57ee370fe39d8d2234b995c7a0c3667f)  \n> Date: Mon, 13 Jan 2025 10:25:40 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 8b79cd5e57ee370fe39d8d2234b995c7a0c3667f)\n[//]: # (START_SECTION 435b3cadb5cdc837343d12c04199ef58ecc88123)\n### Fix Libjwt / Libstirshaken Builds\n\n> Commit: [435b3cadb5cdc837343d12c04199ef58ecc88123](https://github.com/dOpensource/dsiprouter/commit/435b3cadb5cdc837343d12c04199ef58ecc88123)  \n> Date: Mon, 13 Jan 2025 09:57:12 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix build for debian-based systems\n- pin libjwt to 2.1.1\n\n\n---\n\n[//]: # (END_SECTION 435b3cadb5cdc837343d12c04199ef58ecc88123)\n[//]: # (START_SECTION ec09a74859312f30a48ec11dd57ed682f4da3868)\n### OS Support Updates\n\n> Commit: [ec09a74859312f30a48ec11dd57ed682f4da3868](https://github.com/dOpensource/dsiprouter/commit/ec09a74859312f30a48ec11dd57ed682f4da3868)  \n> Date: Tue, 19 Nov 2024 14:23:43 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- centos7 --> deprecated\n- centos9 stability fixes\n- rhel9 --> alpha\n- rhel8 --> beta\n- alma8 --> alpha\n- alma8 --> beta\n- rocky9 --> alpha\n- rocky8 --> beta\n- ubuntu 24.04  --> beta\n- debian 10/11/12 pinned kamailio to 5.8.3\n- debian 9 pinned kamailio to 5.5.7\n- centos 8/9 pinned kamailio to 5.8.3\n- centos7 pinned kamailio to 5.7.6\n- amazn2 pinned kamailo to 5.7.6\n- ubuntu 24.04 pinned kamailio to 5.8.4 / rtpengine to mr11.5.1.11\n- ubuntu 22.04 pinned kamailio to 5.8.3 / rtpengine to mr11.5.1.11\n- ubuntu 20.04 pinned kamailio to 5.8.3\n- rhel8/9 pinned kamailio to 5.8.3\n- alma 8/9 pinned kamalio to 5.8.3 / rtpengine to mr11.5.1.11\n- rocky 8/9 pinned kamalio to 5.8.3 / rtpengine to mr11.5.1.11\n- add back in swap file for low memory systems (2GB)\n- update OS support in docs\n- fix `RTPENGINE_URI` missing from `dsip_settings` python interfaces\n\n\n---\n\n[//]: # (END_SECTION ec09a74859312f30a48ec11dd57ed682f4da3868)\n[//]: # (START_SECTION edc1122b046b2948c5ccff28940693c0dcae2821)\n### Debian12 UltraDict Dependency Fix\n\n> Commit: [edc1122b046b2948c5ccff28940693c0dcae2821](https://github.com/dOpensource/dsiprouter/commit/edc1122b046b2948c5ccff28940693c0dcae2821)  \n> Date: Fri, 8 Nov 2024 13:50:00 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- run UltraDict install separate to workaround hanging install\n\n\n---\n\n[//]: # (END_SECTION edc1122b046b2948c5ccff28940693c0dcae2821)\n[//]: # (START_SECTION deb523e80b4f468c47b0f56e89480b8d19b2c899)\n### Fix isHostLocal Matching Pattern\n\n> Commit: [deb523e80b4f468c47b0f56e89480b8d19b2c899](https://github.com/dOpensource/dsiprouter/commit/deb523e80b4f468c47b0f56e89480b8d19b2c899)  \n> Date: Thu, 31 Oct 2024 14:15:02 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION deb523e80b4f468c47b0f56e89480b8d19b2c899)\n[//]: # (START_SECTION 2e52b3173dc0bfec031da450adc0bc06ff51dab2)\n### Support Remote RTPEngine Media Server\n\n> Commit: [2e52b3173dc0bfec031da450adc0bc06ff51dab2](https://github.com/dOpensource/dsiprouter/commit/2e52b3173dc0bfec031da450adc0bc06ff51dab2)  \n> Date: Tue, 24 Sep 2024 18:48:30 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for media proxy through a remote RTPEngine instance\n- add capability for rtpengine service to be dynamically disabled\n- update RHEL-based distros rtpengine installs\n- add CLI option `--rtpengine-uri=` to `install` subcommand\n- improve local host check for services that can be either remote or local\n\n\n---\n\n[//]: # (END_SECTION 2e52b3173dc0bfec031da450adc0bc06ff51dab2)\n[//]: # (START_SECTION 2d2491605e0912a54a66a6e141b5f093120f6ee6)\n### Fix HEP Port Not Set In Some Cases\n\n> Commit: [2d2491605e0912a54a66a6e141b5f093120f6ee6](https://github.com/dOpensource/dsiprouter/commit/2d2491605e0912a54a66a6e141b5f093120f6ee6)  \n> Date: Fri, 20 Sep 2024 13:12:18 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- remove HEP port from static variables in CLI\n- make dynamic lookups for homer variables more reliable\n\n\n---\n\n[//]: # (END_SECTION 2d2491605e0912a54a66a6e141b5f093120f6ee6)\n[//]: # (START_SECTION abc7359613387ce22dc82a1f8bf9a336835f505e)\n### Fix Homer Updates in RTPEngine Config\n\n> Commit: [abc7359613387ce22dc82a1f8bf9a336835f505e](https://github.com/dOpensource/dsiprouter/commit/abc7359613387ce22dc82a1f8bf9a336835f505e)  \n> Date: Wed, 11 Sep 2024 16:04:29 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- get default values from `settings.py` when running `dsiprouter updatertpconfig`\n\n\n---\n\n[//]: # (END_SECTION abc7359613387ce22dc82a1f8bf9a336835f505e)\n[//]: # (START_SECTION fe0127248147c98388771c02900afa45c17eb565)\n### Compartmentalize Mysql Portion of Installation\n\n> Commit: [fe0127248147c98388771c02900afa45c17eb565](https://github.com/dOpensource/dsiprouter/commit/fe0127248147c98388771c02900afa45c17eb565)  \n> Date: Sun, 11 Aug 2024 11:41:39 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- mariadb server now installs with `-all` or explicitly with `-mysql`\n- move dev/lib package installs to the rtpengine installation scripts\n\n\n---\n\n[//]: # (END_SECTION fe0127248147c98388771c02900afa45c17eb565)\n[//]: # (START_SECTION f8aecea6bd3c217b2073234c9f6ba3fae4c4e5f8)\n### Allow External RTPEngine to be Configured\n\n> Commit: [f8aecea6bd3c217b2073234c9f6ba3fae4c4e5f8](https://github.com/dOpensource/dsiprouter/commit/f8aecea6bd3c217b2073234c9f6ba3fae4c4e5f8)  \n> Date: Sun, 11 Aug 2024 13:16:48 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for specifying remote rtpengine uri during install\n- add settings to allow changing the above later on\n- fix typos in `dsip_lib.sh`\n- update DB URI parsing func to not rely on system python\n\n\n---\n\n[//]: # (END_SECTION f8aecea6bd3c217b2073234c9f6ba3fae4c4e5f8)\n[//]: # (START_SECTION 4bae4f3d0153a244c1441213327a83884547ea96)\n### Fix HA Script Not Removing init Commands\n\n> Commit: [4bae4f3d0153a244c1441213327a83884547ea96](https://github.com/dOpensource/dsiprouter/commit/4bae4f3d0153a244c1441213327a83884547ea96)  \n> Date: Sun, 11 Aug 2024 13:23:43 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 4bae4f3d0153a244c1441213327a83884547ea96)\n[//]: # (START_SECTION f833e8374efecbc4a9319251db9941f68f331617)\n### Allow Python Selection for hashCreds()\n\n> Commit: [f833e8374efecbc4a9319251db9941f68f331617](https://github.com/dOpensource/dsiprouter/commit/f833e8374efecbc4a9319251db9941f68f331617)  \n> Date: Mon, 12 Aug 2024 07:10:46 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- this will allow us to remove the system python package after installation\n\n\n---\n\n[//]: # (END_SECTION f833e8374efecbc4a9319251db9941f68f331617)\n[//]: # (START_SECTION 17084e4bb069807f065f847086b5b002ff4d54ed)\n### Fixed LibJWT: - Missed adding install as a command to make\n\n> Commit: [17084e4bb069807f065f847086b5b002ff4d54ed](https://github.com/dOpensource/dsiprouter/commit/17084e4bb069807f065f847086b5b002ff4d54ed)  \n> Date: Sat, 4 Jan 2025 01:32:21 +0000  \n> Author: root (root@demo.dsiprouter.net)  \n> Committer: root (root@demo.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 17084e4bb069807f065f847086b5b002ff4d54ed)\n[//]: # (START_SECTION fea919d0b4f5184c4ae2d319ed39cfdfb1e3a1dd)\n### Fix LibJWT: The LibJWT build system switched over to cmake from autoconf so Kamailio install script had to be updated\n\n> Commit: [fea919d0b4f5184c4ae2d319ed39cfdfb1e3a1dd](https://github.com/dOpensource/dsiprouter/commit/fea919d0b4f5184c4ae2d319ed39cfdfb1e3a1dd)  \n> Date: Fri, 3 Jan 2025 23:52:26 +0000  \n> Author: root (root@demo.dsiprouter.net)  \n> Committer: root (root@demo.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fea919d0b4f5184c4ae2d319ed39cfdfb1e3a1dd)\n[//]: # (START_SECTION 351dc8ef45d4f09072584b1b906bc0b04973df8d)\n### Fix libjwt Not Compiling\n\n> Commit: [351dc8ef45d4f09072584b1b906bc0b04973df8d](https://github.com/dOpensource/dsiprouter/commit/351dc8ef45d4f09072584b1b906bc0b04973df8d)  \n> Date: Wed, 18 Dec 2024 12:56:49 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 351dc8ef45d4f09072584b1b906bc0b04973df8d)\n[//]: # (START_SECTION bda08eb49639f20e85563131cc2356770eaea61f)\n### Stabilize Flux Capacitors\n\n> Commit: [bda08eb49639f20e85563131cc2356770eaea61f](https://github.com/dOpensource/dsiprouter/commit/bda08eb49639f20e85563131cc2356770eaea61f)  \n> Date: Mon, 25 Nov 2024 17:05:43 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- rhel9 --> STABLE\n- rhel9: bump rtpengine version to mr11.5.1.11\n- alma9 --> STABLE\n- rocky9 --> STABLE\n- ubuntu 24.04 --> STABLE\n- rhel9/alma9/rocky9/centos9 update to use dsiprouter-repo instead of rpmfusion\n\n\n---\n\n[//]: # (END_SECTION bda08eb49639f20e85563131cc2356770eaea61f)\n[//]: # (START_SECTION 3408b9a6c8d68a473bb2b7a53ec31ceff6900f91)\n### OS Support Updates\n\n> Commit: [3408b9a6c8d68a473bb2b7a53ec31ceff6900f91](https://github.com/dOpensource/dsiprouter/commit/3408b9a6c8d68a473bb2b7a53ec31ceff6900f91)  \n> Date: Tue, 19 Nov 2024 14:23:43 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- centos7 --> deprecated\n- centos9 stability fixes\n- rhel9 --> alpha\n- rhel8 --> beta\n- alma8 --> alpha\n- alma8 --> beta\n- rocky9 --> alpha\n- rocky8 --> beta\n- ubuntu 24.04  --> beta\n- debian 10/11/12 pinned kamailio to 5.8.3\n- debian 9 pinned kamailio to 5.5.7\n- centos 8/9 pinned kamailio to 5.8.3\n- centos7 pinned kamailio to 5.7.6\n- amazn2 pinned kamailo to 5.7.6\n- ubuntu 24.04 pinned kamailio to 5.8.4 / rtpengine to mr11.5.1.11\n- ubuntu 22.04 pinned kamailio to 5.8.3 / rtpengine to mr11.5.1.11\n- ubuntu 20.04 pinned kamailio to 5.8.3\n- rhel8/9 pinned kamailio to 5.8.3\n- alma 8/9 pinned kamalio to 5.8.3 / rtpengine to mr11.5.1.11\n- rocky 8/9 pinned kamalio to 5.8.3 / rtpengine to mr11.5.1.11\n- add back in swap file for low memory systems (2GB)\n- update OS support in docs\n- fix `RTPENGINE_URI` missing from `dsip_settings` python interfaces\n\n\n---\n\n[//]: # (END_SECTION 3408b9a6c8d68a473bb2b7a53ec31ceff6900f91)\n[//]: # (START_SECTION 014c30de6ac92ab9ca51e758b74c8563a875d98b)\n### Debian12 UltraDict Dependency Fix\n\n> Commit: [014c30de6ac92ab9ca51e758b74c8563a875d98b](https://github.com/dOpensource/dsiprouter/commit/014c30de6ac92ab9ca51e758b74c8563a875d98b)  \n> Date: Fri, 8 Nov 2024 13:50:00 -0700  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- run UltraDict install separate to workaround hanging install\n\n\n---\n\n[//]: # (END_SECTION 014c30de6ac92ab9ca51e758b74c8563a875d98b)\n[//]: # (START_SECTION b46462ea5d8160b69a7999de6116f0b2aa0c42ed)\n### Fix isHostLocal Matching Pattern\n\n> Commit: [b46462ea5d8160b69a7999de6116f0b2aa0c42ed](https://github.com/dOpensource/dsiprouter/commit/b46462ea5d8160b69a7999de6116f0b2aa0c42ed)  \n> Date: Thu, 31 Oct 2024 14:15:02 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b46462ea5d8160b69a7999de6116f0b2aa0c42ed)\n[//]: # (START_SECTION 1c46e8cd1d1cf82cced662b3e6af94b667c77ac9)\n### Support Remote RTPEngine Media Server\n\n> Commit: [1c46e8cd1d1cf82cced662b3e6af94b667c77ac9](https://github.com/dOpensource/dsiprouter/commit/1c46e8cd1d1cf82cced662b3e6af94b667c77ac9)  \n> Date: Tue, 24 Sep 2024 18:48:30 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- add support for media proxy through a remote RTPEngine instance\n- add capability for rtpengine service to be dynamically disabled\n- update RHEL-based distros rtpengine installs\n- add CLI option `--rtpengine-uri=` to `install` subcommand\n- improve local host check for services that can be either remote or local\n\n\n---\n\n[//]: # (END_SECTION 1c46e8cd1d1cf82cced662b3e6af94b667c77ac9)\n[//]: # (START_SECTION dcf2b437e94bf987e60d1c16bf85b331de1ff592)\n### Fix HEP Port Not Set In Some Cases\n\n> Commit: [dcf2b437e94bf987e60d1c16bf85b331de1ff592](https://github.com/dOpensource/dsiprouter/commit/dcf2b437e94bf987e60d1c16bf85b331de1ff592)  \n> Date: Fri, 20 Sep 2024 13:12:18 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- remove HEP port from static variables in CLI\n- make dynamic lookups for homer variables more reliable\n\n\n---\n\n[//]: # (END_SECTION dcf2b437e94bf987e60d1c16bf85b331de1ff592)\n[//]: # (START_SECTION f0ac44f8041b10a3675f5a2accfe29199a795990)\n### Fix Homer Updates in RTPEngine Config\n\n> Commit: [f0ac44f8041b10a3675f5a2accfe29199a795990](https://github.com/dOpensource/dsiprouter/commit/f0ac44f8041b10a3675f5a2accfe29199a795990)  \n> Date: Wed, 11 Sep 2024 16:04:29 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- get default values from `settings.py` when running `dsiprouter updatertpconfig`\n\n\n---\n\n[//]: # (END_SECTION f0ac44f8041b10a3675f5a2accfe29199a795990)\n[//]: # (START_SECTION 482bd41f6d5bc599e632c76095c3f0ea09ee7712)\n### Compartmentalize Mysql Portion of Installation\n\n> Commit: [482bd41f6d5bc599e632c76095c3f0ea09ee7712](https://github.com/dOpensource/dsiprouter/commit/482bd41f6d5bc599e632c76095c3f0ea09ee7712)  \n> Date: Sun, 11 Aug 2024 11:41:39 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- mariadb server now installs with `-all` or explicitly with `-mysql`\n- move dev/lib package installs to the rtpengine installation scripts\n\n\n---\n\n[//]: # (END_SECTION 482bd41f6d5bc599e632c76095c3f0ea09ee7712)\n[//]: # (START_SECTION 559fec30e82c5553b483acfe7c59d801d65cfd5d)\n### Allow External RTPEngine to be Configured\n\n> Commit: [559fec30e82c5553b483acfe7c59d801d65cfd5d](https://github.com/dOpensource/dsiprouter/commit/559fec30e82c5553b483acfe7c59d801d65cfd5d)  \n> Date: Sun, 11 Aug 2024 13:16:48 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- add support for specifying remote rtpengine uri during install\n- add settings to allow changing the above later on\n- fix typos in `dsip_lib.sh`\n- update DB URI parsing func to not rely on system python\n\n\n---\n\n[//]: # (END_SECTION 559fec30e82c5553b483acfe7c59d801d65cfd5d)\n[//]: # (START_SECTION d2f71046f6eb8b7dabc13cbb88b74d530d686367)\n### Fix HA Script Not Removing init Commands\n\n> Commit: [d2f71046f6eb8b7dabc13cbb88b74d530d686367](https://github.com/dOpensource/dsiprouter/commit/d2f71046f6eb8b7dabc13cbb88b74d530d686367)  \n> Date: Sun, 11 Aug 2024 13:23:43 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d2f71046f6eb8b7dabc13cbb88b74d530d686367)\n[//]: # (START_SECTION 1b8064c9760fd98037e0471373fdc6288f8c2910)\n### Allow Python Selection for hashCreds()\n\n> Commit: [1b8064c9760fd98037e0471373fdc6288f8c2910](https://github.com/dOpensource/dsiprouter/commit/1b8064c9760fd98037e0471373fdc6288f8c2910)  \n> Date: Mon, 12 Aug 2024 07:10:46 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- this will allow us to remove the system python package after installation\n\n\n---\n\n[//]: # (END_SECTION 1b8064c9760fd98037e0471373fdc6288f8c2910)\n[//]: # (START_SECTION 313906939ddd7d64a3ef9562c3e86c7101bdff8c)\n### Explictily defined the Kamailio modules that will be installed.  This overcomes the bug in the lastest version of Kamailio, which has ims_icscf as a default extra module to install, but the corresponding ims_icscf-create.sql file is not part of the Debian package\n\n> Commit: [313906939ddd7d64a3ef9562c3e86c7101bdff8c](https://github.com/dOpensource/dsiprouter/commit/313906939ddd7d64a3ef9562c3e86c7101bdff8c)  \n> Date: Mon, 18 Nov 2024 03:34:53 +0000  \n> Author: root (root@mack.test.dsiprouter.net)  \n> Committer: root (root@mack.test.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 313906939ddd7d64a3ef9562c3e86c7101bdff8c)\n[//]: # (START_SECTION 1d7bc2d6a526458a943b79cc23b49169cdf4974e)\n### Made Terrafrom Config more generic\n\n> Commit: [1d7bc2d6a526458a943b79cc23b49169cdf4974e](https://github.com/dOpensource/dsiprouter/commit/1d7bc2d6a526458a943b79cc23b49169cdf4974e)  \n> Date: Mon, 11 Nov 2024 01:43:22 +0000  \n> Author: root (root@chelsea.test.dsiprouter.net)  \n> Committer: root (root@chelsea.test.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1d7bc2d6a526458a943b79cc23b49169cdf4974e)\n[//]: # (START_SECTION 9b5afa06020b84b598576c56edc09f3fa98be14c)\n### Increased the default size of DigitalOcean Images from 1gb to 2gb\n\n> Commit: [9b5afa06020b84b598576c56edc09f3fa98be14c](https://github.com/dOpensource/dsiprouter/commit/9b5afa06020b84b598576c56edc09f3fa98be14c)  \n> Date: Sun, 10 Nov 2024 23:12:26 +0000  \n> Author: root (root@chelsea.test.dsiprouter.net)  \n> Committer: root (root@chelsea.test.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9b5afa06020b84b598576c56edc09f3fa98be14c)\n[//]: # (START_SECTION 3da760ed578537f9f39475bb6404d3931af22dfc)\n### Made UltraDict the first python package to be installed\n\n> Commit: [3da760ed578537f9f39475bb6404d3931af22dfc](https://github.com/dOpensource/dsiprouter/commit/3da760ed578537f9f39475bb6404d3931af22dfc)  \n> Date: Sun, 10 Nov 2024 22:47:22 +0000  \n> Author: root (root@mack.test.dsiprouter.net)  \n> Committer: root (root@mack.test.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3da760ed578537f9f39475bb6404d3931af22dfc)\n[//]: # (START_SECTION 92eefb44fc4b1cb17ad9908a77b7587f5a408baa)\n### Terraform PR Build\n\n> Commit: [92eefb44fc4b1cb17ad9908a77b7587f5a408baa](https://github.com/dOpensource/dsiprouter/commit/92eefb44fc4b1cb17ad9908a77b7587f5a408baa)  \n> Date: Thu, 3 Oct 2024 17:02:42 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for building from a PR in terraform builds\n\n\n---\n\n[//]: # (END_SECTION 92eefb44fc4b1cb17ad9908a77b7587f5a408baa)\n[//]: # (START_SECTION e19f0de2ab42f6630ca65aff48a7cea1b2f656c8)\n### Fix UltraDict Build on Debian12\n\n> Commit: [e19f0de2ab42f6630ca65aff48a7cea1b2f656c8](https://github.com/dOpensource/dsiprouter/commit/e19f0de2ab42f6630ca65aff48a7cea1b2f656c8)  \n> Date: Wed, 11 Sep 2024 11:24:35 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add libffi-dev dependency to fix UltraDict wheel build\n\n\n---\n\n[//]: # (END_SECTION e19f0de2ab42f6630ca65aff48a7cea1b2f656c8)\n[//]: # (START_SECTION 9560aab94b63edc79181c8e973a226cffaf89aa3)\n### Ensure Python venv Built From New System Deps on Debian11\n\n> Commit: [9560aab94b63edc79181c8e973a226cffaf89aa3](https://github.com/dOpensource/dsiprouter/commit/9560aab94b63edc79181c8e973a226cffaf89aa3)  \n> Date: Tue, 3 Sep 2024 15:53:38 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 9560aab94b63edc79181c8e973a226cffaf89aa3)\n[//]: # (START_SECTION 59a745237cd9586f8dc2224a423365724a90ad21)\n### Update dsiprouter.sh\n\n> Commit: [59a745237cd9586f8dc2224a423365724a90ad21](https://github.com/dOpensource/dsiprouter/commit/59a745237cd9586f8dc2224a423365724a90ad21)  \n> Date: Tue, 13 Aug 2024 06:07:37 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Removed the creation of swap and removal of swap from the installer until we review the effects on different operating systems.\n\n\n---\n\n[//]: # (END_SECTION 59a745237cd9586f8dc2224a423365724a90ad21)\n[//]: # (START_SECTION 8bc13858f70633ec2ec2ade2b878759a3d6715b0)\n### Fix Stubborn Woocommerce \"Delivered\" Status Check\n\n> Commit: [8bc13858f70633ec2ec2ade2b878759a3d6715b0](https://github.com/dOpensource/dsiprouter/commit/8bc13858f70633ec2ec2ade2b878759a3d6715b0)  \n> Date: Thu, 8 Aug 2024 11:56:32 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 8bc13858f70633ec2ec2ade2b878759a3d6715b0)\n[//]: # (START_SECTION 6c73f8c46eb1477fcbaee6014e2a861e80f04c81)\n### Updated Terraform scripts to work with Jenkins Parameters\n\n> Commit: [6c73f8c46eb1477fcbaee6014e2a861e80f04c81](https://github.com/dOpensource/dsiprouter/commit/6c73f8c46eb1477fcbaee6014e2a861e80f04c81)  \n> Date: Thu, 8 Aug 2024 05:25:00 +0000  \n> Author: Mack Hendricks (mack.hendricks@gmail.com)  \n> Committer: Mack Hendricks (mack.hendricks@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6c73f8c46eb1477fcbaee6014e2a861e80f04c81)\n[//]: # (START_SECTION 073aae22bea1191cd548d7ee676d571df4487891)\n### Added logic to handle adding and updating carriers from the UI\n\n> Commit: [073aae22bea1191cd548d7ee676d571df4487891](https://github.com/dOpensource/dsiprouter/commit/073aae22bea1191cd548d7ee676d571df4487891)  \n> Date: Thu, 8 Aug 2024 03:50:53 +0000  \n> Author: Mack Hendricks (mack.hendricks@gmail.com)  \n> Committer: Mack Hendricks (mack.hendricks@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 073aae22bea1191cd548d7ee676d571df4487891)\n[//]: # (START_SECTION 0a9b0322870cd277aa41e0abb7f95ed026909d4f)\n### Fixed Carriergroups Post API\n\n> Commit: [0a9b0322870cd277aa41e0abb7f95ed026909d4f](https://github.com/dOpensource/dsiprouter/commit/0a9b0322870cd277aa41e0abb7f95ed026909d4f)  \n> Date: Wed, 7 Aug 2024 23:50:21 +0000  \n> Author: root (root@demo-dsip-v0.760)  \n> Committer: root (root@demo-dsip-v0.760)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0a9b0322870cd277aa41e0abb7f95ed026909d4f)\n[//]: # (START_SECTION 2f42fe9d97523d96b226f74ad9c25f60ff3a992f)\n### Update Version in Settings File\n\n> Commit: [2f42fe9d97523d96b226f74ad9c25f60ff3a992f](https://github.com/dOpensource/dsiprouter/commit/2f42fe9d97523d96b226f74ad9c25f60ff3a992f)  \n> Date: Wed, 7 Aug 2024 13:43:25 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 2f42fe9d97523d96b226f74ad9c25f60ff3a992f)\n[//]: # (START_SECTION 4ef9068ca819def5eb7df6abdd9f799c4baaedd4)\n### Add Support for Upgrading to v0.76\n\n> Commit: [4ef9068ca819def5eb7df6abdd9f799c4baaedd4](https://github.com/dOpensource/dsiprouter/commit/4ef9068ca819def5eb7df6abdd9f799c4baaedd4)  \n> Date: Sun, 4 Aug 2024 14:28:00 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 4ef9068ca819def5eb7df6abdd9f799c4baaedd4)\n[//]: # (START_SECTION a5513488ce466250a812748d7d757af95acdff8e)\n### Better Handling ufw Removal\n\n> Commit: [a5513488ce466250a812748d7d757af95acdff8e](https://github.com/dOpensource/dsiprouter/commit/a5513488ce466250a812748d7d757af95acdff8e)  \n> Date: Sun, 4 Aug 2024 13:24:11 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION a5513488ce466250a812748d7d757af95acdff8e)\n[//]: # (START_SECTION a10cce22b9a632e0b4a1540b0fc06ed42a4412a3)\n### Fix DMZ Feature Listening Sockets (#584)\n\n> Commit: [a10cce22b9a632e0b4a1540b0fc06ed42a4412a3](https://github.com/dOpensource/dsiprouter/commit/a10cce22b9a632e0b4a1540b0fc06ed42a4412a3)  \n> Date: Sun, 4 Aug 2024 13:02:05 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- * Better Naming for Sending Media Directly to Endpoint\r\n\n- change naming from \"bypass media\" to \"direct media\"\r\n\n- * Domain Routing Translations\r\n\n- add support for signalling / media features on domain pass thru\r\n\n- * Carrier Group Regression Hotfix\r\n\n- fix regression in attributes query for carriers\r\n\n- * Fix CarrierGroup Load Balancing Icon\r\n\n- fix load balancing icon not shown on toggle button\r\n\n- * Fix CarrierGroup lb_enabled Not Forwarded\r\n\n- add `lb_enabled` to data forwarded to the backend\r\n\n- * Fix Carrier Update Via API Out of Sync\r\n\n- updated the API logic for carriers to match the GUI logic\r\n\n- * Fix SQL Munging on CSV Field Queries\r\n\n- fixed a class of bugs where the description and tag fields could return the wrong rows\r\n\n- * Fix Dangling Comma When Adding Carriers\r\n\n- handle case when `split()` is called on empty `gwlist`\r\n\n- * Dispatcher Fixes\r\n\n- fix dispatcher relations and related queries\r\n- update defaults to match new formats of table associations\r\n\n- * Remove Duplicate Chcek for validateFields()\r\n\n- * Fix Empty Domain Prevents Kamailio Reload\r\n\n- * CarrierGroup Load Balancing Missing Disable\r\n\n- fix the DB query that disables load balancing for carrier groups\r\n\n- * Fix DMZ Feature Listening Sockets\r\n\n- fix logic bug in ifdefs causing kam to not listen on ext iface\n\n\n---\n\n[//]: # (END_SECTION a10cce22b9a632e0b4a1540b0fc06ed42a4412a3)\n[//]: # (START_SECTION 52cfbb1f29bb83db1dc2f4d4454d1da048f2a467)\n### License Manager CLI\n\n> Commit: [52cfbb1f29bb83db1dc2f4d4454d1da048f2a467](https://github.com/dOpensource/dsiprouter/commit/52cfbb1f29bb83db1dc2f4d4454d1da048f2a467)  \n> Date: Sun, 4 Aug 2024 12:56:07 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for `licensemanager` command in the dsiprouter CLI\n- add documentation for new command\n- add CLI completion for new command\n\n\n---\n\n[//]: # (END_SECTION 52cfbb1f29bb83db1dc2f4d4454d1da048f2a467)\n[//]: # (START_SECTION f1d9e0098908132570596a1fa9fd2ceb17978d14)\n### LIcense Manager Function Improvements\n\n> Commit: [f1d9e0098908132570596a1fa9fd2ceb17978d14](https://github.com/dOpensource/dsiprouter/commit/f1d9e0098908132570596a1fa9fd2ceb17978d14)  \n> Date: Sun, 4 Aug 2024 12:31:07 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- improve the function api's to be more useful\n- make check for shared memory loaded when upgrading more reliable\n\n\n---\n\n[//]: # (END_SECTION f1d9e0098908132570596a1fa9fd2ceb17978d14)\n[//]: # (START_SECTION 1ab82314fab1f8521df7b2ec8a9954d92548958a)\n### License Manager Fixes\n\n> Commit: [1ab82314fab1f8521df7b2ec8a9954d92548958a](https://github.com/dOpensource/dsiprouter/commit/1ab82314fab1f8521df7b2ec8a9954d92548958a)  \n> Date: Sun, 4 Aug 2024 12:25:23 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix error message formatting on woocommerce error\n- fix timestamp check for licenses without an end date\n\n\n---\n\n[//]: # (END_SECTION 1ab82314fab1f8521df7b2ec8a9954d92548958a)\n[//]: # (START_SECTION ee088949886ce13d66ddea732d23b51b90ee4ffb)\n### Support Licensing Via the API\n\n> Commit: [ee088949886ce13d66ddea732d23b51b90ee4ffb](https://github.com/dOpensource/dsiprouter/commit/ee088949886ce13d66ddea732d23b51b90ee4ffb)  \n> Date: Fri, 2 Aug 2024 07:41:29 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION ee088949886ce13d66ddea732d23b51b90ee4ffb)\n[//]: # (START_SECTION c3eb4d56b294866513087636aa96f6c3a748982a)\n### Better Validation in chown Subcommand\n\n> Commit: [c3eb4d56b294866513087636aa96f6c3a748982a](https://github.com/dOpensource/dsiprouter/commit/c3eb4d56b294866513087636aa96f6c3a748982a)  \n> Date: Fri, 2 Aug 2024 07:39:43 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add check for configured services in default `chown` CLI command\n\n\n---\n\n[//]: # (END_SECTION c3eb4d56b294866513087636aa96f6c3a748982a)\n[//]: # (START_SECTION f1800b77e276cc28469c6f11bf2cd315271d6d17)\n### CarrierGroup Load Balancing Missing Disable\n\n> Commit: [f1800b77e276cc28469c6f11bf2cd315271d6d17](https://github.com/dOpensource/dsiprouter/commit/f1800b77e276cc28469c6f11bf2cd315271d6d17)  \n> Date: Mon, 22 Jul 2024 12:40:16 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix the DB query that disables load balancing for carrier groups\n\n\n---\n\n[//]: # (END_SECTION f1800b77e276cc28469c6f11bf2cd315271d6d17)\n[//]: # (START_SECTION a183a8c60c6774bb8823b8f73be8a414066ab7fb)\n### Fix Empty Domain Prevents Kamailio Reload\n\n> Commit: [a183a8c60c6774bb8823b8f73be8a414066ab7fb](https://github.com/dOpensource/dsiprouter/commit/a183a8c60c6774bb8823b8f73be8a414066ab7fb)  \n> Date: Mon, 22 Jul 2024 09:35:14 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION a183a8c60c6774bb8823b8f73be8a414066ab7fb)\n[//]: # (START_SECTION 7309bf1dffba660067e70977ec9f636eee86a510)\n### Remove Duplicate Chcek for validateFields()\n\n> Commit: [7309bf1dffba660067e70977ec9f636eee86a510](https://github.com/dOpensource/dsiprouter/commit/7309bf1dffba660067e70977ec9f636eee86a510)  \n> Date: Mon, 22 Jul 2024 08:31:21 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 7309bf1dffba660067e70977ec9f636eee86a510)\n[//]: # (START_SECTION 8a2675034dd8293dcb3b1aa0efdb3641f5c1005b)\n### Dispatcher Fixes\n\n> Commit: [8a2675034dd8293dcb3b1aa0efdb3641f5c1005b](https://github.com/dOpensource/dsiprouter/commit/8a2675034dd8293dcb3b1aa0efdb3641f5c1005b)  \n> Date: Mon, 22 Jul 2024 07:55:14 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix dispatcher relations and related queries\n- update defaults to match new formats of table associations\n\n\n---\n\n[//]: # (END_SECTION 8a2675034dd8293dcb3b1aa0efdb3641f5c1005b)\n[//]: # (START_SECTION 5ec677df29e26d2d78251e8d9582202d50665ac5)\n### Fix Dangling Comma When Adding Carriers\n\n> Commit: [5ec677df29e26d2d78251e8d9582202d50665ac5](https://github.com/dOpensource/dsiprouter/commit/5ec677df29e26d2d78251e8d9582202d50665ac5)  \n> Date: Fri, 19 Jul 2024 16:26:23 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- handle case when `split()` is called on empty `gwlist`\n\n\n---\n\n[//]: # (END_SECTION 5ec677df29e26d2d78251e8d9582202d50665ac5)\n[//]: # (START_SECTION 8759e869034f9410f2622fed0ab40e5dbb107d97)\n### Fix SQL Munging on CSV Field Queries\n\n> Commit: [8759e869034f9410f2622fed0ab40e5dbb107d97](https://github.com/dOpensource/dsiprouter/commit/8759e869034f9410f2622fed0ab40e5dbb107d97)  \n> Date: Fri, 19 Jul 2024 16:04:07 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fixed a class of bugs where the description and tag fields could return the wrong rows\n\n\n---\n\n[//]: # (END_SECTION 8759e869034f9410f2622fed0ab40e5dbb107d97)\n[//]: # (START_SECTION 75b3c327bbb19116894e0703ca417b8b1654cb4a)\n### Fix Carrier Update Via API Out of Sync\n\n> Commit: [75b3c327bbb19116894e0703ca417b8b1654cb4a](https://github.com/dOpensource/dsiprouter/commit/75b3c327bbb19116894e0703ca417b8b1654cb4a)  \n> Date: Fri, 19 Jul 2024 11:56:57 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- updated the API logic for carriers to match the GUI logic\n\n\n---\n\n[//]: # (END_SECTION 75b3c327bbb19116894e0703ca417b8b1654cb4a)\n[//]: # (START_SECTION 426a4c2b0a5bacc8f4727b2f35127d94237cac40)\n### Fix CarrierGroup lb_enabled Not Forwarded\n\n> Commit: [426a4c2b0a5bacc8f4727b2f35127d94237cac40](https://github.com/dOpensource/dsiprouter/commit/426a4c2b0a5bacc8f4727b2f35127d94237cac40)  \n> Date: Fri, 19 Jul 2024 10:11:23 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add `lb_enabled` to data forwarded to the backend\n\n\n---\n\n[//]: # (END_SECTION 426a4c2b0a5bacc8f4727b2f35127d94237cac40)\n[//]: # (START_SECTION 6b3ea5bb772a6e0d75eb91368d9a0f0a1e15c9e6)\n### Fix CarrierGroup Load Balancing Icon\n\n> Commit: [6b3ea5bb772a6e0d75eb91368d9a0f0a1e15c9e6](https://github.com/dOpensource/dsiprouter/commit/6b3ea5bb772a6e0d75eb91368d9a0f0a1e15c9e6)  \n> Date: Fri, 19 Jul 2024 10:10:23 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix load balancing icon not shown on toggle button\n\n\n---\n\n[//]: # (END_SECTION 6b3ea5bb772a6e0d75eb91368d9a0f0a1e15c9e6)\n[//]: # (START_SECTION f13aec39adf65e0d750820e4f7851da645eae6e7)\n### Carrier Group Regression Hotfix\n\n> Commit: [f13aec39adf65e0d750820e4f7851da645eae6e7](https://github.com/dOpensource/dsiprouter/commit/f13aec39adf65e0d750820e4f7851da645eae6e7)  \n> Date: Tue, 16 Jul 2024 13:28:57 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix regression in attributes query for carriers\n\n\n---\n\n[//]: # (END_SECTION f13aec39adf65e0d750820e4f7851da645eae6e7)\n[//]: # (START_SECTION 5a824fe4be28edbef8103b3cd1a8013f91e25492)\n### Domain Routing Translations\n\n> Commit: [5a824fe4be28edbef8103b3cd1a8013f91e25492](https://github.com/dOpensource/dsiprouter/commit/5a824fe4be28edbef8103b3cd1a8013f91e25492)  \n> Date: Thu, 11 Jul 2024 15:52:08 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for signalling / media features on domain pass thru\n\n\n---\n\n[//]: # (END_SECTION 5a824fe4be28edbef8103b3cd1a8013f91e25492)\n[//]: # (START_SECTION 9eb470f8a4dd79a2562395324c7d2e60583315bc)\n### Better Naming for Sending Media Directly to Endpoint\n\n> Commit: [9eb470f8a4dd79a2562395324c7d2e60583315bc](https://github.com/dOpensource/dsiprouter/commit/9eb470f8a4dd79a2562395324c7d2e60583315bc)  \n> Date: Thu, 11 Jul 2024 15:50:41 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- change naming from \"bypass media\" to \"direct media\"\n\n\n---\n\n[//]: # (END_SECTION 9eb470f8a4dd79a2562395324c7d2e60583315bc)\n[//]: # (START_SECTION 543e2f3a85e2256879d22f7651cd924b6fc47a95)\n### Cleanup Artifacts From c529665\n\n> Commit: [543e2f3a85e2256879d22f7651cd924b6fc47a95](https://github.com/dOpensource/dsiprouter/commit/543e2f3a85e2256879d22f7651cd924b6fc47a95)  \n> Date: Fri, 5 Jul 2024 12:21:42 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 543e2f3a85e2256879d22f7651cd924b6fc47a95)\n[//]: # (START_SECTION 4e748d9225c0471535e529d428247ec110caaf0d)\n### Fix DB Name not Set in withKamDB()\n\n> Commit: [4e748d9225c0471535e529d428247ec110caaf0d](https://github.com/dOpensource/dsiprouter/commit/4e748d9225c0471535e529d428247ec110caaf0d)  \n> Date: Wed, 3 Jul 2024 15:26:21 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 4e748d9225c0471535e529d428247ec110caaf0d)\n[//]: # (START_SECTION 05d8a3db546cddf462cbe5ba7072f940acfededd)\n### configuresslcert enhancement - Added the ability to overview the FQDN by using the -o or --override option with the FQDN that you want to use.  Otherwise, dSIP was using reverse lookup to obtain the hostname, which in somecases isn't what the user wanted\n\n> Commit: [05d8a3db546cddf462cbe5ba7072f940acfededd](https://github.com/dOpensource/dsiprouter/commit/05d8a3db546cddf462cbe5ba7072f940acfededd)  \n> Date: Tue, 2 Jul 2024 01:06:49 +0000  \n> Author: Mack (mack@dopensource.com)  \n> Committer: Mack (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 05d8a3db546cddf462cbe5ba7072f940acfededd)\n[//]: # (START_SECTION 7816850f83fad98085c3fec4fe1470c014f6b244)\n### MSTeams UI Fixes - Fixes that prevented a MSTeams Domain from being added\n\n> Commit: [7816850f83fad98085c3fec4fe1470c014f6b244](https://github.com/dOpensource/dsiprouter/commit/7816850f83fad98085c3fec4fe1470c014f6b244)  \n> Date: Sun, 30 Jun 2024 22:08:39 +0000  \n> Author: Mack (mack@dopensource.com)  \n> Committer: Mack (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7816850f83fad98085c3fec4fe1470c014f6b244)\n[//]: # (START_SECTION 39fd67f26e204be7f0389d16f7f51bb51411bdda)\n### Fixed routing to endpoints when a 401 or 407 is received from a PBX using dispatcher\n\n> Commit: [39fd67f26e204be7f0389d16f7f51bb51411bdda](https://github.com/dOpensource/dsiprouter/commit/39fd67f26e204be7f0389d16f7f51bb51411bdda)  \n> Date: Sun, 30 Jun 2024 13:46:09 +0000  \n> Author: Mack (mack@dopensource.com)  \n> Committer: Mack (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 39fd67f26e204be7f0389d16f7f51bb51411bdda)\n[//]: # (START_SECTION a0e020d98ca138243813a25931ce2bfd44f856f7)\n### Fixed routing to endpoints when a 401 or 407 is received from a PBX using dispatcher\n\n> Commit: [a0e020d98ca138243813a25931ce2bfd44f856f7](https://github.com/dOpensource/dsiprouter/commit/a0e020d98ca138243813a25931ce2bfd44f856f7)  \n> Date: Sun, 30 Jun 2024 04:16:11 +0000  \n> Author: Mack (mack@dopensource.com)  \n> Committer: Mack (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a0e020d98ca138243813a25931ce2bfd44f856f7)\n[//]: # (START_SECTION 87a29f49f7e0ef6e456aadfeb58d4c2155665999)\n### Fixed endpoint group failover - The next gateway in the endpoint group will be tried if a failure is detected\n\n> Commit: [87a29f49f7e0ef6e456aadfeb58d4c2155665999](https://github.com/dOpensource/dsiprouter/commit/87a29f49f7e0ef6e456aadfeb58d4c2155665999)  \n> Date: Sat, 29 Jun 2024 19:33:12 +0000  \n> Author: Mack (mack@dopensource.com)  \n> Committer: Mack (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 87a29f49f7e0ef6e456aadfeb58d4c2155665999)\n[//]: # (START_SECTION 53e56fa76503e703d7fa27170ed403927d33d741)\n### Inbound Routing Fixes\n\n> Commit: [53e56fa76503e703d7fa27170ed403927d33d741](https://github.com/dOpensource/dsiprouter/commit/53e56fa76503e703d7fa27170ed403927d33d741)  \n> Date: Fri, 28 Jun 2024 14:15:52 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix inbound failover via drouting -> drouting epg\n- fix inbound failover via dispatcher -> drouting epg\n- fix forwarded DID from hardfwd not reset on failover\n- cleanup the main routing config\n\n\n---\n\n[//]: # (END_SECTION 53e56fa76503e703d7fa27170ed403927d33d741)\n[//]: # (START_SECTION ca380a7b55e3dcb5f1baf08c70dd734b917188d9)\n### Fixes for v0.75 - Fixed CDR's so that API works correctly - Removed TLS reload from Kamailio reload due to delay in reloading\n\n> Commit: [ca380a7b55e3dcb5f1baf08c70dd734b917188d9](https://github.com/dOpensource/dsiprouter/commit/ca380a7b55e3dcb5f1baf08c70dd734b917188d9)  \n> Date: Fri, 28 Jun 2024 18:25:21 +0000  \n> Author: Mack (mack@dopensource.com)  \n> Committer: Mack (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ca380a7b55e3dcb5f1baf08c70dd734b917188d9)\n[//]: # (START_SECTION ca0d727353041c1fd1ac7a1054dcdeb9c927cb11)\n### Load Balancing Bug Fixes\n\n> Commit: [ca0d727353041c1fd1ac7a1054dcdeb9c927cb11](https://github.com/dOpensource/dsiprouter/commit/ca0d727353041c1fd1ac7a1054dcdeb9c927cb11)  \n> Date: Thu, 27 Jun 2024 16:46:13 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix load balanacing failover\n- fix fusionpbx load balancing dispatcher updates\n- fix final dst in load balancing group not disabled when rweight=0\n- add support for enabling/disabling keepalive on endpoints\n- update keepalive to be disabled on enpoints by default\n- update keepalive timeout to 60 seconds\n- fix hanging dispatcher entries in some cases\n- fix edge case where `gwgroup2lb` was not updated correctly\n\n\n---\n\n[//]: # (END_SECTION ca0d727353041c1fd1ac7a1054dcdeb9c927cb11)\n[//]: # (START_SECTION 0bf4420aabbefb4d6d2e9e32a5f4ee641edfe683)\n### Fixed an issue with auto-unregister that prevented gateways from being removed from te dr_gw_list\n\n> Commit: [0bf4420aabbefb4d6d2e9e32a5f4ee641edfe683](https://github.com/dOpensource/dsiprouter/commit/0bf4420aabbefb4d6d2e9e32a5f4ee641edfe683)  \n> Date: Thu, 20 Jun 2024 06:03:03 +0000  \n> Author: Mack (mack@dopensource.com)  \n> Committer: Mack (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0bf4420aabbefb4d6d2e9e32a5f4ee641edfe683)\n[//]: # (START_SECTION 2e00a284992b45dbf5d75c4de87f4704fd78395c)\n### Fix Services on Centos7\n\n> Commit: [2e00a284992b45dbf5d75c4de87f4704fd78395c](https://github.com/dOpensource/dsiprouter/commit/2e00a284992b45dbf5d75c4de87f4704fd78395c)  \n> Date: Tue, 18 Jun 2024 09:58:46 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- revert centos7 to older systemd service file versions\n\n\n---\n\n[//]: # (END_SECTION 2e00a284992b45dbf5d75c4de87f4704fd78395c)\n[//]: # (START_SECTION 3bee7a4c7e837e0e8a70acf18a65964d6edf0600)\n### Fix Sync with CLUSTER_SYNC Enabled\n\n> Commit: [3bee7a4c7e837e0e8a70acf18a65964d6edf0600](https://github.com/dOpensource/dsiprouter/commit/3bee7a4c7e837e0e8a70acf18a65964d6edf0600)  \n> Date: Tue, 18 Jun 2024 08:32:50 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 3bee7a4c7e837e0e8a70acf18a65964d6edf0600)\n[//]: # (START_SECTION b7a5a67c73f390e26d5d6d912a1a48d41636382c)\n### Fix dsiprouter.srevice Version on CetnOS7\n\n> Commit: [b7a5a67c73f390e26d5d6d912a1a48d41636382c](https://github.com/dOpensource/dsiprouter/commit/b7a5a67c73f390e26d5d6d912a1a48d41636382c)  \n> Date: Tue, 18 Jun 2024 07:59:54 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION b7a5a67c73f390e26d5d6d912a1a48d41636382c)\n[//]: # (START_SECTION d77f068ca08ee2c842a62cbcb58d7038bd138534)\n### Add Support for Upgrading from 0.72x to 0.75\n\n> Commit: [d77f068ca08ee2c842a62cbcb58d7038bd138534](https://github.com/dOpensource/dsiprouter/commit/d77f068ca08ee2c842a62cbcb58d7038bd138534)  \n> Date: Mon, 17 Jun 2024 20:27:35 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION d77f068ca08ee2c842a62cbcb58d7038bd138534)\n[//]: # (START_SECTION 83d6af70e487b84ee55c3b9522410fe30db23ad8)\n### Comment Out Cert Generation\n\n> Commit: [83d6af70e487b84ee55c3b9522410fe30db23ad8](https://github.com/dOpensource/dsiprouter/commit/83d6af70e487b84ee55c3b9522410fe30db23ad8)  \n> Date: Wed, 12 Jun 2024 22:34:26 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- commented out the broken generate radio buttons\n\n\n---\n\n[//]: # (END_SECTION 83d6af70e487b84ee55c3b9522410fe30db23ad8)\n[//]: # (START_SECTION 575cceede455b4809e2da145592ca78b5cbf6156)\n### Fix GUI Docs Build\n\n> Commit: [575cceede455b4809e2da145592ca78b5cbf6156](https://github.com/dOpensource/dsiprouter/commit/575cceede455b4809e2da145592ca78b5cbf6156)  \n> Date: Wed, 12 Jun 2024 22:12:06 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 575cceede455b4809e2da145592ca78b5cbf6156)\n[//]: # (START_SECTION fc2aa26bd5fc83eb74ab86f81dc9c630a777f989)\n### Fix Ext-to-Ext Calling\n\n> Commit: [fc2aa26bd5fc83eb74ab86f81dc9c630a777f989](https://github.com/dOpensource/dsiprouter/commit/fc2aa26bd5fc83eb74ab86f81dc9c630a777f989)  \n> Date: Wed, 12 Jun 2024 21:12:17 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix `FLB_SRC_PBX` not set properly\n- fix checks for same flag\n- make digest auth get checked before IP auth\n\n\n---\n\n[//]: # (END_SECTION fc2aa26bd5fc83eb74ab86f81dc9c630a777f989)\n[//]: # (START_SECTION c0a47655b5a2b5739121efbc9a0e03c059a291b4)\n### Carrier Group Load Balancing\n\n> Commit: [c0a47655b5a2b5739121efbc9a0e03c059a291b4](https://github.com/dOpensource/dsiprouter/commit/c0a47655b5a2b5739121efbc9a0e03c059a291b4)  \n> Date: Wed, 12 Jun 2024 19:31:37 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix load balancing selection for carrier groups\n- add support for toggling load balancing on carrier groups\n- fix registered endpoints missing default signal/media params\n\n\n---\n\n[//]: # (END_SECTION c0a47655b5a2b5739121efbc9a0e03c059a291b4)\n[//]: # (START_SECTION 10523849ac49dfa56c99eadab438d1f576666966)\n### Fix Teleblock Not Routing\n\n> Commit: [10523849ac49dfa56c99eadab438d1f576666966](https://github.com/dOpensource/dsiprouter/commit/10523849ac49dfa56c99eadab438d1f576666966)  \n> Date: Wed, 12 Jun 2024 13:53:00 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- make sure teleblock setting is handled from gui -> kamailio\n- fix resetting selected routes after routing to teleblock\n\n\n---\n\n[//]: # (END_SECTION 10523849ac49dfa56c99eadab438d1f576666966)\n[//]: # (START_SECTION 7cf4ac77b3693c06d94f6a3ec3c96e44acbaf713)\n### License Check Edge Case Fix\n\n> Commit: [7cf4ac77b3693c06d94f6a3ec3c96e44acbaf713](https://github.com/dOpensource/dsiprouter/commit/7cf4ac77b3693c06d94f6a3ec3c96e44acbaf713)  \n> Date: Tue, 11 Jun 2024 17:48:44 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for license keys with `_valid_for` or no expiration to load\n\n\n---\n\n[//]: # (END_SECTION 7cf4ac77b3693c06d94f6a3ec3c96e44acbaf713)\n[//]: # (START_SECTION 44ef8c01b6c02603d423425141e0557cf7602a3b)\n### Fix Carrier Auth Encoding\n\n> Commit: [44ef8c01b6c02603d423425141e0557cf7602a3b](https://github.com/dOpensource/dsiprouter/commit/44ef8c01b6c02603d423425141e0557cf7602a3b)  \n> Date: Mon, 10 Jun 2024 22:41:20 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- encode r_username per RFC 2396 in uacreg\n- update network library to encode username portion\n- remove uneeded user portion of auth_proxy by default\n\n\n---\n\n[//]: # (END_SECTION 44ef8c01b6c02603d423425141e0557cf7602a3b)\n[//]: # (START_SECTION c1f16fd78645972af29cede0046df2412d0f1aac)\n### Add Field Validation to Twilio Carrier Plugin\n\n> Commit: [c1f16fd78645972af29cede0046df2412d0f1aac](https://github.com/dOpensource/dsiprouter/commit/c1f16fd78645972af29cede0046df2412d0f1aac)  \n> Date: Mon, 10 Jun 2024 14:39:26 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add field validation for twilio carrier plugin\n\n\n---\n\n[//]: # (END_SECTION c1f16fd78645972af29cede0046df2412d0f1aac)\n[//]: # (START_SECTION 5cc5742704f45c6cffee27f1dda3dad5a4fd2da7)\n### Fix FuxionPBX Endpoint Failure\n\n> Commit: [5cc5742704f45c6cffee27f1dda3dad5a4fd2da7](https://github.com/dOpensource/dsiprouter/commit/5cc5742704f45c6cffee27f1dda3dad5a4fd2da7)  \n> Date: Mon, 10 Jun 2024 11:24:08 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix regression in denpoint creation for fusionpbx endpoint groups\n\n\n---\n\n[//]: # (END_SECTION 5cc5742704f45c6cffee27f1dda3dad5a4fd2da7)\n[//]: # (START_SECTION 27dd5e3a87f3bac5942a454cd13dce5f5f60cfe4)\n### RTPEngine Patches\n\n> Commit: [27dd5e3a87f3bac5942a454cd13dce5f5f60cfe4](https://github.com/dOpensource/dsiprouter/commit/27dd5e3a87f3bac5942a454cd13dce5f5f60cfe4)  \n> Date: Sun, 9 Jun 2024 00:10:11 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- revert version on OS installs that have not been updated to support it\n- fix rtpengine kernel module build on centos\n- fix rtpengine config defaults on debian/centos\n\n\n---\n\n[//]: # (END_SECTION 27dd5e3a87f3bac5942a454cd13dce5f5f60cfe4)\n[//]: # (START_SECTION 32face946d43cbc0df179550f6d612786808230a)\n### Fix RTP Media Control Arguments\n\n> Commit: [32face946d43cbc0df179550f6d612786808230a](https://github.com/dOpensource/dsiprouter/commit/32face946d43cbc0df179550f6d612786808230a)  \n> Date: Sat, 8 Jun 2024 21:52:54 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix control-ng args for UDTPL\n- fix control-ng args for OSRTP\n- make display names for media/signalling easier to understand\n\n\n---\n\n[//]: # (END_SECTION 32face946d43cbc0df179550f6d612786808230a)\n[//]: # (START_SECTION 6b6e1b6dc489867566e398ca50913ef4be733c59)\n### Fix RTPEngine Reload\n\n> Commit: [6b6e1b6dc489867566e398ca50913ef4be733c59](https://github.com/dOpensource/dsiprouter/commit/6b6e1b6dc489867566e398ca50913ef4be733c59)  \n> Date: Sat, 8 Jun 2024 20:03:38 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix bug where rtpengine instances disabled on reload\n\n\n---\n\n[//]: # (END_SECTION 6b6e1b6dc489867566e398ca50913ef4be733c59)\n[//]: # (START_SECTION 8804887bf59e6317ccea7e4e64d95d092825d9fc)\n### Fix RTPEngine Disabled on Boot\n\n> Commit: [8804887bf59e6317ccea7e4e64d95d092825d9fc](https://github.com/dOpensource/dsiprouter/commit/8804887bf59e6317ccea7e4e64d95d092825d9fc)  \n> Date: Fri, 7 Jun 2024 15:55:44 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update `kamailio.service` to be ordered after `rtpengine.service`\n\n\n---\n\n[//]: # (END_SECTION 8804887bf59e6317ccea7e4e64d95d092825d9fc)\n[//]: # (START_SECTION bca5ab613fbb009b4b835e2903290888ba7f3574)\n### Fix Missing BYE on Upstream\n\n> Commit: [bca5ab613fbb009b4b835e2903290888ba7f3574](https://github.com/dOpensource/dsiprouter/commit/bca5ab613fbb009b4b835e2903290888ba7f3574)  \n> Date: Fri, 7 Jun 2024 13:35:22 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- make in-dialog requests to upstream route to source from initial setup\n\n\n---\n\n[//]: # (END_SECTION bca5ab613fbb009b4b835e2903290888ba7f3574)\n[//]: # (START_SECTION 17f6768ba9b4d3f58f20a2239f88b8728c67eaef)\n### Auth Plugin Updates\n\n> Commit: [17f6768ba9b4d3f58f20a2239f88b8728c67eaef](https://github.com/dOpensource/dsiprouter/commit/17f6768ba9b4d3f58f20a2239f88b8728c67eaef)  \n> Date: Thu, 6 Jun 2024 14:26:35 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- make the auth module format easier to extend\n- reset the default to no extra auth modules\n\n\n---\n\n[//]: # (END_SECTION 17f6768ba9b4d3f58f20a2239f88b8728c67eaef)\n[//]: # (START_SECTION 1d13139952f73f9ff9dfca79789c0ad1e36a2577)\n### Update Dependency Requirements for CLI\n\n> Commit: [1d13139952f73f9ff9dfca79789c0ad1e36a2577](https://github.com/dOpensource/dsiprouter/commit/1d13139952f73f9ff9dfca79789c0ad1e36a2577)  \n> Date: Wed, 5 Jun 2024 14:19:49 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- make sure dependencies for CLI are installed\n\n\n---\n\n[//]: # (END_SECTION 1d13139952f73f9ff9dfca79789c0ad1e36a2577)\n[//]: # (START_SECTION 3882f40b0177622d09e4b1267a007c3911cae25d)\n### Fix CentOS SELinux Permissions\n\n> Commit: [3882f40b0177622d09e4b1267a007c3911cae25d](https://github.com/dOpensource/dsiprouter/commit/3882f40b0177622d09e4b1267a007c3911cae25d)  \n> Date: Mon, 3 Jun 2024 17:49:36 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update permissions to allow nginx acces to dsiprouter.sock\n\n\n---\n\n[//]: # (END_SECTION 3882f40b0177622d09e4b1267a007c3911cae25d)\n[//]: # (START_SECTION bcdcf178a97f27ddcb129d830f794c5a3fe0c717)\n### CentOS Installation Updates\n\n> Commit: [bcdcf178a97f27ddcb129d830f794c5a3fe0c717](https://github.com/dOpensource/dsiprouter/commit/bcdcf178a97f27ddcb129d830f794c5a3fe0c717)  \n> Date: Fri, 31 May 2024 14:14:10 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add stable support back to centos7\n- fix various dependency issues\n- bump kamailio/rtpengine versino on centos7\n- fix dnsmasq support\n\n\n---\n\n[//]: # (END_SECTION bcdcf178a97f27ddcb129d830f794c5a3fe0c717)\n[//]: # (START_SECTION 692157482f7971a2ce2c90fe75184b3e51f23734)\n### Amazon Linux 2 Patches\n\n> Commit: [692157482f7971a2ce2c90fe75184b3e51f23734](https://github.com/dOpensource/dsiprouter/commit/692157482f7971a2ce2c90fe75184b3e51f23734)  \n> Date: Fri, 31 May 2024 14:04:11 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- bump rtpengine version\n- downgrade kamailio version\n- add patch for htable coldelim/colnull in kam5.7\n- add support for ldap\n\n\n---\n\n[//]: # (END_SECTION 692157482f7971a2ce2c90fe75184b3e51f23734)\n[//]: # (START_SECTION b1f784d5484b7a53e64ffc94200149500529d3b9)\n### Cluster / Install Fixes\n\n> Commit: [b1f784d5484b7a53e64ffc94200149500529d3b9](https://github.com/dOpensource/dsiprouter/commit/b1f784d5484b7a53e64ffc94200149500529d3b9)  \n> Date: Fri, 31 May 2024 13:57:41 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix `clusterinstall` when running with cluster sync enabled\n- add support for re-running `clusterinstall` after partial completion\n- add support for changing key used in `dsip_lib.sh` encryption funcs\n- fix ordering of module installs\n- updated CDR feature to be conditionally loaded\n\n\n---\n\n[//]: # (END_SECTION b1f784d5484b7a53e64ffc94200149500529d3b9)\n[//]: # (START_SECTION 64640ba1e2bf8ad5a2ffefba4ff9c00682954af8)\n### Updated Privelege Escalation in HA Scripts\n\n> Commit: [64640ba1e2bf8ad5a2ffefba4ff9c00682954af8](https://github.com/dOpensource/dsiprouter/commit/64640ba1e2bf8ad5a2ffefba4ff9c00682954af8)  \n> Date: Wed, 29 May 2024 21:10:04 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for `su` in pacemaker cluster install\n- add support for `sudo` or `su` escalation using password\n\n\n---\n\n[//]: # (END_SECTION 64640ba1e2bf8ad5a2ffefba4ff9c00682954af8)\n[//]: # (START_SECTION c0f2604cbe4bed4a9681348ea7cd2ce4067fbc06)\n### Improve Privilege Escalation on Galera Install\n\n> Commit: [c0f2604cbe4bed4a9681348ea7cd2ce4067fbc06](https://github.com/dOpensource/dsiprouter/commit/c0f2604cbe4bed4a9681348ea7cd2ce4067fbc06)  \n> Date: Tue, 28 May 2024 15:31:58 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for `su` privilege escalation\n- add helper function for selecting program for escalation\n\n\n---\n\n[//]: # (END_SECTION c0f2604cbe4bed4a9681348ea7cd2ce4067fbc06)\n[//]: # (START_SECTION 8630f8cee5b28b0ac9eaebf5751c1df3b17409fb)\n### Make DNSMasq Optional\n\n> Commit: [8630f8cee5b28b0ac9eaebf5751c1df3b17409fb](https://github.com/dOpensource/dsiprouter/commit/8630f8cee5b28b0ac9eaebf5751c1df3b17409fb)  \n> Date: Thu, 23 May 2024 13:22:58 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- move dnsmasq / dsiprouter network stack to an install option\n- add documentation for new `-dns`/`--dnsmasq` options\n- make `clusterinstall` require dnsmasq install\n\n\n---\n\n[//]: # (END_SECTION 8630f8cee5b28b0ac9eaebf5751c1df3b17409fb)\n[//]: # (START_SECTION 6fc903b9086178ddd02322d17b5999719df733bc)\n### Updated dSIPRouter Service files to have Kamailio Service start first\n\n> Commit: [6fc903b9086178ddd02322d17b5999719df733bc](https://github.com/dOpensource/dsiprouter/commit/6fc903b9086178ddd02322d17b5999719df733bc)  \n> Date: Thu, 23 May 2024 15:10:12 +0000  \n> Author: Mack (mack@dopensource.com)  \n> Committer: Mack (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6fc903b9086178ddd02322d17b5999719df733bc)\n[//]: # (START_SECTION 13989cbf1e16ee377db7ab6a4d1138aad614417b)\n### Removed DnsMasq from the install\n\n> Commit: [13989cbf1e16ee377db7ab6a4d1138aad614417b](https://github.com/dOpensource/dsiprouter/commit/13989cbf1e16ee377db7ab6a4d1138aad614417b)  \n> Date: Thu, 23 May 2024 05:17:23 +0000  \n> Author: root (root@075test.dsiprouter.net)  \n> Committer: root (root@075test.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 13989cbf1e16ee377db7ab6a4d1138aad614417b)\n[//]: # (START_SECTION a7f092254269a5a33732667ab0cae56c4c562bcc)\n### Networking Edge Cases\n\n> Commit: [a7f092254269a5a33732667ab0cae56c4c562bcc](https://github.com/dOpensource/dsiprouter/commit/a7f092254269a5a33732667ab0cae56c4c562bcc)  \n> Date: Tue, 21 May 2024 14:06:19 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix edge cases where network stack was failing\n- fix edge cases where apt prompted during installs\n\n\n---\n\n[//]: # (END_SECTION a7f092254269a5a33732667ab0cae56c4c562bcc)\n[//]: # (START_SECTION c041cfd3e048e48df24d927680e38d383da9c126)\n### Bug Fixes\n\n> Commit: [c041cfd3e048e48df24d927680e38d383da9c126](https://github.com/dOpensource/dsiprouter/commit/c041cfd3e048e48df24d927680e38d383da9c126)  \n> Date: Mon, 20 May 2024 18:42:16 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update uninstall files for debian dnsmasq\n- conditionally enable sctp on boot if module loaded\n\n\n---\n\n[//]: # (END_SECTION c041cfd3e048e48df24d927680e38d383da9c126)\n[//]: # (START_SECTION 14e8619eab2e549ea5522cc70f65ffb629d4dfc5)\n### Upgrade Fixes\n\n> Commit: [14e8619eab2e549ea5522cc70f65ffb629d4dfc5](https://github.com/dOpensource/dsiprouter/commit/14e8619eab2e549ea5522cc70f65ffb629d4dfc5)  \n> Date: Mon, 20 May 2024 16:49:53 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fixup upgrade script issues\n- fix dsip_settings migration filter\n- make uac patch error handling more robust\n\n\n---\n\n[//]: # (END_SECTION 14e8619eab2e549ea5522cc70f65ffb629d4dfc5)\n[//]: # (START_SECTION 17bad27719f91cc0ba496a31453be46816224546)\n### Fixup Debian12 Networking\n\n> Commit: [17bad27719f91cc0ba496a31453be46816224546](https://github.com/dOpensource/dsiprouter/commit/17bad27719f91cc0ba496a31453be46816224546)  \n> Date: Mon, 20 May 2024 15:27:49 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- make services skip interfaces managed by other services\n\n\n---\n\n[//]: # (END_SECTION 17bad27719f91cc0ba496a31453be46816224546)\n[//]: # (START_SECTION fbf026c7e76dad7a0f6e17947c9200b73b98c985)\n### Networking Fixes\n\n> Commit: [fbf026c7e76dad7a0f6e17947c9200b73b98c985](https://github.com/dOpensource/dsiprouter/commit/fbf026c7e76dad7a0f6e17947c9200b73b98c985)  \n> Date: Mon, 20 May 2024 12:01:28 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix networking on centos\n- fix docker interface management\n\n\n---\n\n[//]: # (END_SECTION fbf026c7e76dad7a0f6e17947c9200b73b98c985)\n[//]: # (START_SECTION f87aa6653d56da4c29bef3c80aa1b94100ae46ab)\n### Fix Latest Release Default\n\n> Commit: [f87aa6653d56da4c29bef3c80aa1b94100ae46ab](https://github.com/dOpensource/dsiprouter/commit/f87aa6653d56da4c29bef3c80aa1b94100ae46ab)  \n> Date: Mon, 20 May 2024 11:53:25 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix default selection for latest release\n\n\n---\n\n[//]: # (END_SECTION f87aa6653d56da4c29bef3c80aa1b94100ae46ab)\n[//]: # (START_SECTION de0aa6126df9eccf60d9cb1d921d7454f90e4aec)\n### Fix DNSMasq Config Path\n\n> Commit: [de0aa6126df9eccf60d9cb1d921d7454f90e4aec](https://github.com/dOpensource/dsiprouter/commit/de0aa6126df9eccf60d9cb1d921d7454f90e4aec)  \n> Date: Mon, 20 May 2024 08:46:34 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix updated config paths for other OS\n\n\n---\n\n[//]: # (END_SECTION de0aa6126df9eccf60d9cb1d921d7454f90e4aec)\n[//]: # (START_SECTION 9e1a4649f51c94ff9f187a3f7ac31e2a8f616d64)\n### Upgrade Improvements\n\n> Commit: [9e1a4649f51c94ff9f187a3f7ac31e2a8f616d64](https://github.com/dOpensource/dsiprouter/commit/9e1a4649f51c94ff9f187a3f7ac31e2a8f616d64)  \n> Date: Fri, 17 May 2024 17:28:04 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for upgrading from v0.73/v0.74 -> v0.75\n- add support for migrating licenses\n- fix a few typos in the v0.74 upgrade script\n\n\n---\n\n[//]: # (END_SECTION 9e1a4649f51c94ff9f187a3f7ac31e2a8f616d64)\n[//]: # (START_SECTION 7652a2c8b52da1097b5b2bf736f655547e13c175)\n### Gateway Update Bug Fix\n\n> Commit: [7652a2c8b52da1097b5b2bf736f655547e13c175](https://github.com/dOpensource/dsiprouter/commit/7652a2c8b52da1097b5b2bf736f655547e13c175)  \n> Date: Fri, 17 May 2024 17:27:31 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix index error on new `attrs` field format\n\n\n---\n\n[//]: # (END_SECTION 7652a2c8b52da1097b5b2bf736f655547e13c175)\n[//]: # (START_SECTION d2d63c7bfc9a96a447e3d3d6e293be630d117985)\n### Fix Merge Conflicts\n\n> Commit: [d2d63c7bfc9a96a447e3d3d6e293be630d117985](https://github.com/dOpensource/dsiprouter/commit/d2d63c7bfc9a96a447e3d3d6e293be630d117985)  \n> Date: Fri, 17 May 2024 12:26:22 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION d2d63c7bfc9a96a447e3d3d6e293be630d117985)\n[//]: # (START_SECTION 1fe2f3ee3f8bfb2bfc1bed9a2a60e4b0c1b40b83)\n### Added Python LDAP to the requirements file\n\n> Commit: [1fe2f3ee3f8bfb2bfc1bed9a2a60e4b0c1b40b83](https://github.com/dOpensource/dsiprouter/commit/1fe2f3ee3f8bfb2bfc1bed9a2a60e4b0c1b40b83)  \n> Date: Fri, 17 May 2024 01:37:52 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1fe2f3ee3f8bfb2bfc1bed9a2a60e4b0c1b40b83)\n[//]: # (START_SECTION bd478e44de1b6a3a53018b761db19fa2b3de4e9f)\n### Fixed dSIPRouter installer so that libraries that support LDAP works properly\n\n> Commit: [bd478e44de1b6a3a53018b761db19fa2b3de4e9f](https://github.com/dOpensource/dsiprouter/commit/bd478e44de1b6a3a53018b761db19fa2b3de4e9f)  \n> Date: Fri, 17 May 2024 01:20:33 +0000  \n> Author: root (root@075testing.dsiprouter.net)  \n> Committer: root (root@075testing.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bd478e44de1b6a3a53018b761db19fa2b3de4e9f)\n[//]: # (START_SECTION 5eac4d78b45be6c85cba5bcb4b0e18a3434be119)\n### LDAP Support: - Added the ability to authenticate using LDAP - Settings for LDAP group membership has been defined - A user must be in a defined LDAP group in order to access the UI\n\n> Commit: [5eac4d78b45be6c85cba5bcb4b0e18a3434be119](https://github.com/dOpensource/dsiprouter/commit/5eac4d78b45be6c85cba5bcb4b0e18a3434be119)  \n> Date: Wed, 15 May 2024 16:04:21 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5eac4d78b45be6c85cba5bcb4b0e18a3434be119)\n[//]: # (START_SECTION d755ccb6bde29d678adcc0b5373be9e8b65866d1)\n### RHEL SCTP Support\n\n> Commit: [d755ccb6bde29d678adcc0b5373be9e8b65866d1](https://github.com/dOpensource/dsiprouter/commit/d755ccb6bde29d678adcc0b5373be9e8b65866d1)  \n> Date: Tue, 14 May 2024 14:06:17 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- install/enable SCTP on RHEL-based distros\n\n\n---\n\n[//]: # (END_SECTION d755ccb6bde29d678adcc0b5373be9e8b65866d1)\n[//]: # (START_SECTION d906755d15830244286b5c7ae84168777e57b7da)\n### Bug Fixes\n\n> Commit: [d906755d15830244286b5c7ae84168777e57b7da](https://github.com/dOpensource/dsiprouter/commit/d906755d15830244286b5c7ae84168777e57b7da)  \n> Date: Tue, 14 May 2024 12:35:21 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix debian network manager integration\n- improve network service support\n- remove unsupported ws uac transports\n- fix duplicate swap entries on reinstall\n\n\n---\n\n[//]: # (END_SECTION d906755d15830244286b5c7ae84168777e57b7da)\n[//]: # (START_SECTION 65eb074c0d896d2fee33ff5424763187d4219cde)\n### Bump Version to v0.75\n\n> Commit: [65eb074c0d896d2fee33ff5424763187d4219cde](https://github.com/dOpensource/dsiprouter/commit/65eb074c0d896d2fee33ff5424763187d4219cde)  \n> Date: Thu, 9 May 2024 09:43:33 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 65eb074c0d896d2fee33ff5424763187d4219cde)\n[//]: # (START_SECTION 0a1d91c4ba6061fd95f1218556e5c8bd6bee8b2d)\n### Fix RTPEngine Stale Hash Entries\n\n> Commit: [0a1d91c4ba6061fd95f1218556e5c8bd6bee8b2d](https://github.com/dOpensource/dsiprouter/commit/0a1d91c4ba6061fd95f1218556e5c8bd6bee8b2d)  \n> Date: Mon, 6 May 2024 12:46:25 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- make hash entries expire on same timeout as rtpengine application\n\n\n---\n\n[//]: # (END_SECTION 0a1d91c4ba6061fd95f1218556e5c8bd6bee8b2d)\n[//]: # (START_SECTION d4e003b62a7e28af4047926dc5c01019e7564167)\n### Signalling & Media Translation Support\n\n> Commit: [d4e003b62a7e28af4047926dc5c01019e7564167](https://github.com/dOpensource/dsiprouter/commit/d4e003b62a7e28af4047926dc5c01019e7564167)  \n> Date: Sun, 5 May 2024 18:31:37 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #555\n- add support for SCTP as an UAS\n- add support for SCTP as an UAC\n- add support for translating media per endpoint\n- add support for translating signalling per endpoint\n- add tooltips to endpoint configuration options\n- switch to rewight and relative load balancing as the default\n- disable WITH_PUSH by default (causing issues with routing)\n- make record routing more reliable (stateful selection instead)\n- fix call limiting pv conversions incorrect in some cases\n- fix typo in dialog timeout\n- allow media bypass as an option for endpoints\n- make modal popups relative to viewport size\n\n\n---\n\n[//]: # (END_SECTION d4e003b62a7e28af4047926dc5c01019e7564167)\n[//]: # (START_SECTION 70a9734b9d723d8384bf483c509c5d911f6a1688)\n### Fixup Styling on Reload Buttons\n\n> Commit: [70a9734b9d723d8384bf483c509c5d911f6a1688](https://github.com/dOpensource/dsiprouter/commit/70a9734b9d723d8384bf483c509c5d911f6a1688)  \n> Date: Wed, 1 May 2024 13:33:36 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- make clicks on \"reload\" open the dropdown\n- make styling more consistent with other dropdowns\n\n\n---\n\n[//]: # (END_SECTION 70a9734b9d723d8384bf483c509c5d911f6a1688)\n[//]: # (START_SECTION 25ebab6b83380c069e94c322049135363073bd05)\n### Fix Dialog Call Settings\n\n> Commit: [25ebab6b83380c069e94c322049135363073bd05](https://github.com/dOpensource/dsiprouter/commit/25ebab6b83380c069e94c322049135363073bd05)  \n> Date: Wed, 1 May 2024 11:44:02 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- bump kamailio version to 5.8.x\n- fix null checks when setting dialog call settings\n\n\n---\n\n[//]: # (END_SECTION 25ebab6b83380c069e94c322049135363073bd05)\n[//]: # (START_SECTION d6af461097c8d15b052bcd478c618b6ff5e10c6a)\n### Fix Improper Htable Config\n\n> Commit: [d6af461097c8d15b052bcd478c618b6ff5e10c6a](https://github.com/dOpensource/dsiprouter/commit/d6af461097c8d15b052bcd478c618b6ff5e10c6a)  \n> Date: Wed, 1 May 2024 11:17:45 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- rename route2lb lookup to gwgroup2lb\n\n\n---\n\n[//]: # (END_SECTION d6af461097c8d15b052bcd478c618b6ff5e10c6a)\n[//]: # (START_SECTION 61917b2d0fa7da63d671d9ca81d0c23953af5427)\n### Fix Cron and JsonRPC Bugs\n\n> Commit: [61917b2d0fa7da63d671d9ca81d0c23953af5427](https://github.com/dOpensource/dsiprouter/commit/61917b2d0fa7da63d671d9ca81d0c23953af5427)  \n> Date: Fri, 26 Apr 2024 12:00:39 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix bug in kamailio reload where some tuples were in wrong format\n- refactor above tuples to lists for easier debugging\n- fix license checks fail after cron job runs (wrong permissions)\n- refactor cron jobs to use least privilege needed\n- refactor fusionpbx certs into the standard configuration dir\n- refactor fusion sync to use standard RPC reloads instead of kamcmd\n\n\n---\n\n[//]: # (END_SECTION 61917b2d0fa7da63d671d9ca81d0c23953af5427)\n[//]: # (START_SECTION 400837f00c4758b2344aec005c93f44d7819d3d6)\n### Improve Error Messages for HTTP Errors\n\n> Commit: [400837f00c4758b2344aec005c93f44d7819d3d6](https://github.com/dOpensource/dsiprouter/commit/400837f00c4758b2344aec005c93f44d7819d3d6)  \n> Date: Wed, 24 Apr 2024 14:49:13 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #276\n\n\n---\n\n[//]: # (END_SECTION 400837f00c4758b2344aec005c93f44d7819d3d6)\n[//]: # (START_SECTION 1108bb9f9266eacb3b49a9ac3d733c76731d68af)\n### Stability Improvements\n\n> Commit: [1108bb9f9266eacb3b49a9ac3d733c76731d68af](https://github.com/dOpensource/dsiprouter/commit/1108bb9f9266eacb3b49a9ac3d733c76731d68af)  \n> Date: Wed, 24 Apr 2024 13:46:58 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update RPC reload deltas in kamailio\n- add support for reload_delta in uac module\n- fix license machine validation false positives\n- fix license state not preloaded into template context\n- fix shared memory not cleaned up on startup failure\n\n\n---\n\n[//]: # (END_SECTION 1108bb9f9266eacb3b49a9ac3d733c76731d68af)\n[//]: # (START_SECTION 8df5e8f8dcaacb911e03448982ec10f470dbd18c)\n### Fix Subcriber Update API\n\n> Commit: [8df5e8f8dcaacb911e03448982ec10f470dbd18c](https://github.com/dOpensource/dsiprouter/commit/8df5e8f8dcaacb911e03448982ec10f470dbd18c)  \n> Date: Mon, 22 Apr 2024 15:37:18 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #566\n\n\n---\n\n[//]: # (END_SECTION 8df5e8f8dcaacb911e03448982ec10f470dbd18c)\n[//]: # (START_SECTION fbc8e958f4c431dc26b91f95196f77013a74f4b3)\n### v0.75 Bug Fixes\n\n> Commit: [fbc8e958f4c431dc26b91f95196f77013a74f4b3](https://github.com/dOpensource/dsiprouter/commit/fbc8e958f4c431dc26b91f95196f77013a74f4b3)  \n> Date: Mon, 22 Apr 2024 15:11:46 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix uninstall for dnsmasq on debian not updated\n- fix password not set in kamailio.cfg on install\n\n\n---\n\n[//]: # (END_SECTION fbc8e958f4c431dc26b91f95196f77013a74f4b3)\n[//]: # (START_SECTION 80f143450a09cd7eae6e081d62a96d7eca14b3b3)\n### fix NetworkManager integration on debian\n\n> Commit: [80f143450a09cd7eae6e081d62a96d7eca14b3b3](https://github.com/dOpensource/dsiprouter/commit/80f143450a09cd7eae6e081d62a96d7eca14b3b3)  \n> Date: Mon, 22 Apr 2024 13:28:47 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 80f143450a09cd7eae6e081d62a96d7eca14b3b3)\n[//]: # (START_SECTION da8e89e81c280e09035b82e7dfa349522c7b8e85)\n### Update Transnexus GUI Settings\n\n> Commit: [da8e89e81c280e09035b82e7dfa349522c7b8e85](https://github.com/dOpensource/dsiprouter/commit/da8e89e81c280e09035b82e7dfa349522c7b8e85)  \n> Date: Thu, 4 Apr 2024 15:37:23 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for configuring verify service in the GUI\n\n\n---\n\n[//]: # (END_SECTION da8e89e81c280e09035b82e7dfa349522c7b8e85)\n[//]: # (START_SECTION c66b16684b0206e1bd71907882bb64cfa95195b2)\n### v0.75 Bug Fixes\n\n> Commit: [c66b16684b0206e1bd71907882bb64cfa95195b2](https://github.com/dOpensource/dsiprouter/commit/c66b16684b0206e1bd71907882bb64cfa95195b2)  \n> Date: Wed, 3 Apr 2024 15:52:39 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix FQDNs with IP in name not parsed by routing config\n- fix htable syntax errors in routing config\n- fix firewalld / ufw package conflict on debian\n- fix systemd-resolved not configured correctly on vultr\n\n\n---\n\n[//]: # (END_SECTION c66b16684b0206e1bd71907882bb64cfa95195b2)\n[//]: # (START_SECTION a53cd57604f817365dbbccde3741dbd83e87e722)\n### Merge pull request #561 from dOpensource/bugfix/560\n\n> Commit: [a53cd57604f817365dbbccde3741dbd83e87e722](https://github.com/dOpensource/dsiprouter/commit/a53cd57604f817365dbbccde3741dbd83e87e722)  \n> Date: Tue, 27 Feb 2024 14:36:07 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Fixes 560 - Enables the OpenSSL Default Provider\n\n\n---\n\n[//]: # (END_SECTION a53cd57604f817365dbbccde3741dbd83e87e722)\n[//]: # (START_SECTION 78920dded1b5cd7afe0c8dd916b780b18e284374)\n### Add Max Time Limit For Endpoint Groups\n\n> Commit: [78920dded1b5cd7afe0c8dd916b780b18e284374](https://github.com/dOpensource/dsiprouter/commit/78920dded1b5cd7afe0c8dd916b780b18e284374)  \n> Date: Fri, 29 Mar 2024 17:17:28 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #177\n- add support for limiting call durations on endpoint groups\n- reformat call limit into call_settings schema\n\n\n---\n\n[//]: # (END_SECTION 78920dded1b5cd7afe0c8dd916b780b18e284374)\n[//]: # (START_SECTION bb168ab0f564235717dece05cd43a625e5da85b0)\n### Added configuration settings for the LDAP auth plugin\n\n> Commit: [bb168ab0f564235717dece05cd43a625e5da85b0](https://github.com/dOpensource/dsiprouter/commit/bb168ab0f564235717dece05cd43a625e5da85b0)  \n> Date: Mon, 22 Apr 2024 18:03:34 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bb168ab0f564235717dece05cd43a625e5da85b0)\n[//]: # (START_SECTION ac530a50b2f7c2e3e4c7135dbaef019ca00645bd)\n### Initial commit of adding LDAP Support for dSIPRouter authentication\n\n> Commit: [ac530a50b2f7c2e3e4c7135dbaef019ca00645bd](https://github.com/dOpensource/dsiprouter/commit/ac530a50b2f7c2e3e4c7135dbaef019ca00645bd)  \n> Date: Mon, 22 Apr 2024 04:28:03 +0000  \n> Author: root (root@ldapsupport.dsiprouter.net)  \n> Committer: root (root@ldapsupport.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ac530a50b2f7c2e3e4c7135dbaef019ca00645bd)\n[//]: # (START_SECTION 09d219da3dfa0c44ab928953c3a88bb92a4fc19c)\n### Fixes #564 - Added logic to convert a LCR outbound route to a simple outbound route once the from prefix is removed\n\n> Commit: [09d219da3dfa0c44ab928953c3a88bb92a4fc19c](https://github.com/dOpensource/dsiprouter/commit/09d219da3dfa0c44ab928953c3a88bb92a4fc19c)  \n> Date: Mon, 15 Apr 2024 22:39:46 +0000  \n> Author: root (root@demo-dsip-master0)  \n> Committer: root (root@demo-dsip-master0)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 09d219da3dfa0c44ab928953c3a88bb92a4fc19c)\n[//]: # (START_SECTION 5515922c7c178bdabb4d159ed167f21c63615b5c)\n### Update Transnexus GUI Settings\n\n> Commit: [5515922c7c178bdabb4d159ed167f21c63615b5c](https://github.com/dOpensource/dsiprouter/commit/5515922c7c178bdabb4d159ed167f21c63615b5c)  \n> Date: Thu, 4 Apr 2024 15:37:23 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for configuring verify service in the GUI\n\n\n---\n\n[//]: # (END_SECTION 5515922c7c178bdabb4d159ed167f21c63615b5c)\n[//]: # (START_SECTION 60cbe341aaec7bc0447dc667a569bc205a1f06aa)\n### v0.75 Bug Fixes\n\n> Commit: [60cbe341aaec7bc0447dc667a569bc205a1f06aa](https://github.com/dOpensource/dsiprouter/commit/60cbe341aaec7bc0447dc667a569bc205a1f06aa)  \n> Date: Wed, 3 Apr 2024 15:52:39 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix FQDNs with IP in name not parsed by routing config\n- fix htable syntax errors in routing config\n- fix firewalld / ufw package conflict on debian\n- fix systemd-resolved not configured correctly on vultr\n\n\n---\n\n[//]: # (END_SECTION 60cbe341aaec7bc0447dc667a569bc205a1f06aa)\n[//]: # (START_SECTION aa2789a6cb688d9e9bfa30f4972a883dbd92145d)\n### Moved the removal of ufw up in the script before Kamailio dependencies\n\n> Commit: [aa2789a6cb688d9e9bfa30f4972a883dbd92145d](https://github.com/dOpensource/dsiprouter/commit/aa2789a6cb688d9e9bfa30f4972a883dbd92145d)  \n> Date: Wed, 3 Apr 2024 12:53:40 +0000  \n> Author: root (root@test1.dsiprouter.net)  \n> Committer: root (root@test1.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION aa2789a6cb688d9e9bfa30f4972a883dbd92145d)\n[//]: # (START_SECTION 529685e23ec84d6da5fa8b945b87b17386cadfbb)\n### Moved the removal of ufw up in the script\n\n> Commit: [529685e23ec84d6da5fa8b945b87b17386cadfbb](https://github.com/dOpensource/dsiprouter/commit/529685e23ec84d6da5fa8b945b87b17386cadfbb)  \n> Date: Wed, 3 Apr 2024 12:35:17 +0000  \n> Author: root (root@test1.dsiprouter.net)  \n> Committer: root (root@test1.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 529685e23ec84d6da5fa8b945b87b17386cadfbb)\n[//]: # (START_SECTION 0e272fbbd64c5ab1855e8d9ffa500205da93cf99)\n### Fixes for dnsmasq and Vultr - Added logic to handle both resolvconf and systemd-resolved DNS stacks - dnsmasq has a specific configuration file for Debian 12 - Added logic to set the EXTERNAL_FQDN to the INTERNAL_FQDN if the vultrusercontent.com domain is detected, which uses the ip address in the hostname portion of the FQDN.  Kamailio doesn't like FQDN's in this format\n\n> Commit: [0e272fbbd64c5ab1855e8d9ffa500205da93cf99](https://github.com/dOpensource/dsiprouter/commit/0e272fbbd64c5ab1855e8d9ffa500205da93cf99)  \n> Date: Wed, 3 Apr 2024 04:39:00 +0000  \n> Author: root (root@test1.dsiprouter.net)  \n> Committer: root (root@test1.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0e272fbbd64c5ab1855e8d9ffa500205da93cf99)\n[//]: # (START_SECTION 64b1947c7da99eecfb02530cf75c211ddc74c2b0)\n### Merge pull request #561 from dOpensource/bugfix/560\n\n> Commit: [64b1947c7da99eecfb02530cf75c211ddc74c2b0](https://github.com/dOpensource/dsiprouter/commit/64b1947c7da99eecfb02530cf75c211ddc74c2b0)  \n> Date: Tue, 27 Feb 2024 14:36:07 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Fixes 560 - Enables the OpenSSL Default Provider\n\n\n---\n\n[//]: # (END_SECTION 64b1947c7da99eecfb02530cf75c211ddc74c2b0)\n[//]: # (START_SECTION be105c9f5058d5b57470e15136dde89363353156)\n### Add Max Time Limit For Endpoint Groups\n\n> Commit: [be105c9f5058d5b57470e15136dde89363353156](https://github.com/dOpensource/dsiprouter/commit/be105c9f5058d5b57470e15136dde89363353156)  \n> Date: Fri, 29 Mar 2024 17:17:28 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #177\n- add support for limiting call durations on endpoint groups\n- reformat call limit into call_settings schema\n\n\n---\n\n[//]: # (END_SECTION be105c9f5058d5b57470e15136dde89363353156)\n[//]: # (START_SECTION c1d7e509bedfbf6a8e4814ce021bbccf58a5b705)\n### Fix dSipRouter Startup Limits\n\n> Commit: [c1d7e509bedfbf6a8e4814ce021bbccf58a5b705](https://github.com/dOpensource/dsiprouter/commit/c1d7e509bedfbf6a8e4814ce021bbccf58a5b705)  \n> Date: Thu, 28 Mar 2024 12:05:45 -0600  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- limit startup failures to 3 attempts when launched via systemd\n\n\n---\n\n[//]: # (END_SECTION c1d7e509bedfbf6a8e4814ce021bbccf58a5b705)\n[//]: # (START_SECTION 4dc7317a42efd4e33f341cd1992ea2249841116e)\n### Fix Kamailio Reloading Errors\n\n> Commit: [4dc7317a42efd4e33f341cd1992ea2249841116e](https://github.com/dOpensource/dsiprouter/commit/4dc7317a42efd4e33f341cd1992ea2249841116e)  \n> Date: Thu, 21 Mar 2024 18:07:30 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix jsonrpc reload failing when features enabled/disabled\n- fix kamailio not starting when WITH_TLS disabled\n\n\n---\n\n[//]: # (END_SECTION 4dc7317a42efd4e33f341cd1992ea2249841116e)\n[//]: # (START_SECTION addbc3dea61239991370950c3f106d1a73a3ff55)\n### Update TLS Cert Renewal Checks\n\n> Commit: [addbc3dea61239991370950c3f106d1a73a3ff55](https://github.com/dOpensource/dsiprouter/commit/addbc3dea61239991370950c3f106d1a73a3ff55)  \n> Date: Fri, 15 Mar 2024 10:44:09 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for renewing self signed cert\n- update wildcard check to instead check for LE issued cert\n- improve error handling during renewal failures\n\n\n---\n\n[//]: # (END_SECTION addbc3dea61239991370950c3f106d1a73a3ff55)\n[//]: # (START_SECTION 618f43763704f8e5782258a50fd974ebec02cad1)\n### Add AWS Pacemaker Support\n\n> Commit: [618f43763704f8e5782258a50fd974ebec02cad1](https://github.com/dOpensource/dsiprouter/commit/618f43763704f8e5782258a50fd974ebec02cad1)  \n> Date: Thu, 14 Mar 2024 14:18:52 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for AWS elastic IPs in pacemaker\n- fix cloud-init hosts template not applying\n\n\n---\n\n[//]: # (END_SECTION 618f43763704f8e5782258a50fd974ebec02cad1)\n[//]: # (START_SECTION 1e603d96e885d05dd7c6a8008f34f7cf051e0756)\n### Central DB Config Updates\n\n> Commit: [1e603d96e885d05dd7c6a8008f34f7cf051e0756](https://github.com/dOpensource/dsiprouter/commit/1e603d96e885d05dd7c6a8008f34f7cf051e0756)  \n> Date: Thu, 29 Feb 2024 18:38:09 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix credentials not being set properly when `-db` given\n- fix DB data not synced properly for fresh install with `-db`\n\n\n---\n\n[//]: # (END_SECTION 1e603d96e885d05dd7c6a8008f34f7cf051e0756)\n[//]: # (START_SECTION 13b4377ee5658eede8dca625e1a07705a3d6ecfc)\n### Add Support For License Tagging\n\n> Commit: [13b4377ee5658eede8dca625e1a07705a3d6ecfc](https://github.com/dOpensource/dsiprouter/commit/13b4377ee5658eede8dca625e1a07705a3d6ecfc)  \n> Date: Thu, 29 Feb 2024 18:36:17 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- adds support for tagged licenses and license combos\n\n\n---\n\n[//]: # (END_SECTION 13b4377ee5658eede8dca625e1a07705a3d6ecfc)\n[//]: # (START_SECTION a89a3bff193bb6c0b69807082fe475fae043fafe)\n### Fixes 560 - Enables the OpenSSL Default Provider\n\n> Commit: [a89a3bff193bb6c0b69807082fe475fae043fafe](https://github.com/dOpensource/dsiprouter/commit/a89a3bff193bb6c0b69807082fe475fae043fafe)  \n> Date: Tue, 27 Feb 2024 18:25:17 +0000  \n> Author: root (root@dsip-debian12-074)  \n> Committer: root (root@dsip-debian12-074)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a89a3bff193bb6c0b69807082fe475fae043fafe)\n[//]: # (START_SECTION 8bd961101b57c5fa7254c6be4f81dc49977a3555)\n### Fix Debian Net ISO Install\n\n> Commit: [8bd961101b57c5fa7254c6be4f81dc49977a3555](https://github.com/dOpensource/dsiprouter/commit/8bd961101b57c5fa7254c6be4f81dc49977a3555)  \n> Date: Mon, 26 Feb 2024 08:44:30 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix dnsmasq installation for debian net ISO installs\n\n\n---\n\n[//]: # (END_SECTION 8bd961101b57c5fa7254c6be4f81dc49977a3555)\n[//]: # (START_SECTION 03a79b1b952fc523b208c381315d58a001fcc69f)\n### Hotfix For Debian10/11 dnsmasq Configuration\n\n> Commit: [03a79b1b952fc523b208c381315d58a001fcc69f](https://github.com/dOpensource/dsiprouter/commit/03a79b1b952fc523b208c381315d58a001fcc69f)  \n> Date: Wed, 14 Feb 2024 10:28:44 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for resolvconf integration on debian versions < 12\n\n\n---\n\n[//]: # (END_SECTION 03a79b1b952fc523b208c381315d58a001fcc69f)\n[//]: # (START_SECTION b7e435108650eb13d77f95ced6e01fd4d9a3c871)\n### merge changes in master into v0.75 (#558)\n\n> Commit: [b7e435108650eb13d77f95ced6e01fd4d9a3c871](https://github.com/dOpensource/dsiprouter/commit/b7e435108650eb13d77f95ced6e01fd4d9a3c871)  \n> Date: Thu, 1 Feb 2024 09:50:11 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- * Fixed regression that prevented Python based crontab processes from executing\r\n\n- * Fix Carrier Prefix Regression\r\n\n- Resolves #548\r\n\n- * Added an additional sed statement to handle the ignoreip statement being commented out\r\n\n- * Added an additional sed statement to the add screen to handle the ignoreip statement being commented out\r\n\n- * Make FusionPBX Regex More Reliable\r\n\n- * Fix FusionPBX Toggle Button\r\n\n- ---------\r\n\n- Co-authored-by: Mack Hendricks <mack@dopensource.com>\r\n- Co-authored-by: root <root@ip-172-31-17-44>\n\n\n---\n\n[//]: # (END_SECTION b7e435108650eb13d77f95ced6e01fd4d9a3c871)\n[//]: # (START_SECTION a593cfa496a38bc552e047eba9b61174ea2647e1)\n### Fix FusionPBX Toggle Button\n\n> Commit: [a593cfa496a38bc552e047eba9b61174ea2647e1](https://github.com/dOpensource/dsiprouter/commit/a593cfa496a38bc552e047eba9b61174ea2647e1)  \n> Date: Wed, 31 Jan 2024 17:43:54 +0000  \n> Author: root (root@ip-172-31-17-44)  \n> Committer: root (root@ip-172-31-17-44)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a593cfa496a38bc552e047eba9b61174ea2647e1)\n[//]: # (START_SECTION 0b6e53cda06ab5496ab7e9197c28cc9311d34b87)\n### Make FusionPBX Regex More Reliable\n\n> Commit: [0b6e53cda06ab5496ab7e9197c28cc9311d34b87](https://github.com/dOpensource/dsiprouter/commit/0b6e53cda06ab5496ab7e9197c28cc9311d34b87)  \n> Date: Wed, 31 Jan 2024 16:00:44 +0000  \n> Author: root (root@ip-172-31-17-44)  \n> Committer: root (root@ip-172-31-17-44)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0b6e53cda06ab5496ab7e9197c28cc9311d34b87)\n[//]: # (START_SECTION 86f0006ee519a0e7a3a5863a7c781b824bfb9b19)\n### Added an additional sed statement to the add screen to handle the ignoreip statement being commented out\n\n> Commit: [86f0006ee519a0e7a3a5863a7c781b824bfb9b19](https://github.com/dOpensource/dsiprouter/commit/86f0006ee519a0e7a3a5863a7c781b824bfb9b19)  \n> Date: Wed, 31 Jan 2024 15:43:23 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 86f0006ee519a0e7a3a5863a7c781b824bfb9b19)\n[//]: # (START_SECTION 8d4b1cc918e6efe4543fe693002c4f31a2d47fb6)\n### Added an additional sed statement to handle the ignoreip statement being commented out\n\n> Commit: [8d4b1cc918e6efe4543fe693002c4f31a2d47fb6](https://github.com/dOpensource/dsiprouter/commit/8d4b1cc918e6efe4543fe693002c4f31a2d47fb6)  \n> Date: Wed, 31 Jan 2024 15:09:16 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8d4b1cc918e6efe4543fe693002c4f31a2d47fb6)\n[//]: # (START_SECTION c529665900eccccc6cb0eeaa76f12e13a95e7d7a)\n### Fix Carrier Prefix Regression\n\n> Commit: [c529665900eccccc6cb0eeaa76f12e13a95e7d7a](https://github.com/dOpensource/dsiprouter/commit/c529665900eccccc6cb0eeaa76f12e13a95e7d7a)  \n> Date: Thu, 25 Jan 2024 09:16:15 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- Resolves #548\n\n\n---\n\n[//]: # (END_SECTION c529665900eccccc6cb0eeaa76f12e13a95e7d7a)\n[//]: # (START_SECTION 0dfe64bd87d43fb425da70b86be312e0f59fa332)\n### Fixed regression that prevented Python based crontab processes from executing\n\n> Commit: [0dfe64bd87d43fb425da70b86be312e0f59fa332](https://github.com/dOpensource/dsiprouter/commit/0dfe64bd87d43fb425da70b86be312e0f59fa332)  \n> Date: Fri, 26 Jan 2024 13:40:47 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0dfe64bd87d43fb425da70b86be312e0f59fa332)\n[//]: # (START_SECTION 532f85a25b4611e2b3bc98a1a2358891ed04efcb)\n### Fixed regression that prevented Python based crontab processes from executing\n\n> Commit: [532f85a25b4611e2b3bc98a1a2358891ed04efcb](https://github.com/dOpensource/dsiprouter/commit/532f85a25b4611e2b3bc98a1a2358891ed04efcb)  \n> Date: Fri, 26 Jan 2024 13:40:47 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 532f85a25b4611e2b3bc98a1a2358891ed04efcb)\n[//]: # (START_SECTION 1c255d5935685ba5d4ec28e4be3b67da7f993183)\n### Fix Carrier Prefix Regression\n\n> Commit: [1c255d5935685ba5d4ec28e4be3b67da7f993183](https://github.com/dOpensource/dsiprouter/commit/1c255d5935685ba5d4ec28e4be3b67da7f993183)  \n> Date: Thu, 25 Jan 2024 09:16:15 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #548\n\n\n---\n\n[//]: # (END_SECTION 1c255d5935685ba5d4ec28e4be3b67da7f993183)\n[//]: # (START_SECTION 66001fadffe3bb7f231a2f2c9968f190293418f5)\n### Hotfix Load Balancing\n\n> Commit: [66001fadffe3bb7f231a2f2c9968f190293418f5](https://github.com/dOpensource/dsiprouter/commit/66001fadffe3bb7f231a2f2c9968f190293418f5)  \n> Date: Wed, 24 Jan 2024 09:49:20 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix load balancing to work in current drouting limitations\n\n\n---\n\n[//]: # (END_SECTION 66001fadffe3bb7f231a2f2c9968f190293418f5)\n[//]: # (START_SECTION 39180322dde955544c3ab31b8e0e9689c366ef78)\n### Hotfix Gateway Digest Auth\n\n> Commit: [39180322dde955544c3ab31b8e0e9689c366ef78](https://github.com/dOpensource/dsiprouter/commit/39180322dde955544c3ab31b8e0e9689c366ef78)  \n> Date: Wed, 24 Jan 2024 09:48:00 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix gateways not updated in epg in clustered environment\n\n\n---\n\n[//]: # (END_SECTION 39180322dde955544c3ab31b8e0e9689c366ef78)\n[//]: # (START_SECTION 58c7da52f023c6b5f4e6392c602158d1fe8a073c)\n### Add Support For v0.74 Upgrade\n\n> Commit: [58c7da52f023c6b5f4e6392c602158d1fe8a073c](https://github.com/dOpensource/dsiprouter/commit/58c7da52f023c6b5f4e6392c602158d1fe8a073c)  \n> Date: Fri, 22 Dec 2023 13:47:55 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- - add license check to CLI upgrade command\n- - add supoprt for v0.73 -> v0.74 upgrade\n- - fix upgrade git url not defaulting to settings.py\n\n\n---\n\n[//]: # (END_SECTION 58c7da52f023c6b5f4e6392c602158d1fe8a073c)\n[//]: # (START_SECTION 8eebafba384a4604c3a754a2b9c708eae462b3ea)\n### DNSmasq and SElinux Fixes\n\n> Commit: [8eebafba384a4604c3a754a2b9c708eae462b3ea](https://github.com/dOpensource/dsiprouter/commit/8eebafba384a4604c3a754a2b9c708eae462b3ea)  \n> Date: Thu, 21 Dec 2023 10:05:49 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- - fix DNSmasq integration with systemdresolved on debian\n- - fix DNSmasq integration with dhclient on amazon linux\n- - fix DNSmasq integration with NetworkManager on centos\n- - fix SElinux support on centos\n- - fix default route resolution when multiple default routes available\n- - fix typo in v0.73 upgrade script\n- - fix centos missing hosts template for cloud-init\n\n\n---\n\n[//]: # (END_SECTION 8eebafba384a4604c3a754a2b9c708eae462b3ea)\n[//]: # (START_SECTION e856b11068c25bd8a5bfe8ec53629d94aba1c5c7)\n### Fixed permissions to allow Nginx to access the the dSIPRouter UI via a UNIX socket\n\n> Commit: [e856b11068c25bd8a5bfe8ec53629d94aba1c5c7](https://github.com/dOpensource/dsiprouter/commit/e856b11068c25bd8a5bfe8ec53629d94aba1c5c7)  \n> Date: Mon, 18 Dec 2023 04:09:31 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e856b11068c25bd8a5bfe8ec53629d94aba1c5c7)\n[//]: # (START_SECTION c6c4bb239aa4870dd8e24d24e68950ec76ece921)\n### OS And Cloud Bug Fixes\n\n> Commit: [c6c4bb239aa4870dd8e24d24e68950ec76ece921](https://github.com/dOpensource/dsiprouter/commit/c6c4bb239aa4870dd8e24d24e68950ec76ece921)  \n> Date: Fri, 8 Dec 2023 14:24:42 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- - fix amzn2 issue with kernel headers when installing rtpengine\n- - fix debian10 dependency issue when installing kamailio\n- - fix missing cron dependency on AWS debian images\n- - fix amazn2 libwebsockets compilation failing (bump openssl version)\n- - fix AWS metadata API changed\n- - fix testing regressions\n- - update references to `dsiprouter.sh` to use the binary path instead\n- - fix update* commands may return bad exit status to systemd\n- - fix run permissions for services to work with SELinux enabled\n- - fix nginx systemd service not loading on older versions\n- - fix selinux DMQ port definition incorrect\n- - fix license activation regression\n- - update debian/ubuntu to swap out dns stack for dnsmasq/resolvconf\n- - refactor dnsmasq install to use separate scripts for each OS\n\n\n---\n\n[//]: # (END_SECTION c6c4bb239aa4870dd8e24d24e68950ec76ece921)\n[//]: # (START_SECTION 3df4ea7af6b656a233208f36b3b7558e07d7094e)\n### Fixed bugs: - The dispatcher options were not being used - Gateways without a weight could not be deleted\n\n> Commit: [3df4ea7af6b656a233208f36b3b7558e07d7094e](https://github.com/dOpensource/dsiprouter/commit/3df4ea7af6b656a233208f36b3b7558e07d7094e)  \n> Date: Fri, 17 Nov 2023 02:30:50 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3df4ea7af6b656a233208f36b3b7558e07d7094e)\n[//]: # (START_SECTION b803e61f8f862aaee163bfdeecd587de319761a5)\n### Added support for weighted load balancing for carrier groups.  It will be used if at least one weight is specified for a carrier within a carrier group.\n\n> Commit: [b803e61f8f862aaee163bfdeecd587de319761a5](https://github.com/dOpensource/dsiprouter/commit/b803e61f8f862aaee163bfdeecd587de319761a5)  \n> Date: Wed, 15 Nov 2023 07:08:19 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b803e61f8f862aaee163bfdeecd587de319761a5)\n[//]: # (START_SECTION c38834a439f34e16c6a8370a1609f1c501513826)\n### Fix Callee BYE Regression\n\n> Commit: [c38834a439f34e16c6a8370a1609f1c501513826](https://github.com/dOpensource/dsiprouter/commit/c38834a439f34e16c6a8370a1609f1c501513826)  \n> Date: Fri, 10 Nov 2023 15:38:51 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix callee BYE not routed to caller\n- fix rtp ports exhausted by updating timeouts\n\n\n---\n\n[//]: # (END_SECTION c38834a439f34e16c6a8370a1609f1c501513826)\n[//]: # (START_SECTION b1bb9d026fa0a2c5de1a28ef73f2fbeebdd39a58)\n### Fix ReadTheDocs\n\n> Commit: [b1bb9d026fa0a2c5de1a28ef73f2fbeebdd39a58](https://github.com/dOpensource/dsiprouter/commit/b1bb9d026fa0a2c5de1a28ef73f2fbeebdd39a58)  \n> Date: Wed, 1 Nov 2023 13:22:47 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix missing python dependencies for readthedocs\n\n\n---\n\n[//]: # (END_SECTION b1bb9d026fa0a2c5de1a28ef73f2fbeebdd39a58)\n[//]: # (START_SECTION 9e71bfb6ef03e5ae7573adabf44578691a1b0402)\n### Update ReadTheDocs Config\n\n> Commit: [9e71bfb6ef03e5ae7573adabf44578691a1b0402](https://github.com/dOpensource/dsiprouter/commit/9e71bfb6ef03e5ae7573adabf44578691a1b0402)  \n> Date: Wed, 1 Nov 2023 09:16:06 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update rtd config to [v2](https://docs.readthedocs.io/en/stable/config-file/v2.html)\n\n\n---\n\n[//]: # (END_SECTION 9e71bfb6ef03e5ae7573adabf44578691a1b0402)\n[//]: # (START_SECTION 239a79a38937b5844b47c2351118155642419320)\n### Fix CentOS 8/9 LibKS\n\n> Commit: [239a79a38937b5844b47c2351118155642419320](https://github.com/dOpensource/dsiprouter/commit/239a79a38937b5844b47c2351118155642419320)  \n> Date: Wed, 1 Nov 2023 01:00:49 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix compiling libks/libstirshaken in centos 8/9\n\n\n---\n\n[//]: # (END_SECTION 239a79a38937b5844b47c2351118155642419320)\n[//]: # (START_SECTION fad1ddc79f5567760d323634fe09adb5ba8cf27d)\n### Upgrade Fixes\n\n> Commit: [fad1ddc79f5567760d323634fe09adb5ba8cf27d](https://github.com/dOpensource/dsiprouter/commit/fad1ddc79f5567760d323634fe09adb5ba8cf27d)  \n> Date: Tue, 31 Oct 2023 21:29:48 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix bug in bash native credential parsing functions\n- fix resetting of credentials in v0.73 upgrade scripts\n- fix bug where multiple scripts could clobber `apt.conf.d` settings\n- add dpkg lock timeout to allow other processes to release apt lock\n- fix kamailio config not regenerated on upgrade\n- add more robust resetting of config files on upgrade failure\n- fix centos7 /etc/default/kamailio.conf in wrong place\n- fix upgrade log is not cleared when clicking show previous log\n- fix typo in `RESTART_DAEMONIZE` variable\n- fix cursor not a pointer on new reload buttons\n- fix reload lib functions to use new reload buttons\n- fix upgrade from gui hanging\n\n\n---\n\n[//]: # (END_SECTION fad1ddc79f5567760d323634fe09adb5ba8cf27d)\n[//]: # (START_SECTION 5cee36cfb362c6fceb14348494749a93cdb1a2d2)\n### Stability Improvements\n\n> Commit: [5cee36cfb362c6fceb14348494749a93cdb1a2d2](https://github.com/dOpensource/dsiprouter/commit/5cee36cfb362c6fceb14348494749a93cdb1a2d2)  \n> Date: Sat, 28 Oct 2023 17:01:03 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for debian 12\n- deprecate debian 9 support\n- drop support for debian7-8 and ubuntu 16.04\n- update support for centos 8-9 to stable\n- deprecate centos 7 support\n- add support for mariadb ver >= 10.6.1\n- update openssl native decrpytion/encrption in dsip_lib.sh\n- update AES CTR implementation to match openssl standard implemenation\n- improve decrypt function API in security.py\n- update project to use virtual env for python dependencies\n- bump kamailio version to 5.7.x for debian10-12/amzn2/centos8-9/ubuntu20-22/rhel8/rocky8/alma8\n- bump rtpengine version to 11.5.1.11 for debian10-12/centos8-9/ubuntu20-22/rhel8/rocky8/alma8\n- bump openssl version on amzn2 to 1.1.1q\n- bump python version on amzn2 to 3.9.18\n- bump python version on debian10 to 3.9.2\n- bump maridb version on debian10 to 10.5.21\n- improve compilation times for debian and amazon linux\n- improve startup times of configured systemd services\n- decouple nginx and dsiprouter service again (speed improvements)\n- add exec internal command for calling into main script from systemd\n- update bug report template\n- revise DB engine loading as other globals are done\n- add python version check to install command\n- allow root DB connection host/port to be set separate from kam DB\n- fix dnsmasq startup issue on debian12\n- add fix for low memory systems failing to compile large libraries\n- update CLI help message\n- imporove performance of help/version CLI commands\n- update dsiprouter manpage\n- fix bug in settings credentials when DB connection changes\n- reset default verbosity level in scripts\n- fix issue with main script project root resolution (when PWD is other git repo)\n- fix misconfigured RTP fw rules when rtpengine is not installed\n- fix systemd inhibitor locking error on centos7/amzn2\n- add initial support for selinux in centos8-9\n- update upgrade script to handle all the above changes\n\n\n---\n\n[//]: # (END_SECTION 5cee36cfb362c6fceb14348494749a93cdb1a2d2)\n[//]: # (START_SECTION 28bf73e59979e32ba4952f8923db4aee3f25f87c)\n### Changed Reload Button to a Split Dropdown Button\n\n> Commit: [28bf73e59979e32ba4952f8923db4aee3f25f87c](https://github.com/dOpensource/dsiprouter/commit/28bf73e59979e32ba4952f8923db4aee3f25f87c)  \n> Date: Thu, 19 Oct 2023 22:30:02 +0000  \n> Author: root (root@demo-dsip-v0.730)  \n> Committer: root (root@demo-dsip-v0.730)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 28bf73e59979e32ba4952f8923db4aee3f25f87c)\n[//]: # (START_SECTION d6c45d81e09759f98a897d36fe73754c261f9d4a)\n### Update Shared Memory Manager\n\n> Commit: [d6c45d81e09759f98a897d36fe73754c261f9d4a](https://github.com/dOpensource/dsiprouter/commit/d6c45d81e09759f98a897d36fe73754c261f9d4a)  \n> Date: Mon, 16 Oct 2023 12:38:19 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- move state manager to shared memory instead of domain sockets\n- move license checks to startup, improving load times drastically\n\n\n---\n\n[//]: # (END_SECTION d6c45d81e09759f98a897d36fe73754c261f9d4a)\n[//]: # (START_SECTION 29d9de5df30bdb2dbe0fb1d1de74c3e654d6920b)\n### Make Docs Great Again\n\n> Commit: [29d9de5df30bdb2dbe0fb1d1de74c3e654d6920b](https://github.com/dOpensource/dsiprouter/commit/29d9de5df30bdb2dbe0fb1d1de74c3e654d6920b)  \n> Date: Fri, 13 Oct 2023 12:46:16 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update theming on local generated documentation\n- improve navigation from main TOC\n- update upgrading insctructions\n- add more developer documentation\n\n\n---\n\n[//]: # (END_SECTION 29d9de5df30bdb2dbe0fb1d1de74c3e654d6920b)\n[//]: # (START_SECTION 4b835bbc8546269a75c79fe7b81a6313af2536b1)\n### V0.73 Release Feature Improvements\n\n> Commit: [4b835bbc8546269a75c79fe7b81a6313af2536b1](https://github.com/dOpensource/dsiprouter/commit/4b835bbc8546269a75c79fe7b81a6313af2536b1)  \n> Date: Wed, 11 Oct 2023 19:54:05 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix annoying \"detached head\" message from dependent repos on install\n- fix GUI reload process killed by systemd\n- add persistent storage to globals\n- update flask session key to be generated on install\n- update CLI to allow setting the flask session key\n- improve import efficiency by removing duplicate paths\n- create standardized response payload and tooling for API\n- implement standardized responses on majority of the API routes\n- add ability to reload GUI/web server from within the GUI\n- add ability to reload GUI/web server from the API\n- fix upgrade release search functions\n- fix edge cases in upgrade merging logic for settings\n- add an overlay when user is waiting on reloads\n- improve look and feel of upgrade page\n- improve performance when showing previous upgrade log\n- add formatted streaming log feature to upgrade page\n- remove unused and confusing error handler overrides\n- update CLI documentation/help/CLI completion\n- update CHANGELOG\n\n\n---\n\n[//]: # (END_SECTION 4b835bbc8546269a75c79fe7b81a6313af2536b1)\n[//]: # (START_SECTION 5caff2334b4dbc19e315b98e9e3b5978d1a60a30)\n### API Change\n\n> Commit: [5caff2334b4dbc19e315b98e9e3b5978d1a60a30](https://github.com/dOpensource/dsiprouter/commit/5caff2334b4dbc19e315b98e9e3b5978d1a60a30)  \n> Date: Mon, 9 Oct 2023 03:18:26 +0000  \n> Author: root (root@demo-dsip-v0.730)  \n> Committer: root (root@demo-dsip-v0.730)  \n> Signed:   \n\n\n- - Moved API to only be available via paid subscription\n\n\n---\n\n[//]: # (END_SECTION 5caff2334b4dbc19e315b98e9e3b5978d1a60a30)\n[//]: # (START_SECTION 8796289a7fbb2a88a44883baae68259b944de22c)\n### Allow Changing Upgrade Repo\n\n> Commit: [8796289a7fbb2a88a44883baae68259b944de22c](https://github.com/dOpensource/dsiprouter/commit/8796289a7fbb2a88a44883baae68259b944de22c)  \n> Date: Fri, 6 Oct 2023 17:50:03 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add ability to change upgrade repo\n\n\n---\n\n[//]: # (END_SECTION 8796289a7fbb2a88a44883baae68259b944de22c)\n[//]: # (START_SECTION 91223e1f6fac5d29ffc064745080839dc6a0892b)\n### Fix Upgrade Feature\n\n> Commit: [91223e1f6fac5d29ffc064745080839dc6a0892b](https://github.com/dOpensource/dsiprouter/commit/91223e1f6fac5d29ffc064745080839dc6a0892b)  \n> Date: Fri, 6 Oct 2023 16:47:22 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix GUI upgrade permissions\n- add bootstrap for v0.73\n- fix transaction handler could not handle routines in `dsip_lib.sh`\n- add `-rel` and `-url` options to `upgrade` command\n- improve update process logic\n- fix issues with v0.73 migrate script\n- add sudo dependency\n\n\n---\n\n[//]: # (END_SECTION 91223e1f6fac5d29ffc064745080839dc6a0892b)\n[//]: # (START_SECTION 89ab6283a42c62d625f9a0cf4d48fe76c8c0049a)\n### Fix Load Balancing Feature\n\n> Commit: [89ab6283a42c62d625f9a0cf4d48fe76c8c0049a](https://github.com/dOpensource/dsiprouter/commit/89ab6283a42c62d625f9a0cf4d48fe76c8c0049a)  \n> Date: Fri, 6 Oct 2023 11:01:31 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix dispatcher lookups and routing when lb enabled on epgroup\n- fix missing htable reloads\n- update timeouts for inbound calls to be more sane\n- update pike defaults to be more sane\n- fix servernat / rtpengine detection in serverside NAT scenarios\n- update record routing checks to work in more NAT scenarios\n- fix clientside NAT handling for UAs that do not support STUN\n- update carrier registrations to be more carrier agnostic by default\n\n\n---\n\n[//]: # (END_SECTION 89ab6283a42c62d625f9a0cf4d48fe76c8c0049a)\n[//]: # (START_SECTION c9596877cfa5d559efe756718bb24370605c5b67)\n### HotFix For CLI Upgrade\n\n> Commit: [c9596877cfa5d559efe756718bb24370605c5b67](https://github.com/dOpensource/dsiprouter/commit/c9596877cfa5d559efe756718bb24370605c5b67)  \n> Date: Wed, 27 Sep 2023 18:24:28 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add the DB procedure changes to upgrade script\n\n\n---\n\n[//]: # (END_SECTION c9596877cfa5d559efe756718bb24370605c5b67)\n[//]: # (START_SECTION bed7432a0883791f9cca6913ab03b708f10f0b85)\n### Updated README\n\n> Commit: [bed7432a0883791f9cca6913ab03b708f10f0b85](https://github.com/dOpensource/dsiprouter/commit/bed7432a0883791f9cca6913ab03b708f10f0b85)  \n> Date: Tue, 26 Sep 2023 07:52:42 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bed7432a0883791f9cca6913ab03b708f10f0b85)\n[//]: # (START_SECTION 0f94ab7184a1e81feab9d5154226630de54a720c)\n### Updated README to make it easier to read\n\n> Commit: [0f94ab7184a1e81feab9d5154226630de54a720c](https://github.com/dOpensource/dsiprouter/commit/0f94ab7184a1e81feab9d5154226630de54a720c)  \n> Date: Tue, 26 Sep 2023 07:50:30 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0f94ab7184a1e81feab9d5154226630de54a720c)\n[//]: # (START_SECTION e32711d41e34c6e07ef5bfdf48eb3b14cc2f5e4b)\n### Hotfix For DB License Storage Mismatch\n\n> Commit: [e32711d41e34c6e07ef5bfdf48eb3b14cc2f5e4b](https://github.com/dOpensource/dsiprouter/commit/e32711d41e34c6e07ef5bfdf48eb3b14cc2f5e4b)  \n> Date: Mon, 25 Sep 2023 14:02:47 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix `update_dsip_settings()` procedure input sizes for licenses\n\n\n---\n\n[//]: # (END_SECTION e32711d41e34c6e07ef5bfdf48eb3b14cc2f5e4b)\n[//]: # (START_SECTION ec3d3696c2c77ef8c2715c32a3cb001422e5ecfa)\n### Inbound Route Load Balancing Fix\n\n> Commit: [ec3d3696c2c77ef8c2715c32a3cb001422e5ecfa](https://github.com/dOpensource/dsiprouter/commit/ec3d3696c2c77ef8c2715c32a3cb001422e5ecfa)  \n> Date: Mon, 25 Sep 2023 17:11:29 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- - Added logic to correctly select the dispatcher set, which hands load balancing of requests\n\n\n---\n\n[//]: # (END_SECTION ec3d3696c2c77ef8c2715c32a3cb001422e5ecfa)\n[//]: # (START_SECTION 9d11a5062fafabc3f39573c583552e6caee2aa51)\n### API Updates\n\n> Commit: [9d11a5062fafabc3f39573c583552e6caee2aa51](https://github.com/dOpensource/dsiprouter/commit/9d11a5062fafabc3f39573c583552e6caee2aa51)  \n> Date: Mon, 25 Sep 2023 02:23:21 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- - Configured the EndpointGroup API to send an error with the hostname/ip that's maltformed or not working\n\n\n---\n\n[//]: # (END_SECTION 9d11a5062fafabc3f39573c583552e6caee2aa51)\n[//]: # (START_SECTION 10f4deb3e10de44df76acb637d7002fb00907231)\n### Core Networks Changes\n\n> Commit: [10f4deb3e10de44df76acb637d7002fb00907231](https://github.com/dOpensource/dsiprouter/commit/10f4deb3e10de44df76acb637d7002fb00907231)  \n> Date: Fri, 22 Sep 2023 20:28:55 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- - Raise an exception with a readable message when the hostname or ip is not routeable\n\n\n---\n\n[//]: # (END_SECTION 10f4deb3e10de44df76acb637d7002fb00907231)\n[//]: # (START_SECTION 1fa981a268f0a17f1d3ec33ad05739130dbf3315)\n### Changes - Added Slack link to the dSIP Dashboard - Fixed issue with the nameserver line in /etc/resolv.conf not being set correctly\n\n> Commit: [1fa981a268f0a17f1d3ec33ad05739130dbf3315](https://github.com/dOpensource/dsiprouter/commit/1fa981a268f0a17f1d3ec33ad05739130dbf3315)  \n> Date: Fri, 22 Sep 2023 02:10:56 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1fa981a268f0a17f1d3ec33ad05739130dbf3315)\n[//]: # (START_SECTION 5b9cbccd2d522d713434ec0da90e10c9b26b665e)\n### NAT Updates - Removed logic that re-wrote the contact address coming from Carriers and SIP Endpoints\n\n> Commit: [5b9cbccd2d522d713434ec0da90e10c9b26b665e](https://github.com/dOpensource/dsiprouter/commit/5b9cbccd2d522d713434ec0da90e10c9b26b665e)  \n> Date: Sat, 16 Sep 2023 02:03:52 +0000  \n> Author: root (root@demo2.dsiprouter.net)  \n> Committer: root (root@demo2.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5b9cbccd2d522d713434ec0da90e10c9b26b665e)\n[//]: # (START_SECTION 8c5e8907aba41c1b92adbc0f19c6084e0502a08d)\n### Change Default UAC Registration Address\n\n> Commit: [8c5e8907aba41c1b92adbc0f19c6084e0502a08d](https://github.com/dOpensource/dsiprouter/commit/8c5e8907aba41c1b92adbc0f19c6084e0502a08d)  \n> Date: Thu, 14 Sep 2023 09:41:44 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- make the uac reg addr default to the external IP for dynamic networking modes\n\n\n---\n\n[//]: # (END_SECTION 8c5e8907aba41c1b92adbc0f19c6084e0502a08d)\n[//]: # (START_SECTION cb9112446174e67633d26ec6b71956060a8531ab)\n### Upgrade Scripts Update\n\n> Commit: [cb9112446174e67633d26ec6b71956060a8531ab](https://github.com/dOpensource/dsiprouter/commit/cb9112446174e67633d26ec6b71956060a8531ab)  \n> Date: Wed, 6 Sep 2023 11:40:34 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- bump dsiprouter version\n- reset dsip/kam settings defaults\n- add upgrade path from v0.7x to v0.73\n- fix versioning issue with upgrade scripts\n- update CHANGELOG\n\n\n---\n\n[//]: # (END_SECTION cb9112446174e67633d26ec6b71956060a8531ab)\n[//]: # (START_SECTION 8b1ccf2e53f1da403341957b57bbb4c318f64330)\n### HotFix For Large Domain Names\n\n> Commit: [8b1ccf2e53f1da403341957b57bbb4c318f64330](https://github.com/dOpensource/dsiprouter/commit/8b1ccf2e53f1da403341957b57bbb4c318f64330)  \n> Date: Mon, 28 Aug 2023 21:59:44 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix large domain names not fitting in DB storage\n\n\n---\n\n[//]: # (END_SECTION 8b1ccf2e53f1da403341957b57bbb4c318f64330)\n[//]: # (START_SECTION ca9e5da8c80b3f9fb09752ebd8e1d2662a55f59a)\n### Cluster Deployment Fixes\n\n> Commit: [ca9e5da8c80b3f9fb09752ebd8e1d2662a55f59a](https://github.com/dOpensource/dsiprouter/commit/ca9e5da8c80b3f9fb09752ebd8e1d2662a55f59a)  \n> Date: Mon, 28 Aug 2023 21:58:49 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add check for missing commands in clusterinstall command\n- add support for choosing ssh key in clusterinstall command\n- add support for choosing ssh key in pacemaker cluster install\n- add support for choosing ssh key in galera cluster install\n- add support for digital ocean virtual IPs in pacemaker cluster install\n- fix galera cluster install service names wrong on debian 11\n- fix local project directory check in clusterinstall command\n- fix pacemaker cluster install invalid hostname resolution in cluster\n- fix pacemaker cluster install missing / conflicting hostname\n- fix pipe checks in shell library functions\n- fix clusterinstall command eating extra argument\n- update pacemaker cluster install to support corosync ver >= 0.10\n- update pacemaker cluster install to manage dsiprouter service as well\n- update pacemaker cluster install to use firewalld by default\n- update pacemaker cluster install to add cluster nodes by internal IP\n- update galera cluster install to use firewalld by default\n- update galera cluster install to add cluster nodes by internal IP\n- update clusterinstall command to use rsync instead of scp\n- update clusterinstall command to reuse credentials when cluster sync enabled\n\n\n---\n\n[//]: # (END_SECTION ca9e5da8c80b3f9fb09752ebd8e1d2662a55f59a)\n[//]: # (START_SECTION d663e2342ac65b1d559376fe3e7db6582648efec)\n### Hotfix For Dnsmasq\n\n> Commit: [d663e2342ac65b1d559376fe3e7db6582648efec](https://github.com/dOpensource/dsiprouter/commit/d663e2342ac65b1d559376fe3e7db6582648efec)  \n> Date: Fri, 18 Aug 2023 12:02:03 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix integration with resolvconf\n- fix ordering of dsiprouter module install\n\n\n---\n\n[//]: # (END_SECTION d663e2342ac65b1d559376fe3e7db6582648efec)\n[//]: # (START_SECTION 71f2577294ad7dddf7206fe30a362d29880a124d)\n### Hotfix For UAC Registration\n\n> Commit: [71f2577294ad7dddf7206fe30a362d29880a124d](https://github.com/dOpensource/dsiprouter/commit/71f2577294ad7dddf7206fe30a362d29880a124d)  \n> Date: Mon, 14 Aug 2023 09:14:52 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix uac registration when external fqdn is non-routable\n\n\n---\n\n[//]: # (END_SECTION 71f2577294ad7dddf7206fe30a362d29880a124d)\n[//]: # (START_SECTION 768793973e59ce600a909a29d744e5edaee3f934)\n### Hotfix For Flood Detection\n\n> Commit: [768793973e59ce600a909a29d744e5edaee3f934](https://github.com/dOpensource/dsiprouter/commit/768793973e59ce600a909a29d744e5edaee3f934)  \n> Date: Thu, 10 Aug 2023 15:13:17 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update flood detection to check any source other than self\n\n\n---\n\n[//]: # (END_SECTION 768793973e59ce600a909a29d744e5edaee3f934)\n[//]: # (START_SECTION 0e7e43b0471528a7dc9175c0939bb93bf9f34c85)\n### HotFix For CDR Generation\n\n> Commit: [0e7e43b0471528a7dc9175c0939bb93bf9f34c85](https://github.com/dOpensource/dsiprouter/commit/0e7e43b0471528a7dc9175c0939bb93bf9f34c85)  \n> Date: Fri, 4 Aug 2023 07:27:12 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #514\n\n\n---\n\n[//]: # (END_SECTION 0e7e43b0471528a7dc9175c0939bb93bf9f34c85)\n[//]: # (START_SECTION 997f911ecce2f4df036a30bed2e488ca983c3fba)\n### HotFix For Settings Update in Cluster Mode\n\n> Commit: [997f911ecce2f4df036a30bed2e488ca983c3fba](https://github.com/dOpensource/dsiprouter/commit/997f911ecce2f4df036a30bed2e488ca983c3fba)  \n> Date: Thu, 3 Aug 2023 13:02:30 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fixed (temp) dsiprouter settings not synced in cluster mode\n\n\n---\n\n[//]: # (END_SECTION 997f911ecce2f4df036a30bed2e488ca983c3fba)\n[//]: # (START_SECTION e1f402ac7faf5604be924bc693cd389fd4ebd3fb)\n### Kamailio Config HotFixes\n\n> Commit: [e1f402ac7faf5604be924bc693cd389fd4ebd3fb](https://github.com/dOpensource/dsiprouter/commit/e1f402ac7faf5604be924bc693cd389fd4ebd3fb)  \n> Date: Wed, 2 Aug 2023 08:58:45 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix sdp only updated on INVITE\n- fix IP Address ban not refreshed on next request from banned IP\n\n\n---\n\n[//]: # (END_SECTION e1f402ac7faf5604be924bc693cd389fd4ebd3fb)\n[//]: # (START_SECTION 6d2aa297487b38829ba832aec10ddc1e3b3d7f10)\n### Cluster Mode HotFixes\n\n> Commit: [6d2aa297487b38829ba832aec10ddc1e3b3d7f10](https://github.com/dOpensource/dsiprouter/commit/6d2aa297487b38829ba832aec10ddc1e3b3d7f10)  \n> Date: Wed, 2 Aug 2023 08:56:00 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix charset mismatches with mysql enterprise\n- fix DSIP_UNIX_SOCK and DSIP_IPC_SOCK swapped on DB update in cluster mode\n- fix DB update broken in cluster mode from sqlalchemy version update\n\n\n---\n\n[//]: # (END_SECTION 6d2aa297487b38829ba832aec10ddc1e3b3d7f10)\n[//]: # (START_SECTION 8adead794dcb8fcea85b07819c2738ada08771ae)\n### Update use-cases.rst\n\n> Commit: [8adead794dcb8fcea85b07819c2738ada08771ae](https://github.com/dOpensource/dsiprouter/commit/8adead794dcb8fcea85b07819c2738ada08771ae)  \n> Date: Thu, 20 Jul 2023 17:04:13 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8adead794dcb8fcea85b07819c2738ada08771ae)\n[//]: # (START_SECTION 55ac4519606a16f54c383c6de10d34725261c8a7)\n### Update 22.sh\n\n> Commit: [55ac4519606a16f54c383c6de10d34725261c8a7](https://github.com/dOpensource/dsiprouter/commit/55ac4519606a16f54c383c6de10d34725261c8a7)  \n> Date: Wed, 19 Jul 2023 13:52:13 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 55ac4519606a16f54c383c6de10d34725261c8a7)\n[//]: # (START_SECTION d4a6a915bc7a4ef12c14899214df01a840c4fe01)\n### Update 20.sh\n\n> Commit: [d4a6a915bc7a4ef12c14899214df01a840c4fe01](https://github.com/dOpensource/dsiprouter/commit/d4a6a915bc7a4ef12c14899214df01a840c4fe01)  \n> Date: Wed, 19 Jul 2023 13:51:08 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d4a6a915bc7a4ef12c14899214df01a840c4fe01)\n[//]: # (START_SECTION 51eb9e2614dae7f49dca658e10c268433cf19702)\n### Update 10.sh\n\n> Commit: [51eb9e2614dae7f49dca658e10c268433cf19702](https://github.com/dOpensource/dsiprouter/commit/51eb9e2614dae7f49dca658e10c268433cf19702)  \n> Date: Wed, 19 Jul 2023 13:49:52 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 51eb9e2614dae7f49dca658e10c268433cf19702)\n[//]: # (START_SECTION 350e777e101b3e0b70134f8571a598348ede7d44)\n### Fixed issue with StirShaken not able to be disbaled\n\n> Commit: [350e777e101b3e0b70134f8571a598348ede7d44](https://github.com/dOpensource/dsiprouter/commit/350e777e101b3e0b70134f8571a598348ede7d44)  \n> Date: Tue, 18 Jul 2023 21:26:55 +0000  \n> Author: root (root@mack-dsip-v0.7210)  \n> Committer: root (root@mack-dsip-v0.7210)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 350e777e101b3e0b70134f8571a598348ede7d44)\n[//]: # (START_SECTION 904879953e2217394b364f7464101dafcdce5e98)\n### Update 11.sh\n\n> Commit: [904879953e2217394b364f7464101dafcdce5e98](https://github.com/dOpensource/dsiprouter/commit/904879953e2217394b364f7464101dafcdce5e98)  \n> Date: Tue, 18 Jul 2023 06:54:50 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 904879953e2217394b364f7464101dafcdce5e98)\n[//]: # (START_SECTION 94f1469f2ed8b7412f8bf2f8b4d6e644415ffefd)\n### Fixed bug with Transnexus UI not able to be enabled\n\n> Commit: [94f1469f2ed8b7412f8bf2f8b4d6e644415ffefd](https://github.com/dOpensource/dsiprouter/commit/94f1469f2ed8b7412f8bf2f8b4d6e644415ffefd)  \n> Date: Mon, 10 Jul 2023 00:33:31 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 94f1469f2ed8b7412f8bf2f8b4d6e644415ffefd)\n[//]: # (START_SECTION b69eb35a5b634101080a43c52ab323fd1ac3faa0)\n### Fix for the gateway duplication\n\n> Commit: [b69eb35a5b634101080a43c52ab323fd1ac3faa0](https://github.com/dOpensource/dsiprouter/commit/b69eb35a5b634101080a43c52ab323fd1ac3faa0)  \n> Date: Fri, 9 Jun 2023 08:12:01 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b69eb35a5b634101080a43c52ab323fd1ac3faa0)\n[//]: # (START_SECTION e4e43e45f6d1c02c0ffa91a77719a2436dab66d5)\n### Critical Fix - Fixed MSTeams Endpoint Group so that it used 5061\n\n> Commit: [e4e43e45f6d1c02c0ffa91a77719a2436dab66d5](https://github.com/dOpensource/dsiprouter/commit/e4e43e45f6d1c02c0ffa91a77719a2436dab66d5)  \n> Date: Mon, 15 May 2023 10:56:24 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e4e43e45f6d1c02c0ffa91a77719a2436dab66d5)\n[//]: # (START_SECTION 5f3fc87b08437f11bcd55d92618f5cc8129c8931)\n### Critical Fixes - Deleting an Endpoint in CarrierGroup Causes dSIPRouter to become unavailable, Fixed #511 - Endpoint Group Weights are not being retrieved properly, Fixed #512\n\n> Commit: [5f3fc87b08437f11bcd55d92618f5cc8129c8931](https://github.com/dOpensource/dsiprouter/commit/5f3fc87b08437f11bcd55d92618f5cc8129c8931)  \n> Date: Sat, 13 May 2023 00:24:36 +0000  \n> Author: root (root@sbc4-dsip-v0.7210)  \n> Committer: root (root@sbc4-dsip-v0.7210)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5f3fc87b08437f11bcd55d92618f5cc8129c8931)\n[//]: # (START_SECTION 8e0fae1a415b8e13427bf66489fd33f362054785)\n### UI Updated pack ported\n\n> Commit: [8e0fae1a415b8e13427bf66489fd33f362054785](https://github.com/dOpensource/dsiprouter/commit/8e0fae1a415b8e13427bf66489fd33f362054785)  \n> Date: Mon, 1 May 2023 18:34:35 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8e0fae1a415b8e13427bf66489fd33f362054785)\n[//]: # (START_SECTION e188ee0611c594e582f7b3c18adb2c65ce2fe3a9)\n### Hotfixes for v0.72\n\n> Commit: [e188ee0611c594e582f7b3c18adb2c65ce2fe3a9](https://github.com/dOpensource/dsiprouter/commit/e188ee0611c594e582f7b3c18adb2c65ce2fe3a9)  \n> Date: Thu, 27 Apr 2023 13:18:12 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- pin Flask version to 2.2.x\n- fix tagging format\n- update upgrade documentation\n- add support for migrating trnasnexus licenses\n- various bug fixes in migration script\n- fix certbot overwrite on reinstall\n- # Please enter the commit message for your changes. Lines starting\n- # with '#' will be ignored, and an empty message aborts the commit.\n- #\n- # Date:      Thu Apr 27 13:03:19 2023 -0400\n- #\n- # On branch v0.721\n- # Your branch is ahead of 'origin/v0.721' by 1 commit.\n- #   (use \"git push\" to publish your local commits)\n- #\n- # Changes to be committed:\n- #\tmodified:   docs/source/user/upgrade.rst\n- #\tmodified:   dsiprouter.sh\n- #\tmodified:   gui/dsiprouter.py\n- #\tmodified:   gui/modules/api/licensemanager/routes.py\n- #\tmodified:   gui/requirements.txt\n- #\tmodified:   gui/settings.py\n- #\tmodified:   kamailio/almalinux/8.sh\n- #\tmodified:   kamailio/amzn/2.sh\n- #\tmodified:   kamailio/configs/kamailio.cfg\n- #\tmodified:   kamailio/debian/10.sh\n- #\tmodified:   kamailio/debian/11.sh\n- #\tmodified:   kamailio/debian/9.sh\n- #\tmodified:   kamailio/rhel/8.sh\n- #\tmodified:   kamailio/rocky/8.sh\n- #\tmodified:   kamailio/ubuntu/20.sh\n- #\tmodified:   kamailio/ubuntu/22.sh\n- #\tmodified:   resources/git/hooks/pre-commit\n- #\tmodified:   resources/upgrade/v0.721/scripts/bootstrap.sh\n- #\tmodified:   resources/upgrade/v0.721/scripts/migrate.sh\n- #\n\n\n---\n\n[//]: # (END_SECTION e188ee0611c594e582f7b3c18adb2c65ce2fe3a9)\n[//]: # (START_SECTION 5d5c37dd97e4337a71782b42878114c1ba391dd7)\n### Update upgrade.rst\n\n> Commit: [5d5c37dd97e4337a71782b42878114c1ba391dd7](https://github.com/dOpensource/dsiprouter/commit/5d5c37dd97e4337a71782b42878114c1ba391dd7)  \n> Date: Wed, 26 Apr 2023 02:01:57 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Added the steps to migrate from 0.70 to 0.721\n\n\n---\n\n[//]: # (END_SECTION 5d5c37dd97e4337a71782b42878114c1ba391dd7)\n[//]: # (START_SECTION 49c5034d79504c2558b57d751ad1bc63006ad2d2)\n### Fixed issue with upgrade script\n\n> Commit: [49c5034d79504c2558b57d751ad1bc63006ad2d2](https://github.com/dOpensource/dsiprouter/commit/49c5034d79504c2558b57d751ad1bc63006ad2d2)  \n> Date: Wed, 26 Apr 2023 05:27:08 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 49c5034d79504c2558b57d751ad1bc63006ad2d2)\n[//]: # (START_SECTION fc1fc199a1859d5f09454010c6e5d9e91d6ad68c)\n### Updated migration scripts to handle upgrading the shared memory\n\n> Commit: [fc1fc199a1859d5f09454010c6e5d9e91d6ad68c](https://github.com/dOpensource/dsiprouter/commit/fc1fc199a1859d5f09454010c6e5d9e91d6ad68c)  \n> Date: Wed, 26 Apr 2023 03:40:20 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fc1fc199a1859d5f09454010c6e5d9e91d6ad68c)\n[//]: # (START_SECTION 84c412b8cd2e945e630120f567402f311defd46e)\n### Added Upgrade to v0.721 logic\n\n> Commit: [84c412b8cd2e945e630120f567402f311defd46e](https://github.com/dOpensource/dsiprouter/commit/84c412b8cd2e945e630120f567402f311defd46e)  \n> Date: Wed, 26 Apr 2023 02:36:22 +0000  \n> Author: root (root@sbc4.customers.dsiprouter.net)  \n> Committer: root (root@sbc4.customers.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 84c412b8cd2e945e630120f567402f311defd46e)\n[//]: # (START_SECTION 473fb813f9d5ccd80270d63d67a02d31a95ce124)\n### Bug Fixes\n\n> Commit: [473fb813f9d5ccd80270d63d67a02d31a95ce124](https://github.com/dOpensource/dsiprouter/commit/473fb813f9d5ccd80270d63d67a02d31a95ce124)  \n> Date: Tue, 25 Apr 2023 14:33:57 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #504\n- fix credential updates broken\n- fix DNS resolution for local DNS providers\n- update login info display to include domain\n- update gitignore to exclude generated files in repo dir\n\n\n---\n\n[//]: # (END_SECTION 473fb813f9d5ccd80270d63d67a02d31a95ce124)\n[//]: # (START_SECTION b51c7a8db53f623a2dff1f84ac004da9de06b4d8)\n### MS Teams Address Table Fix\n\n> Commit: [b51c7a8db53f623a2dff1f84ac004da9de06b4d8](https://github.com/dOpensource/dsiprouter/commit/b51c7a8db53f623a2dff1f84ac004da9de06b4d8)  \n> Date: Tue, 25 Apr 2023 12:29:54 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix address entries for default ms teams endpoints\n\n\n---\n\n[//]: # (END_SECTION b51c7a8db53f623a2dff1f84ac004da9de06b4d8)\n[//]: # (START_SECTION e2a6e5fc12e9ce7015a7e73c4a93627a24fa93ce)\n### Update dsip_settings.sql\n\n> Commit: [e2a6e5fc12e9ce7015a7e73c4a93627a24fa93ce](https://github.com/dOpensource/dsiprouter/commit/e2a6e5fc12e9ce7015a7e73c4a93627a24fa93ce)  \n> Date: Fri, 21 Apr 2023 13:14:52 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Changed HOMER_ID type from INT to BIGINT\n\n\n---\n\n[//]: # (END_SECTION e2a6e5fc12e9ce7015a7e73c4a93627a24fa93ce)\n[//]: # (START_SECTION 709d07e8f693bd7fb2899735f3ac0a37024f211b)\n### MSTeams Fix\n\n> Commit: [709d07e8f693bd7fb2899735f3ac0a37024f211b](https://github.com/dOpensource/dsiprouter/commit/709d07e8f693bd7fb2899735f3ac0a37024f211b)  \n> Date: Fri, 21 Apr 2023 11:36:06 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- - Fixed the UI so that domains of type MSTeams shows up correctly\n- - Updated the Kamailio configurtion to use 3 as the MSTeams domain type\n- - Changed the MSTEAMS Endpoints back to using TLS and port 5061\n\n\n---\n\n[//]: # (END_SECTION 709d07e8f693bd7fb2899735f3ac0a37024f211b)\n[//]: # (START_SECTION 035438eb210cc122a233eb1c1543946f0bb890f6)\n### Credentials Update Hotfix\n\n> Commit: [035438eb210cc122a233eb1c1543946f0bb890f6](https://github.com/dOpensource/dsiprouter/commit/035438eb210cc122a233eb1c1543946f0bb890f6)  \n> Date: Wed, 19 Apr 2023 11:08:04 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix formatting issue with new version of `DSIP_ID` in `setCredentials()`\n\n\n---\n\n[//]: # (END_SECTION 035438eb210cc122a233eb1c1543946f0bb890f6)\n[//]: # (START_SECTION 242f6999566658e2cc385639da0fd5d169bd1bac)\n### Better Coverage For DB Update HotFix\n\n> Commit: [242f6999566658e2cc385639da0fd5d169bd1bac](https://github.com/dOpensource/dsiprouter/commit/242f6999566658e2cc385639da0fd5d169bd1bac)  \n> Date: Tue, 18 Apr 2023 16:15:09 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix extra test cases found in 816f78bd4adc634bd31ff95086f27813b78dcc41\n\n\n---\n\n[//]: # (END_SECTION 242f6999566658e2cc385639da0fd5d169bd1bac)\n[//]: # (START_SECTION 626da8c3d4947651afcbb7061e6cf07e2bd3739c)\n### Domain Pass-thru Fixes - Fixed the PBX_TO_ENDPOINT_LOOKUP to lookup the location of an endpoint only if the request is coming from a PBX and the IP Address is private\n\n> Commit: [626da8c3d4947651afcbb7061e6cf07e2bd3739c](https://github.com/dOpensource/dsiprouter/commit/626da8c3d4947651afcbb7061e6cf07e2bd3739c)  \n> Date: Tue, 18 Apr 2023 19:17:58 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 626da8c3d4947651afcbb7061e6cf07e2bd3739c)\n[//]: # (START_SECTION 816f78bd4adc634bd31ff95086f27813b78dcc41)\n### DB Sync and Certbot Hotfix\n\n> Commit: [816f78bd4adc634bd31ff95086f27813b78dcc41](https://github.com/dOpensource/dsiprouter/commit/816f78bd4adc634bd31ff95086f27813b78dcc41)  \n> Date: Tue, 18 Apr 2023 15:03:09 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix issue where `dsip_settings` table was not updating properly\n- fix missing python package for updated certbot version\n\n\n---\n\n[//]: # (END_SECTION 816f78bd4adc634bd31ff95086f27813b78dcc41)\n[//]: # (START_SECTION 2a54a13ce90f9ec2b0907631e9995457d7f778ef)\n### Update Default Settings\n\n> Commit: [2a54a13ce90f9ec2b0907631e9995457d7f778ef](https://github.com/dOpensource/dsiprouter/commit/2a54a13ce90f9ec2b0907631e9995457d7f778ef)  \n> Date: Mon, 17 Apr 2023 09:41:32 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- reset default dsiprouter and kamailio settings\n- update dsiprouter version number\n\n\n---\n\n[//]: # (END_SECTION 2a54a13ce90f9ec2b0907631e9995457d7f778ef)\n[//]: # (START_SECTION 3209ac195e1b47e42811a44ce5affb4764d4072b)\n### Patch Dependencies and Backup Feature\n\n> Commit: [3209ac195e1b47e42811a44ce5affb4764d4072b](https://github.com/dOpensource/dsiprouter/commit/3209ac195e1b47e42811a44ce5affb4764d4072b)  \n> Date: Mon, 17 Apr 2023 09:22:01 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #503\n- fix `josepy` and `acme` dependencies after certbot version update\n- fix `send_file` function calls after Flask version update\n\n\n---\n\n[//]: # (END_SECTION 3209ac195e1b47e42811a44ce5affb4764d4072b)\n[//]: # (START_SECTION 6758cb002b0bbc88e1e8c736f4a770fb190accc7)\n### Cert / MS Teams HotFix\n\n> Commit: [6758cb002b0bbc88e1e8c736f4a770fb190accc7](https://github.com/dOpensource/dsiprouter/commit/6758cb002b0bbc88e1e8c736f4a770fb190accc7)  \n> Date: Fri, 14 Apr 2023 16:14:53 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix DST_Root_CA_X3 expired in lets encrypt chain\n- update certbot install version\n- fix uploading certs only parses the first cert in the chain\n- fix MS Teams inbound routing\n- fix MS Teams outbound routing via IP address\n- add option to route MS Teams domain via FQDN\n- fix MS Teams default address settings typo\n- fix kamailio TLS reload bug\n- update default memory allocatoin for kamailio\n- fix kamailio does not update TLS cert when reloading\n\n\n---\n\n[//]: # (END_SECTION 6758cb002b0bbc88e1e8c736f4a770fb190accc7)\n[//]: # (START_SECTION d8c889ecc66770e3d388c91860bad31a68fa0803)\n### handle a bug where multiple addresses are returned from the db.\n\n> Commit: [d8c889ecc66770e3d388c91860bad31a68fa0803](https://github.com/dOpensource/dsiprouter/commit/d8c889ecc66770e3d388c91860bad31a68fa0803)  \n> Date: Thu, 13 Apr 2023 08:50:04 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d8c889ecc66770e3d388c91860bad31a68fa0803)\n[//]: # (START_SECTION 25b049f577776529d1e6eb1b651c895eeb8363e2)\n### MSTeams fix - The load balancing attribute will not be set when creating an endpoint group of type Microsoft Teams\n\n> Commit: [25b049f577776529d1e6eb1b651c895eeb8363e2](https://github.com/dOpensource/dsiprouter/commit/25b049f577776529d1e6eb1b651c895eeb8363e2)  \n> Date: Thu, 13 Apr 2023 14:04:41 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 25b049f577776529d1e6eb1b651c895eeb8363e2)\n[//]: # (START_SECTION eeb1eaad119a1ee5809f88b9af203956fc7f02eb)\n### MSTeams fix - A dispatcher load balancing gorup will not be created when a MSTeams domain is created\n\n> Commit: [eeb1eaad119a1ee5809f88b9af203956fc7f02eb](https://github.com/dOpensource/dsiprouter/commit/eeb1eaad119a1ee5809f88b9af203956fc7f02eb)  \n> Date: Thu, 13 Apr 2023 13:26:45 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eeb1eaad119a1ee5809f88b9af203956fc7f02eb)\n[//]: # (START_SECTION 86cb8df0f93f1a6cd07f560396de031e4a7abe08)\n### Licensing Patch\n\n> Commit: [86cb8df0f93f1a6cd07f560396de031e4a7abe08](https://github.com/dOpensource/dsiprouter/commit/86cb8df0f93f1a6cd07f560396de031e4a7abe08)  \n> Date: Fri, 7 Apr 2023 14:54:27 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for variable length license keys\n- fix error handling in license manager\n- update error message for license check failures\n- fix `stop` command scope issue in CLI\n\n\n---\n\n[//]: # (END_SECTION 86cb8df0f93f1a6cd07f560396de031e4a7abe08)\n[//]: # (START_SECTION 91d62644a07fc9b3693fffbb3ccfdcbddb4db794)\n### Added a fix to select the Correct load balance ID in the UI of the inbound route page\n\n> Commit: [91d62644a07fc9b3693fffbb3ccfdcbddb4db794](https://github.com/dOpensource/dsiprouter/commit/91d62644a07fc9b3693fffbb3ccfdcbddb4db794)  \n> Date: Tue, 4 Apr 2023 07:06:23 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 91d62644a07fc9b3693fffbb3ccfdcbddb4db794)\n[//]: # (START_SECTION 7ccb9c615e62c349a67e2509eed9741d5d064cae)\n### Update Documentation for v0.72\n\n> Commit: [7ccb9c615e62c349a67e2509eed9741d5d064cae](https://github.com/dOpensource/dsiprouter/commit/7ccb9c615e62c349a67e2509eed9741d5d064cae)  \n> Date: Mon, 3 Apr 2023 15:52:09 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update boostrap command\n- update changelog\n\n\n---\n\n[//]: # (END_SECTION 7ccb9c615e62c349a67e2509eed9741d5d064cae)\n[//]: # (START_SECTION 47c73b18c4d1eed30af136cdcb36f44fa8f0229f)\n### Update Documentation for v0.72\n\n> Commit: [47c73b18c4d1eed30af136cdcb36f44fa8f0229f](https://github.com/dOpensource/dsiprouter/commit/47c73b18c4d1eed30af136cdcb36f44fa8f0229f)  \n> Date: Mon, 3 Apr 2023 15:52:09 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update boostrap command\n- update changelog\n\n\n---\n\n[//]: # (END_SECTION 47c73b18c4d1eed30af136cdcb36f44fa8f0229f)\n[//]: # (START_SECTION 28309c1a2d37d8ec3b3e3bfdbce1afda1c33ca28)\n### v0.72 Release Bug Fix\n\n> Commit: [28309c1a2d37d8ec3b3e3bfdbce1afda1c33ca28](https://github.com/dOpensource/dsiprouter/commit/28309c1a2d37d8ec3b3e3bfdbce1afda1c33ca28)  \n> Date: Mon, 3 Apr 2023 14:09:23 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix bad hash algorithm implementation in the CLI functions\n- fix schema migration issues on `dsip_settings` table\n- fix edge cases where bootstrapping failed\n- # Please enter the commit message for your changes. Lines starting\n- # with '#' will be ignored, and an empty message aborts the commit.\n- #\n- # On branch v0.72\n- # Your branch is up to date with 'origin/v0.72'.\n- #\n- # Changes to be committed:\n- #\tmodified:   dsiprouter/dsip_lib.sh\n- #\tmodified:   resources/upgrade/v0.72/scripts/bootstrap.sh\n- #\tmodified:   resources/upgrade/v0.72/scripts/migrate.sh\n- #\n\n\n---\n\n[//]: # (END_SECTION 28309c1a2d37d8ec3b3e3bfdbce1afda1c33ca28)\n[//]: # (START_SECTION e0bbbfa860e57f9549736149142c9df8be72433b)\n### Bootstrap Fixes\n\n> Commit: [e0bbbfa860e57f9549736149142c9df8be72433b](https://github.com/dOpensource/dsiprouter/commit/e0bbbfa860e57f9549736149142c9df8be72433b)  \n> Date: Mon, 3 Apr 2023 10:32:08 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- change bootstrap to use tag instead of branch\n- fix python versioning\n- fix boostrap variable scoping\n- # Please enter the commit message for your changes. Lines starting\n- # with '#' will be ignored, and an empty message aborts the commit.\n- #\n- # Date:      Mon Apr 3 10:32:08 2023 -0400\n- #\n- # On branch v0.72\n- # Your branch is up to date with 'origin/v0.72'.\n- #\n- # Changes to be committed:\n- #\tmodified:   dsiprouter.sh\n- #\tmodified:   resources/upgrade/v0.72/scripts/bootstrap.sh\n- #\tmodified:   resources/upgrade/v0.72/scripts/migrate.sh\n- #\n\n\n---\n\n[//]: # (END_SECTION e0bbbfa860e57f9549736149142c9df8be72433b)\n[//]: # (START_SECTION a659fedb44ef2a844282d3de0d5503c567837948)\n### Merge Changes From Master (#500)\n\n> Commit: [a659fedb44ef2a844282d3de0d5503c567837948](https://github.com/dOpensource/dsiprouter/commit/a659fedb44ef2a844282d3de0d5503c567837948)  \n> Date: Mon, 3 Apr 2023 10:10:11 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Co-authored-by: Mack Hendricks <mack@dopensource.com>\n\n\n---\n\n[//]: # (END_SECTION a659fedb44ef2a844282d3de0d5503c567837948)\n[//]: # (START_SECTION baa6415f1d630b695cd28de742ed609eb65f1399)\n### Update The Changelog\n\n> Commit: [baa6415f1d630b695cd28de742ed609eb65f1399](https://github.com/dOpensource/dsiprouter/commit/baa6415f1d630b695cd28de742ed609eb65f1399)  \n> Date: Mon, 3 Apr 2023 09:12:46 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update `CHANGELOG.md`\n\n\n---\n\n[//]: # (END_SECTION baa6415f1d630b695cd28de742ed609eb65f1399)\n[//]: # (START_SECTION bc560f0fd419cbe271e22c0801e64e92a9469397)\n### Fixed permissions again\n\n> Commit: [bc560f0fd419cbe271e22c0801e64e92a9469397](https://github.com/dOpensource/dsiprouter/commit/bc560f0fd419cbe271e22c0801e64e92a9469397)  \n> Date: Sat, 1 Apr 2023 21:09:50 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bc560f0fd419cbe271e22c0801e64e92a9469397)\n[//]: # (START_SECTION 0209c81fa9302517ac674725cefc1f4799d70d28)\n### Added execute permissions\n\n> Commit: [0209c81fa9302517ac674725cefc1f4799d70d28](https://github.com/dOpensource/dsiprouter/commit/0209c81fa9302517ac674725cefc1f4799d70d28)  \n> Date: Sat, 1 Apr 2023 20:29:56 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0209c81fa9302517ac674725cefc1f4799d70d28)\n[//]: # (START_SECTION c850b78988eed29591fcacbc01d4c26f2f49c22a)\n### Fixed permissions\n\n> Commit: [c850b78988eed29591fcacbc01d4c26f2f49c22a](https://github.com/dOpensource/dsiprouter/commit/c850b78988eed29591fcacbc01d4c26f2f49c22a)  \n> Date: Sat, 1 Apr 2023 20:25:34 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c850b78988eed29591fcacbc01d4c26f2f49c22a)\n[//]: # (START_SECTION 36297d60599a1f214e8d183f978d2688236b4479)\n### Stir Shaken Fixes - Fixed an issue with the certificate URL not being saved - Added logic to copy the self-signed certs to the proper location\n\n> Commit: [36297d60599a1f214e8d183f978d2688236b4479](https://github.com/dOpensource/dsiprouter/commit/36297d60599a1f214e8d183f978d2688236b4479)  \n> Date: Sat, 1 Apr 2023 20:12:38 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 36297d60599a1f214e8d183f978d2688236b4479)\n[//]: # (START_SECTION fbeeb172c70957d7397f66a612811e75fd31dd6c)\n### reverting change\n\n> Commit: [fbeeb172c70957d7397f66a612811e75fd31dd6c](https://github.com/dOpensource/dsiprouter/commit/fbeeb172c70957d7397f66a612811e75fd31dd6c)  \n> Date: Fri, 31 Mar 2023 14:42:14 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fbeeb172c70957d7397f66a612811e75fd31dd6c)\n[//]: # (START_SECTION 2ec6da12a319bb378df586332c56261484ae1ace)\n### fixed a bug that caused password encoding issues\n\n> Commit: [2ec6da12a319bb378df586332c56261484ae1ace](https://github.com/dOpensource/dsiprouter/commit/2ec6da12a319bb378df586332c56261484ae1ace)  \n> Date: Fri, 31 Mar 2023 14:21:37 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2ec6da12a319bb378df586332c56261484ae1ace)\n[//]: # (START_SECTION d2b1e55b3a9ea5743f68389a1ef864b4c06642a5)\n### Update migrate.sh\n\n> Commit: [d2b1e55b3a9ea5743f68389a1ef864b4c06642a5](https://github.com/dOpensource/dsiprouter/commit/d2b1e55b3a9ea5743f68389a1ef864b4c06642a5)  \n> Date: Fri, 31 Mar 2023 15:38:16 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d2b1e55b3a9ea5743f68389a1ef864b4c06642a5)\n[//]: # (START_SECTION 4e717ae72b7d49bd35dbb9d6bef4b37b3fdb66ae)\n### Update migrate.sh\n\n> Commit: [4e717ae72b7d49bd35dbb9d6bef4b37b3fdb66ae](https://github.com/dOpensource/dsiprouter/commit/4e717ae72b7d49bd35dbb9d6bef4b37b3fdb66ae)  \n> Date: Fri, 31 Mar 2023 15:15:23 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4e717ae72b7d49bd35dbb9d6bef4b37b3fdb66ae)\n[//]: # (START_SECTION 1c939ac8a7fc954a21354672fab2cb0e8f877ce4)\n### Update upgrade.rst\n\n> Commit: [1c939ac8a7fc954a21354672fab2cb0e8f877ce4](https://github.com/dOpensource/dsiprouter/commit/1c939ac8a7fc954a21354672fab2cb0e8f877ce4)  \n> Date: Fri, 31 Mar 2023 12:18:01 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1c939ac8a7fc954a21354672fab2cb0e8f877ce4)\n[//]: # (START_SECTION 8351d90c77289f126aad66c145eef2dc753591e9)\n### Update upgrade.rst\n\n> Commit: [8351d90c77289f126aad66c145eef2dc753591e9](https://github.com/dOpensource/dsiprouter/commit/8351d90c77289f126aad66c145eef2dc753591e9)  \n> Date: Fri, 31 Mar 2023 12:12:01 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8351d90c77289f126aad66c145eef2dc753591e9)\n[//]: # (START_SECTION cbea100db0f75e72d39ae6eeeaaf6164221cdc4e)\n### Update upgrade.rst\n\n> Commit: [cbea100db0f75e72d39ae6eeeaaf6164221cdc4e](https://github.com/dOpensource/dsiprouter/commit/cbea100db0f75e72d39ae6eeeaaf6164221cdc4e)  \n> Date: Fri, 31 Mar 2023 12:09:29 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cbea100db0f75e72d39ae6eeeaaf6164221cdc4e)\n[//]: # (START_SECTION 7b883cf29bc9ae40ad5cc09a58f9830a4d7a8942)\n### Update upgrade.rst\n\n> Commit: [7b883cf29bc9ae40ad5cc09a58f9830a4d7a8942](https://github.com/dOpensource/dsiprouter/commit/7b883cf29bc9ae40ad5cc09a58f9830a4d7a8942)  \n> Date: Fri, 31 Mar 2023 12:05:36 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7b883cf29bc9ae40ad5cc09a58f9830a4d7a8942)\n[//]: # (START_SECTION 5674c7f6191b002b43d9f2e89736b26c890e1ea7)\n### Update upgrade.rst\n\n> Commit: [5674c7f6191b002b43d9f2e89736b26c890e1ea7](https://github.com/dOpensource/dsiprouter/commit/5674c7f6191b002b43d9f2e89736b26c890e1ea7)  \n> Date: Fri, 31 Mar 2023 12:03:58 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5674c7f6191b002b43d9f2e89736b26c890e1ea7)\n[//]: # (START_SECTION 852f7412f7ad9c0502eaab76579a6e0f71f51f60)\n### Update upgrade.rst\n\n> Commit: [852f7412f7ad9c0502eaab76579a6e0f71f51f60](https://github.com/dOpensource/dsiprouter/commit/852f7412f7ad9c0502eaab76579a6e0f71f51f60)  \n> Date: Fri, 31 Mar 2023 12:02:52 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 852f7412f7ad9c0502eaab76579a6e0f71f51f60)\n[//]: # (START_SECTION 80ae06ac5d83a3d219cbcca7432cae4de6403fc5)\n### Add files via upload\n\n> Commit: [80ae06ac5d83a3d219cbcca7432cae4de6403fc5](https://github.com/dOpensource/dsiprouter/commit/80ae06ac5d83a3d219cbcca7432cae4de6403fc5)  \n> Date: Fri, 31 Mar 2023 07:57:31 -0700  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 80ae06ac5d83a3d219cbcca7432cae4de6403fc5)\n[//]: # (START_SECTION 669a4e115aad6efc4027cf9bb989e0fc2ea52729)\n### Upgrade Feature CLI Completion\n\n> Commit: [669a4e115aad6efc4027cf9bb989e0fc2ea52729](https://github.com/dOpensource/dsiprouter/commit/669a4e115aad6efc4027cf9bb989e0fc2ea52729)  \n> Date: Fri, 31 Mar 2023 10:31:14 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 669a4e115aad6efc4027cf9bb989e0fc2ea52729)\n[//]: # (START_SECTION 898f6dc5d884c92a049e06bbf4b3aac1f2a01f01)\n### WIP Upgrade Feature\n\n> Commit: [898f6dc5d884c92a049e06bbf4b3aac1f2a01f01](https://github.com/dOpensource/dsiprouter/commit/898f6dc5d884c92a049e06bbf4b3aac1f2a01f01)  \n> Date: Fri, 31 Mar 2023 09:05:55 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 898f6dc5d884c92a049e06bbf4b3aac1f2a01f01)\n[//]: # (START_SECTION 5f7bec3e9b4802429701f3cdbc159ac379370f48)\n### Update api.rst\n\n> Commit: [5f7bec3e9b4802429701f3cdbc159ac379370f48](https://github.com/dOpensource/dsiprouter/commit/5f7bec3e9b4802429701f3cdbc159ac379370f48)  \n> Date: Thu, 30 Mar 2023 23:58:55 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5f7bec3e9b4802429701f3cdbc159ac379370f48)\n[//]: # (START_SECTION dda4c22b944c3a1972fff24cd112a377b92f8d64)\n### Update api.rst\n\n> Commit: [dda4c22b944c3a1972fff24cd112a377b92f8d64](https://github.com/dOpensource/dsiprouter/commit/dda4c22b944c3a1972fff24cd112a377b92f8d64)  \n> Date: Thu, 30 Mar 2023 23:57:44 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dda4c22b944c3a1972fff24cd112a377b92f8d64)\n[//]: # (START_SECTION 9ee06fb3c6c2f1eed7b2db7bcd750ed152bc8895)\n### Update api.rst\n\n> Commit: [9ee06fb3c6c2f1eed7b2db7bcd750ed152bc8895](https://github.com/dOpensource/dsiprouter/commit/9ee06fb3c6c2f1eed7b2db7bcd750ed152bc8895)  \n> Date: Thu, 30 Mar 2023 23:56:38 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9ee06fb3c6c2f1eed7b2db7bcd750ed152bc8895)\n[//]: # (START_SECTION 5522370d4d6f461c03c1caf31d6c7bebb90d9712)\n### Update api.rst\n\n> Commit: [5522370d4d6f461c03c1caf31d6c7bebb90d9712](https://github.com/dOpensource/dsiprouter/commit/5522370d4d6f461c03c1caf31d6c7bebb90d9712)  \n> Date: Thu, 30 Mar 2023 23:55:22 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5522370d4d6f461c03c1caf31d6c7bebb90d9712)\n[//]: # (START_SECTION 98e5622375544c0eb7ae3f8233dc676f01de80e2)\n### Update Bootstrap Process\n\n> Commit: [98e5622375544c0eb7ae3f8233dc676f01de80e2](https://github.com/dOpensource/dsiprouter/commit/98e5622375544c0eb7ae3f8233dc676f01de80e2)  \n> Date: Thu, 30 Mar 2023 23:45:48 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 98e5622375544c0eb7ae3f8233dc676f01de80e2)\n[//]: # (START_SECTION d43dd36b6fcd625fcf80b9ce7cd380cc699c4666)\n### Update api.rst\n\n> Commit: [d43dd36b6fcd625fcf80b9ce7cd380cc699c4666](https://github.com/dOpensource/dsiprouter/commit/d43dd36b6fcd625fcf80b9ce7cd380cc699c4666)  \n> Date: Thu, 30 Mar 2023 23:24:04 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d43dd36b6fcd625fcf80b9ce7cd380cc699c4666)\n[//]: # (START_SECTION b599ffed2123ac4a8db6e572d71c540e44031ca3)\n### Update api.rst\n\n> Commit: [b599ffed2123ac4a8db6e572d71c540e44031ca3](https://github.com/dOpensource/dsiprouter/commit/b599ffed2123ac4a8db6e572d71c540e44031ca3)  \n> Date: Thu, 30 Mar 2023 23:19:27 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b599ffed2123ac4a8db6e572d71c540e44031ca3)\n[//]: # (START_SECTION 4b5f72a3b6d30d2bc660e947bcb2f924785979b7)\n### Update api.rst\n\n> Commit: [4b5f72a3b6d30d2bc660e947bcb2f924785979b7](https://github.com/dOpensource/dsiprouter/commit/4b5f72a3b6d30d2bc660e947bcb2f924785979b7)  \n> Date: Thu, 30 Mar 2023 23:17:51 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4b5f72a3b6d30d2bc660e947bcb2f924785979b7)\n[//]: # (START_SECTION 8258df375bc73cfc17b19cc92cc1e371295b00e9)\n### Update api.rst\n\n> Commit: [8258df375bc73cfc17b19cc92cc1e371295b00e9](https://github.com/dOpensource/dsiprouter/commit/8258df375bc73cfc17b19cc92cc1e371295b00e9)  \n> Date: Thu, 30 Mar 2023 23:16:23 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8258df375bc73cfc17b19cc92cc1e371295b00e9)\n[//]: # (START_SECTION fb34d3bc071c54e92d5840c5c818bdb79f8209cc)\n### Update use-cases.rst\n\n> Commit: [fb34d3bc071c54e92d5840c5c818bdb79f8209cc](https://github.com/dOpensource/dsiprouter/commit/fb34d3bc071c54e92d5840c5c818bdb79f8209cc)  \n> Date: Thu, 30 Mar 2023 23:06:12 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fb34d3bc071c54e92d5840c5c818bdb79f8209cc)\n[//]: # (START_SECTION 1d46d255fed53ec1511b103af5a99d64edf82edb)\n### Update use-cases.rst\n\n> Commit: [1d46d255fed53ec1511b103af5a99d64edf82edb](https://github.com/dOpensource/dsiprouter/commit/1d46d255fed53ec1511b103af5a99d64edf82edb)  \n> Date: Thu, 30 Mar 2023 23:00:45 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1d46d255fed53ec1511b103af5a99d64edf82edb)\n[//]: # (START_SECTION a1738ef6ecf934e6775afcc7045adac7b1e31819)\n### Update use-cases.rst\n\n> Commit: [a1738ef6ecf934e6775afcc7045adac7b1e31819](https://github.com/dOpensource/dsiprouter/commit/a1738ef6ecf934e6775afcc7045adac7b1e31819)  \n> Date: Thu, 30 Mar 2023 22:44:33 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a1738ef6ecf934e6775afcc7045adac7b1e31819)\n[//]: # (START_SECTION 34cd79c63c171b3bcb8b37a6857bb386b0aad127)\n### Update carrier_groups.rst\n\n> Commit: [34cd79c63c171b3bcb8b37a6857bb386b0aad127](https://github.com/dOpensource/dsiprouter/commit/34cd79c63c171b3bcb8b37a6857bb386b0aad127)  \n> Date: Thu, 30 Mar 2023 22:34:59 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 34cd79c63c171b3bcb8b37a6857bb386b0aad127)\n[//]: # (START_SECTION a54465a970ab50e701cf5a8c44ddb2fd55f6acee)\n### Allow v0.70 Upgrade Bootstrapping\n\n> Commit: [a54465a970ab50e701cf5a8c44ddb2fd55f6acee](https://github.com/dOpensource/dsiprouter/commit/a54465a970ab50e701cf5a8c44ddb2fd55f6acee)  \n> Date: Thu, 30 Mar 2023 22:00:53 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION a54465a970ab50e701cf5a8c44ddb2fd55f6acee)\n[//]: # (START_SECTION 2953f326f02116afc1dc4fb5acbb458c63dba834)\n### Update use-cases.rst\n\n> Commit: [2953f326f02116afc1dc4fb5acbb458c63dba834](https://github.com/dOpensource/dsiprouter/commit/2953f326f02116afc1dc4fb5acbb458c63dba834)  \n> Date: Thu, 30 Mar 2023 20:01:21 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2953f326f02116afc1dc4fb5acbb458c63dba834)\n[//]: # (START_SECTION 6843ead09ca062c856a7c199a247aaf817b88306)\n### Update use-cases.rst\n\n> Commit: [6843ead09ca062c856a7c199a247aaf817b88306](https://github.com/dOpensource/dsiprouter/commit/6843ead09ca062c856a7c199a247aaf817b88306)  \n> Date: Thu, 30 Mar 2023 19:58:31 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6843ead09ca062c856a7c199a247aaf817b88306)\n[//]: # (START_SECTION b935436bb85ffc97b3abb14458ff6d8de657f204)\n### Update use-cases.rst\n\n> Commit: [b935436bb85ffc97b3abb14458ff6d8de657f204](https://github.com/dOpensource/dsiprouter/commit/b935436bb85ffc97b3abb14458ff6d8de657f204)  \n> Date: Thu, 30 Mar 2023 19:55:33 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b935436bb85ffc97b3abb14458ff6d8de657f204)\n[//]: # (START_SECTION 96799b3245092cc24ed082212edd023802ad60b8)\n### Update use-cases.rst\n\n> Commit: [96799b3245092cc24ed082212edd023802ad60b8](https://github.com/dOpensource/dsiprouter/commit/96799b3245092cc24ed082212edd023802ad60b8)  \n> Date: Thu, 30 Mar 2023 19:43:08 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 96799b3245092cc24ed082212edd023802ad60b8)\n[//]: # (START_SECTION c08a7f914caf43d37ffeff09bb8f9bf75dd442dd)\n### Update use-cases.rst\n\n> Commit: [c08a7f914caf43d37ffeff09bb8f9bf75dd442dd](https://github.com/dOpensource/dsiprouter/commit/c08a7f914caf43d37ffeff09bb8f9bf75dd442dd)  \n> Date: Thu, 30 Mar 2023 19:40:02 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c08a7f914caf43d37ffeff09bb8f9bf75dd442dd)\n[//]: # (START_SECTION 05f1eca4837d1676905d882a44d112de5c92562b)\n### Add files via upload\n\n> Commit: [05f1eca4837d1676905d882a44d112de5c92562b](https://github.com/dOpensource/dsiprouter/commit/05f1eca4837d1676905d882a44d112de5c92562b)  \n> Date: Thu, 30 Mar 2023 16:36:42 -0700  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 05f1eca4837d1676905d882a44d112de5c92562b)\n[//]: # (START_SECTION 6e931ce7092aecb7ce511059b3e40d4f6b1c9fda)\n### Update use-cases.rst\n\n> Commit: [6e931ce7092aecb7ce511059b3e40d4f6b1c9fda](https://github.com/dOpensource/dsiprouter/commit/6e931ce7092aecb7ce511059b3e40d4f6b1c9fda)  \n> Date: Thu, 30 Mar 2023 19:35:45 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6e931ce7092aecb7ce511059b3e40d4f6b1c9fda)\n[//]: # (START_SECTION aaf31fce475ee60b5dccce765c34d9208a3af280)\n### Update use-cases.rst\n\n> Commit: [aaf31fce475ee60b5dccce765c34d9208a3af280](https://github.com/dOpensource/dsiprouter/commit/aaf31fce475ee60b5dccce765c34d9208a3af280)  \n> Date: Thu, 30 Mar 2023 17:42:03 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION aaf31fce475ee60b5dccce765c34d9208a3af280)\n[//]: # (START_SECTION 3b341f4168e65f653fdf7191a08978e967e86237)\n### Update use-cases.rst\n\n> Commit: [3b341f4168e65f653fdf7191a08978e967e86237](https://github.com/dOpensource/dsiprouter/commit/3b341f4168e65f653fdf7191a08978e967e86237)  \n> Date: Thu, 30 Mar 2023 15:35:55 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3b341f4168e65f653fdf7191a08978e967e86237)\n[//]: # (START_SECTION b926ea7f70d26818a04a8c75b68f295cdb8f4978)\n### Fix Upgrade Feature Entrypoints\n\n> Commit: [b926ea7f70d26818a04a8c75b68f295cdb8f4978](https://github.com/dOpensource/dsiprouter/commit/b926ea7f70d26818a04a8c75b68f295cdb8f4978)  \n> Date: Thu, 30 Mar 2023 15:13:41 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION b926ea7f70d26818a04a8c75b68f295cdb8f4978)\n[//]: # (START_SECTION 02ea31c21261e6c9c374cf51e67484b0d358409f)\n### Update rhel_install.rst\n\n> Commit: [02ea31c21261e6c9c374cf51e67484b0d358409f](https://github.com/dOpensource/dsiprouter/commit/02ea31c21261e6c9c374cf51e67484b0d358409f)  \n> Date: Thu, 30 Mar 2023 15:01:51 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 02ea31c21261e6c9c374cf51e67484b0d358409f)\n[//]: # (START_SECTION 3c29315838afcc70648170e0cd12d8bcde3cb6ae)\n### Update debian_install.rst\n\n> Commit: [3c29315838afcc70648170e0cd12d8bcde3cb6ae](https://github.com/dOpensource/dsiprouter/commit/3c29315838afcc70648170e0cd12d8bcde3cb6ae)  \n> Date: Thu, 30 Mar 2023 15:01:19 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3c29315838afcc70648170e0cd12d8bcde3cb6ae)\n[//]: # (START_SECTION cce7d0412b19159e65436c667241a7580b384263)\n### Upgrade Feature Updates\n\n> Commit: [cce7d0412b19159e65436c667241a7580b384263](https://github.com/dOpensource/dsiprouter/commit/cce7d0412b19159e65436c667241a7580b384263)  \n> Date: Thu, 30 Mar 2023 14:55:44 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION cce7d0412b19159e65436c667241a7580b384263)\n[//]: # (START_SECTION 89450c0d2b9cbff0f9c5360b3301811bde4a17c1)\n### Update installing.rst\n\n> Commit: [89450c0d2b9cbff0f9c5360b3301811bde4a17c1](https://github.com/dOpensource/dsiprouter/commit/89450c0d2b9cbff0f9c5360b3301811bde4a17c1)  \n> Date: Thu, 30 Mar 2023 14:44:18 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 89450c0d2b9cbff0f9c5360b3301811bde4a17c1)\n[//]: # (START_SECTION 41736eb05ed2c9469622d49d9f41b7bc5b8125d0)\n### Update installing.rst\n\n> Commit: [41736eb05ed2c9469622d49d9f41b7bc5b8125d0](https://github.com/dOpensource/dsiprouter/commit/41736eb05ed2c9469622d49d9f41b7bc5b8125d0)  \n> Date: Thu, 30 Mar 2023 14:36:36 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 41736eb05ed2c9469622d49d9f41b7bc5b8125d0)\n[//]: # (START_SECTION a7b22b34d458ba0b53af1bf452064caa61897078)\n### Standalong Script checkpoint\n\n> Commit: [a7b22b34d458ba0b53af1bf452064caa61897078](https://github.com/dOpensource/dsiprouter/commit/a7b22b34d458ba0b53af1bf452064caa61897078)  \n> Date: Thu, 30 Mar 2023 10:12:26 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a7b22b34d458ba0b53af1bf452064caa61897078)\n[//]: # (START_SECTION e415705f275a8aa4ec19f595c156338ecbed1075)\n### WIP Changes for Upgrade Feature\n\n> Commit: [e415705f275a8aa4ec19f595c156338ecbed1075](https://github.com/dOpensource/dsiprouter/commit/e415705f275a8aa4ec19f595c156338ecbed1075)  \n> Date: Thu, 30 Mar 2023 11:50:17 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION e415705f275a8aa4ec19f595c156338ecbed1075)\n[//]: # (START_SECTION e24d53894f2f613e8d76d61b0065fce3fe63adcb)\n### Upgrade script tweaks\n\n> Commit: [e24d53894f2f613e8d76d61b0065fce3fe63adcb](https://github.com/dOpensource/dsiprouter/commit/e24d53894f2f613e8d76d61b0065fce3fe63adcb)  \n> Date: Thu, 30 Mar 2023 07:43:15 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e24d53894f2f613e8d76d61b0065fce3fe63adcb)\n[//]: # (START_SECTION 2c80df409e6cb9e0782d6f7544096041ebfa225b)\n### Bug Fixes And Cleanup For v0.72\n\n> Commit: [2c80df409e6cb9e0782d6f7544096041ebfa225b](https://github.com/dOpensource/dsiprouter/commit/2c80df409e6cb9e0782d6f7544096041ebfa225b)  \n> Date: Thu, 30 Mar 2023 09:34:52 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add flask gui deprecation comments\n- add additional documenting comments\n- silence superfluous output when sources.pref missing\n- fix systemd services bugfix not working\n- fix updating existing key/cert pair fails\n\n\n---\n\n[//]: # (END_SECTION 2c80df409e6cb9e0782d6f7544096041ebfa225b)\n[//]: # (START_SECTION 69b735d9bf020d111d5251e70281767fb4e94faa)\n### Bug Fixes And Cleanup For v0.72\n\n> Commit: [69b735d9bf020d111d5251e70281767fb4e94faa](https://github.com/dOpensource/dsiprouter/commit/69b735d9bf020d111d5251e70281767fb4e94faa)  \n> Date: Wed, 29 Mar 2023 16:02:15 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- allow notification message animation to be interrupted by another request\n- fix carrier defaults misaligned\n- fix msteams gwgroupid confilct\n- fix description field storage requirements for other DB tables\n\n\n---\n\n[//]: # (END_SECTION 69b735d9bf020d111d5251e70281767fb4e94faa)\n[//]: # (START_SECTION 7dbe2558f6c045311ef043f2c5859c711345491b)\n### Update index.rst\n\n> Commit: [7dbe2558f6c045311ef043f2c5859c711345491b](https://github.com/dOpensource/dsiprouter/commit/7dbe2558f6c045311ef043f2c5859c711345491b)  \n> Date: Wed, 29 Mar 2023 14:27:37 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7dbe2558f6c045311ef043f2c5859c711345491b)\n[//]: # (START_SECTION 33c3bee02578082f7c3192d3496b0067af992f9c)\n### Update index.rst\n\n> Commit: [33c3bee02578082f7c3192d3496b0067af992f9c](https://github.com/dOpensource/dsiprouter/commit/33c3bee02578082f7c3192d3496b0067af992f9c)  \n> Date: Wed, 29 Mar 2023 14:26:15 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 33c3bee02578082f7c3192d3496b0067af992f9c)\n[//]: # (START_SECTION f0f02584af775a85e723c6ccdb09db2c6141a145)\n### Update index.rst\n\n> Commit: [f0f02584af775a85e723c6ccdb09db2c6141a145](https://github.com/dOpensource/dsiprouter/commit/f0f02584af775a85e723c6ccdb09db2c6141a145)  \n> Date: Wed, 29 Mar 2023 14:17:44 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f0f02584af775a85e723c6ccdb09db2c6141a145)\n[//]: # (START_SECTION d7f7da68512e560bd9bc5f2dbb35b07e6630b7f9)\n### Stir-Shaken Fixes - Added a missing parameter to the Stir-Shaken outbound logic - Added a script that will generate a self-signed certificate for testing out Stir-Shaken\n\n> Commit: [d7f7da68512e560bd9bc5f2dbb35b07e6630b7f9](https://github.com/dOpensource/dsiprouter/commit/d7f7da68512e560bd9bc5f2dbb35b07e6630b7f9)  \n> Date: Wed, 29 Mar 2023 16:34:06 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d7f7da68512e560bd9bc5f2dbb35b07e6630b7f9)\n[//]: # (START_SECTION eb720e2961d73a826fbd6fae55a0b7e93eef25bf)\n### Updated SQL in Call Detail Records so that it loads properly\n\n> Commit: [eb720e2961d73a826fbd6fae55a0b7e93eef25bf](https://github.com/dOpensource/dsiprouter/commit/eb720e2961d73a826fbd6fae55a0b7e93eef25bf)  \n> Date: Wed, 29 Mar 2023 01:56:45 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eb720e2961d73a826fbd6fae55a0b7e93eef25bf)\n[//]: # (START_SECTION bbcfaeeb0d88324f3dc8a9a85b3875e7ff3e76dd)\n### Bug Fixes And Cleanup For v0.72\n\n> Commit: [bbcfaeeb0d88324f3dc8a9a85b3875e7ff3e76dd](https://github.com/dOpensource/dsiprouter/commit/bbcfaeeb0d88324f3dc8a9a85b3875e7ff3e76dd)  \n> Date: Tue, 28 Mar 2023 15:09:59 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- remove superfluous cache files\n- fix \"nginx not reloaded when default SSL cert updated\"\n- update systemd services to use `dsiprouter chown` for preparation\n- fix `dsiprouter restart -debug` fails when started with `DEBUG=False`\n- remove deprecations noted for removal in v0.72\n\n\n---\n\n[//]: # (END_SECTION bbcfaeeb0d88324f3dc8a9a85b3875e7ff3e76dd)\n[//]: # (START_SECTION 050f5fbb39bdb1e2073fa98fb471acf01777f0f1)\n### Fixed Issue #493\n\n> Commit: [050f5fbb39bdb1e2073fa98fb471acf01777f0f1](https://github.com/dOpensource/dsiprouter/commit/050f5fbb39bdb1e2073fa98fb471acf01777f0f1)  \n> Date: Mon, 27 Mar 2023 17:13:36 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 050f5fbb39bdb1e2073fa98fb471acf01777f0f1)\n[//]: # (START_SECTION 94bfc589ae9777163855e52b511fc90f3f4adf1d)\n### Fixed a bug that prevented domain_attrs from being deleted\n\n> Commit: [94bfc589ae9777163855e52b511fc90f3f4adf1d](https://github.com/dOpensource/dsiprouter/commit/94bfc589ae9777163855e52b511fc90f3f4adf1d)  \n> Date: Sat, 25 Mar 2023 05:46:55 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 94bfc589ae9777163855e52b511fc90f3f4adf1d)\n[//]: # (START_SECTION 13fcb33a140bba7391d9ad8b52a1aa7f25a09955)\n### Fixed the WooCommerce Hashing Len\n\n> Commit: [13fcb33a140bba7391d9ad8b52a1aa7f25a09955](https://github.com/dOpensource/dsiprouter/commit/13fcb33a140bba7391d9ad8b52a1aa7f25a09955)  \n> Date: Fri, 17 Mar 2023 17:27:24 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 13fcb33a140bba7391d9ad8b52a1aa7f25a09955)\n[//]: # (START_SECTION 72cd4261677d9de449577489edb50fafce56b662)\n### Bug Fixes For v0.72\n\n> Commit: [72cd4261677d9de449577489edb50fafce56b662](https://github.com/dOpensource/dsiprouter/commit/72cd4261677d9de449577489edb50fafce56b662)  \n> Date: Tue, 14 Mar 2023 12:08:39 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- msteams address records missing\n- fix typos in dsiprouter.sh\n- fix `FLT_MSTEAMS` value in dsiprouter.sh\n- fix address records tag field too long\n\n\n---\n\n[//]: # (END_SECTION 72cd4261677d9de449577489edb50fafce56b662)\n[//]: # (START_SECTION b7d9a30838189b0e240a590f610b66505d85d0ec)\n### Updated the SQL logic for Inbound Routes to work with the text() function\n\n> Commit: [b7d9a30838189b0e240a590f610b66505d85d0ec](https://github.com/dOpensource/dsiprouter/commit/b7d9a30838189b0e240a590f610b66505d85d0ec)  \n> Date: Tue, 14 Mar 2023 15:05:10 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b7d9a30838189b0e240a590f610b66505d85d0ec)\n[//]: # (START_SECTION 8e19d470d3fff85329e78239c84e54998f39f247)\n### Suggested Changes and Comments\n\n> Commit: [8e19d470d3fff85329e78239c84e54998f39f247](https://github.com/dOpensource/dsiprouter/commit/8e19d470d3fff85329e78239c84e54998f39f247)  \n> Date: Mon, 13 Mar 2023 14:33:44 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 8e19d470d3fff85329e78239c84e54998f39f247)\n[//]: # (START_SECTION a1a55ec01f46433fb7ac64eae58089cc8e506a19)\n### Updated the database logic to use the new SQLAlchemy 2.0 result-object\n\n> Commit: [a1a55ec01f46433fb7ac64eae58089cc8e506a19](https://github.com/dOpensource/dsiprouter/commit/a1a55ec01f46433fb7ac64eae58089cc8e506a19)  \n> Date: Sun, 12 Mar 2023 00:44:26 +0000  \n> Author: root (root@mack.dsiprouter.net)  \n> Committer: root (root@mack.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a1a55ec01f46433fb7ac64eae58089cc8e506a19)\n[//]: # (START_SECTION 5a34993875502fc1cd9f21dec686a3035a8dcb92)\n### Allow Static Networking\n\n> Commit: [5a34993875502fc1cd9f21dec686a3035a8dcb92](https://github.com/dOpensource/dsiprouter/commit/5a34993875502fc1cd9f21dec686a3035a8dcb92)  \n> Date: Tue, 7 Mar 2023 15:39:52 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves [#473](https://github.com/dOpensource/dsiprouter/issues/473)\n\n\n---\n\n[//]: # (END_SECTION 5a34993875502fc1cd9f21dec686a3035a8dcb92)\n[//]: # (START_SECTION 8cac48d464f70bd5da4c8227abfc7025a23a82a0)\n### Resolve Merge Conflicts\n\n> Commit: [8cac48d464f70bd5da4c8227abfc7025a23a82a0](https://github.com/dOpensource/dsiprouter/commit/8cac48d464f70bd5da4c8227abfc7025a23a82a0)  \n> Date: Mon, 6 Mar 2023 15:06:24 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 8cac48d464f70bd5da4c8227abfc7025a23a82a0)\n[//]: # (START_SECTION 63ac499d92fac90f8bb1c3fabc7c03136abd505f)\n### Process Updates\n\n> Commit: [63ac499d92fac90f8bb1c3fabc7c03136abd505f](https://github.com/dOpensource/dsiprouter/commit/63ac499d92fac90f8bb1c3fabc7c03136abd505f)  \n> Date: Fri, 3 Mar 2023 08:24:03 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 63ac499d92fac90f8bb1c3fabc7c03136abd505f)\n[//]: # (START_SECTION 2bba54fc6e7061893b205149d7c9172c97239cbf)\n### Security Updates For WSGI Dependencies\n\n> Commit: [2bba54fc6e7061893b205149d7c9172c97239cbf](https://github.com/dOpensource/dsiprouter/commit/2bba54fc6e7061893b205149d7c9172c97239cbf)  \n> Date: Wed, 1 Mar 2023 14:31:56 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves [#484](https://github.com/dOpensource/dsiprouter/pull/484)\n- remove unused imports\n- update WSGI and Flask dependencies\n\n\n---\n\n[//]: # (END_SECTION 2bba54fc6e7061893b205149d7c9172c97239cbf)\n[//]: # (START_SECTION 50348be3339f4d3f9be532dcd4164964992bdf17)\n### Fix Default Dispatcher Attributes\n\n> Commit: [50348be3339f4d3f9be532dcd4164964992bdf17](https://github.com/dOpensource/dsiprouter/commit/50348be3339f4d3f9be532dcd4164964992bdf17)  \n> Date: Wed, 1 Mar 2023 14:22:23 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves [#468](https://github.com/dOpensource/dsiprouter/issues/468)\n\n\n---\n\n[//]: # (END_SECTION 50348be3339f4d3f9be532dcd4164964992bdf17)\n[//]: # (START_SECTION a1174d1e922ffd85eb767b7047fb84e3e3e0745e)\n### Update Installation Documentation\n\n> Commit: [a1174d1e922ffd85eb767b7047fb84e3e3e0745e](https://github.com/dOpensource/dsiprouter/commit/a1174d1e922ffd85eb767b7047fb84e3e3e0745e)  \n> Date: Wed, 1 Mar 2023 11:59:41 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves [#470](https://github.com/dOpensource/dsiprouter/issues/470)\n\n\n---\n\n[//]: # (END_SECTION a1174d1e922ffd85eb767b7047fb84e3e3e0745e)\n[//]: # (START_SECTION da72adbd503e5fce324c8a801d90c1e0b38ca054)\n### Require Issue Template Usage\n\n> Commit: [da72adbd503e5fce324c8a801d90c1e0b38ca054](https://github.com/dOpensource/dsiprouter/commit/da72adbd503e5fce324c8a801d90c1e0b38ca054)  \n> Date: Wed, 15 Feb 2023 09:50:31 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION da72adbd503e5fce324c8a801d90c1e0b38ca054)\n[//]: # (START_SECTION 2a19a64551c0a191adc60a956111f25b2a6a340c)\n### Update Installation Documentation\n\n> Commit: [2a19a64551c0a191adc60a956111f25b2a6a340c](https://github.com/dOpensource/dsiprouter/commit/2a19a64551c0a191adc60a956111f25b2a6a340c)  \n> Date: Wed, 1 Mar 2023 11:59:41 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves [#470](https://github.com/dOpensource/dsiprouter/issues/470)\n\n\n---\n\n[//]: # (END_SECTION 2a19a64551c0a191adc60a956111f25b2a6a340c)\n[//]: # (START_SECTION 6dfb127bd0e7f504522e80a4ad60ac01118f2c6f)\n### Add PR Template\n\n> Commit: [6dfb127bd0e7f504522e80a4ad60ac01118f2c6f](https://github.com/dOpensource/dsiprouter/commit/6dfb127bd0e7f504522e80a4ad60ac01118f2c6f)  \n> Date: Wed, 15 Feb 2023 11:32:46 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 6dfb127bd0e7f504522e80a4ad60ac01118f2c6f)\n[//]: # (START_SECTION bb58600d9294fbb5ce39c2e273155602d7981638)\n### Require Issue Template Usage\n\n> Commit: [bb58600d9294fbb5ce39c2e273155602d7981638](https://github.com/dOpensource/dsiprouter/commit/bb58600d9294fbb5ce39c2e273155602d7981638)  \n> Date: Wed, 15 Feb 2023 09:50:31 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION bb58600d9294fbb5ce39c2e273155602d7981638)\n[//]: # (START_SECTION cc8a46218083c85894e090228f3cd55eb5a238c7)\n### Update issue templates (#481)\n\n> Commit: [cc8a46218083c85894e090228f3cd55eb5a238c7](https://github.com/dOpensource/dsiprouter/commit/cc8a46218083c85894e090228f3cd55eb5a238c7)  \n> Date: Wed, 15 Feb 2023 09:27:23 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Merging\n\n\n---\n\n[//]: # (END_SECTION cc8a46218083c85894e090228f3cd55eb5a238c7)\n[//]: # (START_SECTION 681c2c432b735a3f67d6732264f50638b47b1552)\n### Update issue templates\n\n> Commit: [681c2c432b735a3f67d6732264f50638b47b1552](https://github.com/dOpensource/dsiprouter/commit/681c2c432b735a3f67d6732264f50638b47b1552)  \n> Date: Wed, 15 Feb 2023 09:26:05 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 681c2c432b735a3f67d6732264f50638b47b1552)\n[//]: # (START_SECTION 799d68b0365eeda8c2d17811575b925a0023ae2a)\n### Merge PR435\n\n> Commit: [799d68b0365eeda8c2d17811575b925a0023ae2a](https://github.com/dOpensource/dsiprouter/commit/799d68b0365eeda8c2d17811575b925a0023ae2a)  \n> Date: Tue, 14 Feb 2023 11:36:04 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- resolves #435\n\n\n---\n\n[//]: # (END_SECTION 799d68b0365eeda8c2d17811575b925a0023ae2a)\n[//]: # (START_SECTION 9a82a5efab8b2d6a3bed6c0611888b0a191119c5)\n### Correction of CDR fields (#465)\n\n> Commit: [9a82a5efab8b2d6a3bed6c0611888b0a191119c5](https://github.com/dOpensource/dsiprouter/commit/9a82a5efab8b2d6a3bed6c0611888b0a191119c5)  \n> Date: Tue, 14 Feb 2023 07:53:03 -0800  \n> Author: VOICE1 (voice1me@gmail.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- * Update cdrs.sql\r\n\n- Adjusted field types to address bug [#464](https://github.com/dOpensource/dsiprouter/issues/464)\r\n\n- ---------\r\n\n- Co-authored-by: Tyler Moore <tmoore@goflyball.com>\n\n\n---\n\n[//]: # (END_SECTION 9a82a5efab8b2d6a3bed6c0611888b0a191119c5)\n[//]: # (START_SECTION e354fbeb2467854770650aec659d98b27dc8d464)\n### Integration Fixes\n\n> Commit: [e354fbeb2467854770650aec659d98b27dc8d464](https://github.com/dOpensource/dsiprouter/commit/e354fbeb2467854770650aec659d98b27dc8d464)  \n> Date: Thu, 9 Feb 2023 11:25:05 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- updated license manager UI table rows to scale properly\n- fix span and button resizing in license manager UI\n- fix redirection links to license manager UI\n- fix typos in domain route\n- fix `dsip_settings.sql` syntax highlighting\n- add deprecation notices where missing\n- fix typoes in `dsiprouter.sh`\n- update default character length in `security.py` to match `dsip_lib.sh`\n- fix typo in `dsip_lib.sh`\n- deprecate dsiprouter module compilation in debian, rhel, and amzn\n\n\n---\n\n[//]: # (END_SECTION e354fbeb2467854770650aec659d98b27dc8d464)\n[//]: # (START_SECTION 98270d267c77f991b3aecf572d38c097fecac68b)\n### Cleanup Branch / Final Fixes\n\n> Commit: [98270d267c77f991b3aecf572d38c097fecac68b](https://github.com/dOpensource/dsiprouter/commit/98270d267c77f991b3aecf572d38c097fecac68b)  \n> Date: Wed, 8 Feb 2023 11:56:33 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix hashing functions in `dsip_lib.sh`\n- fix `homer-id` format (based on machine-id)\n- fix outbound clearip default hostname\n- add WIP PBKDF2 bash-native implementation\n- cleanup branch for merging\n\n\n---\n\n[//]: # (END_SECTION 98270d267c77f991b3aecf572d38c097fecac68b)\n[//]: # (START_SECTION 16a5a16cf1ef8a7aad44ac3d9da1e4c8706fac35)\n### API Updates\n\n> Commit: [16a5a16cf1ef8a7aad44ac3d9da1e4c8706fac35](https://github.com/dOpensource/dsiprouter/commit/16a5a16cf1ef8a7aad44ac3d9da1e4c8706fac35)  \n> Date: Tue, 7 Feb 2023 13:52:19 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 16a5a16cf1ef8a7aad44ac3d9da1e4c8706fac35)\n[//]: # (START_SECTION 4082e01348c3279f71685ff9eeaccaee25a6e53d)\n### Upgrade Checkpoint\n\n> Commit: [4082e01348c3279f71685ff9eeaccaee25a6e53d](https://github.com/dOpensource/dsiprouter/commit/4082e01348c3279f71685ff9eeaccaee25a6e53d)  \n> Date: Tue, 7 Feb 2023 13:06:59 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4082e01348c3279f71685ff9eeaccaee25a6e53d)\n[//]: # (START_SECTION 92d323dc7d6d82645ae7c25a4134679009dba4a6)\n### Update installing.rst\n\n> Commit: [92d323dc7d6d82645ae7c25a4134679009dba4a6](https://github.com/dOpensource/dsiprouter/commit/92d323dc7d6d82645ae7c25a4134679009dba4a6)  \n> Date: Sun, 5 Feb 2023 14:16:38 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 92d323dc7d6d82645ae7c25a4134679009dba4a6)\n[//]: # (START_SECTION 0b3ed50bbf81577558e79b77dd8b2c65f61ad034)\n### Update installing.rst\n\n> Commit: [0b3ed50bbf81577558e79b77dd8b2c65f61ad034](https://github.com/dOpensource/dsiprouter/commit/0b3ed50bbf81577558e79b77dd8b2c65f61ad034)  \n> Date: Sun, 5 Feb 2023 14:10:11 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0b3ed50bbf81577558e79b77dd8b2c65f61ad034)\n[//]: # (START_SECTION 203f8ea5953044b05b047a5565c36c23155cff43)\n### Update installing.rst\n\n> Commit: [203f8ea5953044b05b047a5565c36c23155cff43](https://github.com/dOpensource/dsiprouter/commit/203f8ea5953044b05b047a5565c36c23155cff43)  \n> Date: Sun, 5 Feb 2023 14:09:09 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 203f8ea5953044b05b047a5565c36c23155cff43)\n[//]: # (START_SECTION d2144a88b8ccba4dba32265d69782e65635b4997)\n### Update debian_install.rst\n\n> Commit: [d2144a88b8ccba4dba32265d69782e65635b4997](https://github.com/dOpensource/dsiprouter/commit/d2144a88b8ccba4dba32265d69782e65635b4997)  \n> Date: Sun, 5 Feb 2023 14:04:35 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Removing documentation about Debian 9 and combining the install documentation for Deb 10 and Deb 11\n\n\n---\n\n[//]: # (END_SECTION d2144a88b8ccba4dba32265d69782e65635b4997)\n[//]: # (START_SECTION af39173450198616f9e1f959f233f980a65a4d47)\n### Pinned the version of SQLAlchemy and Requests to 1.4.46 and 2.28.1, respectively\n\n> Commit: [af39173450198616f9e1f959f233f980a65a4d47](https://github.com/dOpensource/dsiprouter/commit/af39173450198616f9e1f959f233f980a65a4d47)  \n> Date: Sun, 5 Feb 2023 18:33:40 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION af39173450198616f9e1f959f233f980a65a4d47)\n[//]: # (START_SECTION 2f31989a6b34be6d213a891c3725dde6c168f2fb)\n### Add Licensing Manager\n\n> Commit: [2f31989a6b34be6d213a891c3725dde6c168f2fb](https://github.com/dOpensource/dsiprouter/commit/2f31989a6b34be6d213a891c3725dde6c168f2fb)  \n> Date: Wed, 1 Feb 2023 15:41:03 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add license management REST api\n- add license management GUI routes\n- update sqlalchemy to version 2.0.0\n- update license checking logic to use new api\n- fix slow reload in systemd services\n- remove deprecated sqlalchemy settings\n- update credential hashing to use standard salt lengths\n- fix edge cases where credential hashing failed\n- add native openssl hashing in bash scripts\n- update all raw sql queries to be parameterized\n- update secrets checking to combat padding attacks\n- add various utility functions\n- update exceptions to show full stack trace by default\n- improve request data handling to handle edge cases\n- update `DSIP_ID` to be machine specific\n- add useful debug output to signal handlers\n- add safeguards against improper app teardown\n- fix missing port and charset in DB connection URI\n\n\n---\n\n[//]: # (END_SECTION 2f31989a6b34be6d213a891c3725dde6c168f2fb)\n[//]: # (START_SECTION 581d8ed65958db142c9c64e3d6f973327a626fcc)\n### Fix Missing Kamailio Config Subst\n\n> Commit: [581d8ed65958db142c9c64e3d6f973327a626fcc](https://github.com/dOpensource/dsiprouter/commit/581d8ed65958db142c9c64e3d6f973327a626fcc)  \n> Date: Sat, 10 Dec 2022 13:17:06 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add missing subst defines to kamailio config\n\n\n---\n\n[//]: # (END_SECTION 581d8ed65958db142c9c64e3d6f973327a626fcc)\n[//]: # (START_SECTION 3b02dc02eeaee8d4e8ca13741489c2e6f72de230)\n### Increase CDR Field Sizes\n\n> Commit: [3b02dc02eeaee8d4e8ca13741489c2e6f72de230](https://github.com/dOpensource/dsiprouter/commit/3b02dc02eeaee8d4e8ca13741489c2e6f72de230)  \n> Date: Sat, 10 Dec 2022 13:12:31 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #472\n- increase varchar sizes to handle edge cases\n\n\n---\n\n[//]: # (END_SECTION 3b02dc02eeaee8d4e8ca13741489c2e6f72de230)\n[//]: # (START_SECTION 63dfa5ae605542cb998a8b03155bdddb75bb1472)\n### Added teh script to generate the self signed cert\n\n> Commit: [63dfa5ae605542cb998a8b03155bdddb75bb1472](https://github.com/dOpensource/dsiprouter/commit/63dfa5ae605542cb998a8b03155bdddb75bb1472)  \n> Date: Thu, 8 Dec 2022 11:53:58 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 63dfa5ae605542cb998a8b03155bdddb75bb1472)\n[//]: # (START_SECTION a787ec5760bf147c755950116a9e440d181a2e9d)\n### Removed Rabbit MQ logic\n\n> Commit: [a787ec5760bf147c755950116a9e440d181a2e9d](https://github.com/dOpensource/dsiprouter/commit/a787ec5760bf147c755950116a9e440d181a2e9d)  \n> Date: Sun, 4 Dec 2022 22:37:46 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a787ec5760bf147c755950116a9e440d181a2e9d)\n[//]: # (START_SECTION 35b4d7233eeda44587c59f0593f5599f82898f65)\n### Added support for spinning up a demo server\n\n> Commit: [35b4d7233eeda44587c59f0593f5599f82898f65](https://github.com/dOpensource/dsiprouter/commit/35b4d7233eeda44587c59f0593f5599f82898f65)  \n> Date: Sun, 4 Dec 2022 22:33:35 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 35b4d7233eeda44587c59f0593f5599f82898f65)\n[//]: # (START_SECTION 1fc9ae71deb1b5191c68a3171bf9b5269e198b80)\n### Added push notification updates\n\n> Commit: [1fc9ae71deb1b5191c68a3171bf9b5269e198b80](https://github.com/dOpensource/dsiprouter/commit/1fc9ae71deb1b5191c68a3171bf9b5269e198b80)  \n> Date: Wed, 30 Nov 2022 09:34:52 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1fc9ae71deb1b5191c68a3171bf9b5269e198b80)\n[//]: # (START_SECTION 200cd34e29349b99b6da38065cee4102e916d98c)\n### Terraform - Added support for adding a hostname record to DNS\n\n> Commit: [200cd34e29349b99b6da38065cee4102e916d98c](https://github.com/dOpensource/dsiprouter/commit/200cd34e29349b99b6da38065cee4102e916d98c)  \n> Date: Fri, 18 Nov 2022 12:03:37 -0600  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 200cd34e29349b99b6da38065cee4102e916d98c)\n[//]: # (START_SECTION cf5c5200f68b97d88ad2dec4331d04db3e189a51)\n### Updated getInternalCIDR with support for DMZ\n\n> Commit: [cf5c5200f68b97d88ad2dec4331d04db3e189a51](https://github.com/dOpensource/dsiprouter/commit/cf5c5200f68b97d88ad2dec4331d04db3e189a51)  \n> Date: Wed, 2 Nov 2022 00:02:26 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cf5c5200f68b97d88ad2dec4331d04db3e189a51)\n[//]: # (START_SECTION ee072c3650e9a008900cf00132804e83af3fbf56)\n### Fixed issues with install scripts\n\n> Commit: [ee072c3650e9a008900cf00132804e83af3fbf56](https://github.com/dOpensource/dsiprouter/commit/ee072c3650e9a008900cf00132804e83af3fbf56)  \n> Date: Tue, 1 Nov 2022 02:28:48 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ee072c3650e9a008900cf00132804e83af3fbf56)\n[//]: # (START_SECTION 90affe1dbac937b1fd09e2d5ba90ae07491948e6)\n### Fixed issues with install scripts\n\n> Commit: [90affe1dbac937b1fd09e2d5ba90ae07491948e6](https://github.com/dOpensource/dsiprouter/commit/90affe1dbac937b1fd09e2d5ba90ae07491948e6)  \n> Date: Tue, 1 Nov 2022 02:27:26 +0000  \n> Author: root (root@mack-dsip-v0.710)  \n> Committer: root (root@mack-dsip-v0.710)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 90affe1dbac937b1fd09e2d5ba90ae07491948e6)\n[//]: # (START_SECTION 0a4cf6a2b0a8352659dfd8145692ed6147ba3694)\n### Fixed issue with Kamailio.cfg\n\n> Commit: [0a4cf6a2b0a8352659dfd8145692ed6147ba3694](https://github.com/dOpensource/dsiprouter/commit/0a4cf6a2b0a8352659dfd8145692ed6147ba3694)  \n> Date: Mon, 31 Oct 2022 23:30:26 +0000  \n> Author: root (root@mack-dsip-v0.710)  \n> Committer: root (root@mack-dsip-v0.710)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0a4cf6a2b0a8352659dfd8145692ed6147ba3694)\n[//]: # (START_SECTION b26b91e04a541685d0d5a78277c4ab7bf564669d)\n### Added DMZ Support\n\n> Commit: [b26b91e04a541685d0d5a78277c4ab7bf564669d](https://github.com/dOpensource/dsiprouter/commit/b26b91e04a541685d0d5a78277c4ab7bf564669d)  \n> Date: Mon, 31 Oct 2022 23:03:33 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b26b91e04a541685d0d5a78277c4ab7bf564669d)\n[//]: # (START_SECTION f321abf3809cc8b6696be3b6423a073a76f81365)\n### Updated Pass-Thru Authentication - Added support to handle pass-thru when the hostname of the media server is used as the endpoint.\n\n> Commit: [f321abf3809cc8b6696be3b6423a073a76f81365](https://github.com/dOpensource/dsiprouter/commit/f321abf3809cc8b6696be3b6423a073a76f81365)  \n> Date: Tue, 18 Oct 2022 10:38:56 +0000  \n> Author: Mack (mack@dopensource.com)  \n> Committer: Mack (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f321abf3809cc8b6696be3b6423a073a76f81365)\n[//]: # (START_SECTION 84bd75cd3f05b01b765ad8be4d4a096681ac681d)\n### Fixed issue with ACK on Domain Routing: - Changed VALIDATE_ROUTE_HEADERS to CUSTOM_WITHINDLG - Changed logic to not remove the first Route header - this causes loose_route to functionaly incorrectly\n\n> Commit: [84bd75cd3f05b01b765ad8be4d4a096681ac681d](https://github.com/dOpensource/dsiprouter/commit/84bd75cd3f05b01b765ad8be4d4a096681ac681d)  \n> Date: Tue, 11 Oct 2022 04:06:58 +0000  \n> Author: root (root@dsiptest.dsiprouter.net)  \n> Committer: root (root@dsiptest.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 84bd75cd3f05b01b765ad8be4d4a096681ac681d)\n[//]: # (START_SECTION 4452960d95d0bf8e6c9e9c8d60b51b376f6551d9)\n### Added a sleep after the update to give apt time to release the locks\n\n> Commit: [4452960d95d0bf8e6c9e9c8d60b51b376f6551d9](https://github.com/dOpensource/dsiprouter/commit/4452960d95d0bf8e6c9e9c8d60b51b376f6551d9)  \n> Date: Tue, 4 Oct 2022 08:02:09 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4452960d95d0bf8e6c9e9c8d60b51b376f6551d9)\n[//]: # (START_SECTION 3341101669b42a7341643d13c284f696da4a0caf)\n### updated the install files to create the directory for serving up manual certificates\n\n> Commit: [3341101669b42a7341643d13c284f696da4a0caf](https://github.com/dOpensource/dsiprouter/commit/3341101669b42a7341643d13c284f696da4a0caf)  \n> Date: Tue, 4 Oct 2022 07:26:45 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3341101669b42a7341643d13c284f696da4a0caf)\n[//]: # (START_SECTION f53bfa21e447f53f22ff20a2a64b7b859835e8e6)\n### Fix Failover Routing\n\n> Commit: [f53bfa21e447f53f22ff20a2a64b7b859835e8e6](https://github.com/dOpensource/dsiprouter/commit/f53bfa21e447f53f22ff20a2a64b7b859835e8e6)  \n> Date: Fri, 30 Sep 2022 10:49:17 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- change sort algorithm for drouting\n- fix call limit out of scope error\n- fix failover routing on no reply\n- fix failover routing on dropped packets\n- update failover timeouts to be more realistic\n- fix route header variable out of scope error\n- make debugger more readable in debug mode\n- fix usrloc variable typo\n\n\n---\n\n[//]: # (END_SECTION f53bfa21e447f53f22ff20a2a64b7b859835e8e6)\n[//]: # (START_SECTION 591aeeb225c1f542bb49a078d6353a7083ee8fc5)\n### FusionPBX Sync Fixes: - Added logic to exclude the default FusionPBX domain from being added during sync's.  This fixes an ACK loop - Deprecated logic for using the Docker instance of Nginx\n\n> Commit: [591aeeb225c1f542bb49a078d6353a7083ee8fc5](https://github.com/dOpensource/dsiprouter/commit/591aeeb225c1f542bb49a078d6353a7083ee8fc5)  \n> Date: Fri, 30 Sep 2022 11:03:48 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 591aeeb225c1f542bb49a078d6353a7083ee8fc5)\n[//]: # (START_SECTION c1e0be12cf158786064bc1f1d1c4bdf3db2159f3)\n### Installation Bug Fixes\n\n> Commit: [c1e0be12cf158786064bc1f1d1c4bdf3db2159f3](https://github.com/dOpensource/dsiprouter/commit/c1e0be12cf158786064bc1f1d1c4bdf3db2159f3)  \n> Date: Thu, 29 Sep 2022 09:37:20 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix unpatched `tls.reload` commands\n- fix typo in `reconfigureMysqlSystemdService()`\n- fix cloud-init edge case where DB passwords not updated\n\n\n---\n\n[//]: # (END_SECTION c1e0be12cf158786064bc1f1d1c4bdf3db2159f3)\n[//]: # (START_SECTION b3a9b851f5b0c095eede8d5913f102b4f62b04f0)\n### Installation Bug Fixes\n\n> Commit: [b3a9b851f5b0c095eede8d5913f102b4f62b04f0](https://github.com/dOpensource/dsiprouter/commit/b3a9b851f5b0c095eede8d5913f102b4f62b04f0)  \n> Date: Wed, 28 Sep 2022 14:31:09 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix credential setting functions edge case not setting DB pass\n- add workaround for TLS module not reloading\n\n\n---\n\n[//]: # (END_SECTION b3a9b851f5b0c095eede8d5913f102b4f62b04f0)\n[//]: # (START_SECTION cdd8aac9f751a212fcca7f01e7a5159d6ab749f2)\n### Installation Bug Fixes\n\n> Commit: [cdd8aac9f751a212fcca7f01e7a5159d6ab749f2](https://github.com/dOpensource/dsiprouter/commit/cdd8aac9f751a212fcca7f01e7a5159d6ab749f2)  \n> Date: Wed, 28 Sep 2022 08:21:37 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix openssl version conflict on amazon linux\n- fix openssl missing rnd file on amazon linux\n- fix missing wheel package on amazon linux\n\n\n---\n\n[//]: # (END_SECTION cdd8aac9f751a212fcca7f01e7a5159d6ab749f2)\n[//]: # (START_SECTION a1cba27cee11cd0de0a46ebec89c65eeb86a302a)\n### Installation Bug Fixes\n\n> Commit: [a1cba27cee11cd0de0a46ebec89c65eeb86a302a](https://github.com/dOpensource/dsiprouter/commit/a1cba27cee11cd0de0a46ebec89c65eeb86a302a)  \n> Date: Tue, 27 Sep 2022 15:45:14 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix permissions errors on install\n- silence false positive error crontab missing\n- fix ipv6 connection check for older iproute versions\n- fix systemd service formats for debian9\n- fix missing uuid module for amazon linux 2\n- fix kamailio TLS configurations\n- fix dupliacte init commands when re-running install\n- fix kamailio service for old systemd versions\n\n\n---\n\n[//]: # (END_SECTION a1cba27cee11cd0de0a46ebec89c65eeb86a302a)\n[//]: # (START_SECTION c3fa6d356292be28d495bdd1a32973880bb31068)\n### dSIPRouter Provisioning: - Changed the provisioning template to run on port 443 and SSL by default - Supports /provision and /app/provision URLs for obtaining provisioning profiles\n\n> Commit: [c3fa6d356292be28d495bdd1a32973880bb31068](https://github.com/dOpensource/dsiprouter/commit/c3fa6d356292be28d495bdd1a32973880bb31068)  \n> Date: Tue, 27 Sep 2022 14:05:41 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c3fa6d356292be28d495bdd1a32973880bb31068)\n[//]: # (START_SECTION b3f6d0264ce15c28953af1f995738d0d4b335f8b)\n### FusionPBX Sync: - Nginx will now do a reload when FusionPBX Sync is enabled vs a restart\n\n> Commit: [b3f6d0264ce15c28953af1f995738d0d4b335f8b](https://github.com/dOpensource/dsiprouter/commit/b3f6d0264ce15c28953af1f995738d0d4b335f8b)  \n> Date: Tue, 27 Sep 2022 10:24:38 +0000  \n> Author: root (root@mack-dsip-v0.700)  \n> Committer: root (root@mack-dsip-v0.700)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b3f6d0264ce15c28953af1f995738d0d4b335f8b)\n[//]: # (START_SECTION 3238a10fb81d182766c38caaee646e3f79e98e71)\n### Misc Bug Fixes for v0.70\n\n> Commit: [3238a10fb81d182766c38caaee646e3f79e98e71](https://github.com/dOpensource/dsiprouter/commit/3238a10fb81d182766c38caaee646e3f79e98e71)  \n> Date: Fri, 23 Sep 2022 15:05:43 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix missing update for INTERNAL_FQDN in `kamailio.cfg`\n- fix formatting in `kamailio.cfg`\n\n\n---\n\n[//]: # (END_SECTION 3238a10fb81d182766c38caaee646e3f79e98e71)\n[//]: # (START_SECTION a8185ebd21e1e7b26aeadda1e39c36c8d9278052)\n### v0.70 Misc Fixes\n\n> Commit: [a8185ebd21e1e7b26aeadda1e39c36c8d9278052](https://github.com/dOpensource/dsiprouter/commit/a8185ebd21e1e7b26aeadda1e39c36c8d9278052)  \n> Date: Fri, 23 Sep 2022 10:59:04 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix typo in `dsiprouter.py`\n- update kamailio log configs to use a standard prefix\n\n\n---\n\n[//]: # (END_SECTION a8185ebd21e1e7b26aeadda1e39c36c8d9278052)\n[//]: # (START_SECTION 858d692c20fb15b7e897c58e428ed094423a136a)\n### Update Precommit Checks\n\n> Commit: [858d692c20fb15b7e897c58e428ed094423a136a](https://github.com/dOpensource/dsiprouter/commit/858d692c20fb15b7e897c58e428ed094423a136a)  \n> Date: Wed, 14 Sep 2022 20:25:21 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update syntax checker to add more use cases\n- fix bug in `dsip_lib.sh` set* regex functions\n\n\n---\n\n[//]: # (END_SECTION 858d692c20fb15b7e897c58e428ed094423a136a)\n[//]: # (START_SECTION 53d99e58631b8304aeb76ca3e0b62143dd445f68)\n### Updated CDR's: - Added support for nonCompletedCalls to the API - Exposed the Data Time Filter (dtfilter)  to the CDR API - Fixed the csv option so that empty CDR's doesn't cause an error\n\n> Commit: [53d99e58631b8304aeb76ca3e0b62143dd445f68](https://github.com/dOpensource/dsiprouter/commit/53d99e58631b8304aeb76ca3e0b62143dd445f68)  \n> Date: Wed, 14 Sep 2022 13:20:22 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 53d99e58631b8304aeb76ca3e0b62143dd445f68)\n[//]: # (START_SECTION 224667b7be2a2416e73475803fd164f99d1081da)\n### Merge Homer Support\n\n> Commit: [224667b7be2a2416e73475803fd164f99d1081da](https://github.com/dOpensource/dsiprouter/commit/224667b7be2a2416e73475803fd164f99d1081da)  \n> Date: Tue, 13 Sep 2022 15:28:04 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves [#456](https://github.com/dOpensource/dsiprouter/issues/456)\n- merge in Homer support from v0.65\n- move rtpengine configs into seperate dir\n- add functions for managing rtpengine config to `dsip_lib.sh`\n- update `dsip_settings` table columns\n- move sections of `settings.py` to their proper locations\n- add / cleanup comments\n\n\n---\n\n[//]: # (END_SECTION 224667b7be2a2416e73475803fd164f99d1081da)\n[//]: # (START_SECTION 6721e7478d45d27d4af231c34c270a87f6d98b75)\n### Fix DMQ Error Messages When Not Enabled\n\n> Commit: [6721e7478d45d27d4af231c34c270a87f6d98b75](https://github.com/dOpensource/dsiprouter/commit/6721e7478d45d27d4af231c34c270a87f6d98b75)  \n> Date: Tue, 13 Sep 2022 11:07:23 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves [#454](https://github.com/dOpensource/dsiprouter/issues/454)\n- ensure dmq relication option is disabled on htables when `WITH_DMQ` is disabled\n\n\n---\n\n[//]: # (END_SECTION 6721e7478d45d27d4af231c34c270a87f6d98b75)\n[//]: # (START_SECTION f2a5fd1d4363af84a8bbe880bc807563bc00ca0a)\n### Update CLI Documentation\n\n> Commit: [f2a5fd1d4363af84a8bbe880bc807563bc00ca0a](https://github.com/dOpensource/dsiprouter/commit/f2a5fd1d4363af84a8bbe880bc807563bc00ca0a)  \n> Date: Mon, 12 Sep 2022 21:00:09 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #452\n- update CLI documentation for current cmds/options\n- structured/re-usable cmdline documentation file to come in next release\n\n\n---\n\n[//]: # (END_SECTION f2a5fd1d4363af84a8bbe880bc807563bc00ca0a)\n[//]: # (START_SECTION c854fb813aabca89cd859016f9b6c9d24d2988f4)\n### FusionPBX Edge Case: - An Endpoing Group with FusionPBX support enabled can now be deleted properly if the FusionPBX setup doesn't complete properly during setup\n\n> Commit: [c854fb813aabca89cd859016f9b6c9d24d2988f4](https://github.com/dOpensource/dsiprouter/commit/c854fb813aabca89cd859016f9b6c9d24d2988f4)  \n> Date: Sun, 11 Sep 2022 20:02:43 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c854fb813aabca89cd859016f9b6c9d24d2988f4)\n[//]: # (START_SECTION 7087d856b39143ba0c9ed14b4ce5c199152a9687)\n### CDR Updates: - Fixed #312 - Fixed #387 - Added a CDR refresh button on the CDR page - The default soft order is based on the Call DateTime in descending order\n\n> Commit: [7087d856b39143ba0c9ed14b4ce5c199152a9687](https://github.com/dOpensource/dsiprouter/commit/7087d856b39143ba0c9ed14b4ce5c199152a9687)  \n> Date: Sun, 11 Sep 2022 01:52:04 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7087d856b39143ba0c9ed14b4ce5c199152a9687)\n[//]: # (START_SECTION 6e25bc7cd582d62cfa688c2203be577804624435)\n### Removed unused imports that were preventing the app from starting\n\n> Commit: [6e25bc7cd582d62cfa688c2203be577804624435](https://github.com/dOpensource/dsiprouter/commit/6e25bc7cd582d62cfa688c2203be577804624435)  \n> Date: Fri, 9 Sep 2022 22:14:33 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6e25bc7cd582d62cfa688c2203be577804624435)\n[//]: # (START_SECTION bfdc34035cb07a2272672d964b855c64b81a63c7)\n### Update dthe file with the latest code\n\n> Commit: [bfdc34035cb07a2272672d964b855c64b81a63c7](https://github.com/dOpensource/dsiprouter/commit/bfdc34035cb07a2272672d964b855c64b81a63c7)  \n> Date: Fri, 9 Sep 2022 21:40:14 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bfdc34035cb07a2272672d964b855c64b81a63c7)\n[//]: # (START_SECTION c121e97d6aba9f566a53f017f8695903bee03556)\n### updating the routes file with the latest code\n\n> Commit: [c121e97d6aba9f566a53f017f8695903bee03556](https://github.com/dOpensource/dsiprouter/commit/c121e97d6aba9f566a53f017f8695903bee03556)  \n> Date: Fri, 9 Sep 2022 21:12:05 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c121e97d6aba9f566a53f017f8695903bee03556)\n[//]: # (START_SECTION a1738694639365fc09a7f4c49ea4f94bb45d5d2a)\n### Reverted Accidental changes to main terraform file\n\n> Commit: [a1738694639365fc09a7f4c49ea4f94bb45d5d2a](https://github.com/dOpensource/dsiprouter/commit/a1738694639365fc09a7f4c49ea4f94bb45d5d2a)  \n> Date: Fri, 9 Sep 2022 18:08:52 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a1738694639365fc09a7f4c49ea4f94bb45d5d2a)\n[//]: # (START_SECTION fff6275b60335280698dbb6a609a1ab34229542e)\n### Added extra logic to handle inbound calls with no identity header\n\n> Commit: [fff6275b60335280698dbb6a609a1ab34229542e](https://github.com/dOpensource/dsiprouter/commit/fff6275b60335280698dbb6a609a1ab34229542e)  \n> Date: Fri, 9 Sep 2022 10:50:24 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fff6275b60335280698dbb6a609a1ab34229542e)\n[//]: # (START_SECTION 2cec15e1f3d8d856511225cbd305a35ffc7d33ba)\n### Add Instance Build Script for Cloud Installs\n\n> Commit: [2cec15e1f3d8d856511225cbd305a35ffc7d33ba](https://github.com/dOpensource/dsiprouter/commit/2cec15e1f3d8d856511225cbd305a35ffc7d33ba)  \n> Date: Wed, 7 Sep 2022 12:38:33 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- added build script for instances making automation easier to script\n\n\n---\n\n[//]: # (END_SECTION 2cec15e1f3d8d856511225cbd305a35ffc7d33ba)\n[//]: # (START_SECTION 921d13593df8d28e981991d9a5e4aabe5ca0fffa)\n### Update Image Build Logic\n\n> Commit: [921d13593df8d28e981991d9a5e4aabe5ca0fffa](https://github.com/dOpensource/dsiprouter/commit/921d13593df8d28e981991d9a5e4aabe5ca0fffa)  \n> Date: Wed, 7 Sep 2022 08:46:38 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- remove check for cloud-init completion allowing script execution from cloud-init\n- remove cloud-init cleanup logic to allow cloud-init to finish proper boot\n\n\n---\n\n[//]: # (END_SECTION 921d13593df8d28e981991d9a5e4aabe5ca0fffa)\n[//]: # (START_SECTION c5e6aa71ce58895c10091db1c7acfac7bb53cdef)\n### FusionPBX Provisioning Update: - Added support for proxying phone provisioning profiles from FusionPBX using the native Nginx server\n\n> Commit: [c5e6aa71ce58895c10091db1c7acfac7bb53cdef](https://github.com/dOpensource/dsiprouter/commit/c5e6aa71ce58895c10091db1c7acfac7bb53cdef)  \n> Date: Wed, 7 Sep 2022 11:59:57 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c5e6aa71ce58895c10091db1c7acfac7bb53cdef)\n[//]: # (START_SECTION 273915422552702005f6ad070f3c726ef4836335)\n### Fix Default Network Sync Settings\n\n> Commit: [273915422552702005f6ad070f3c726ef4836335](https://github.com/dOpensource/dsiprouter/commit/273915422552702005f6ad070f3c726ef4836335)  \n> Date: Fri, 2 Sep 2022 11:22:52 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- set network settings to empty string when resolution fails\n- make teardown code more reliable\n- fix sqlalchemy debug settings not being set\n\n\n---\n\n[//]: # (END_SECTION 273915422552702005f6ad070f3c726ef4836335)\n[//]: # (START_SECTION 0b07c0332e0153766bc273f29979cf2beec337e8)\n### IPv6 Serverside NAT Features\n\n> Commit: [0b07c0332e0153766bc273f29979cf2beec337e8](https://github.com/dOpensource/dsiprouter/commit/0b07c0332e0153766bc273f29979cf2beec337e8)  \n> Date: Thu, 1 Sep 2022 14:34:48 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add support for isolated enablement of serverside NAT on ipv4 or ipv6\n- add support for rtpengine interface switching on ipv6 w/ or w/o serverside NAT\n- fix a few typos\n- remove `-servernat` option, serverside NAT is now automatically detected/configured\n\n\n---\n\n[//]: # (END_SECTION 0b07c0332e0153766bc273f29979cf2beec337e8)\n[//]: # (START_SECTION ae07741723edb6b01165ece997a1da2d172fb50e)\n### Updated module to use new dSIP networking library\n\n> Commit: [ae07741723edb6b01165ece997a1da2d172fb50e](https://github.com/dOpensource/dsiprouter/commit/ae07741723edb6b01165ece997a1da2d172fb50e)  \n> Date: Wed, 31 Aug 2022 01:10:09 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ae07741723edb6b01165ece997a1da2d172fb50e)\n[//]: # (START_SECTION 3e684eef5ec045020461fbafe4ce58245a096de2)\n### Replace main.tf after removing it by accident\n\n> Commit: [3e684eef5ec045020461fbafe4ce58245a096de2](https://github.com/dOpensource/dsiprouter/commit/3e684eef5ec045020461fbafe4ce58245a096de2)  \n> Date: Tue, 30 Aug 2022 19:32:08 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3e684eef5ec045020461fbafe4ce58245a096de2)\n[//]: # (START_SECTION bab65db299f3412abc5fa28b2dffc16aff574c55)\n### User API Checkpoint\n\n> Commit: [bab65db299f3412abc5fa28b2dffc16aff574c55](https://github.com/dOpensource/dsiprouter/commit/bab65db299f3412abc5fa28b2dffc16aff574c55)  \n> Date: Tue, 30 Aug 2022 19:25:40 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bab65db299f3412abc5fa28b2dffc16aff574c55)\n[//]: # (START_SECTION c0478fe2ab8142e746a703956b89a3f5ec69f524)\n### Fix IPv6 Enabled Check\n\n> Commit: [c0478fe2ab8142e746a703956b89a3f5ec69f524](https://github.com/dOpensource/dsiprouter/commit/c0478fe2ab8142e746a703956b89a3f5ec69f524)  \n> Date: Tue, 30 Aug 2022 08:28:45 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add check for routable IPv6 address before enabling IPv6 in kamailio\n\n\n---\n\n[//]: # (END_SECTION c0478fe2ab8142e746a703956b89a3f5ec69f524)\n[//]: # (START_SECTION fe84f3a2e63bc5190493b469fa2524ddbc585cb7)\n### General Bug Fixes for Release\n\n> Commit: [fe84f3a2e63bc5190493b469fa2524ddbc585cb7](https://github.com/dOpensource/dsiprouter/commit/fe84f3a2e63bc5190493b469fa2524ddbc585cb7)  \n> Date: Mon, 29 Aug 2022 13:14:19 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix IPv6 address resolution issues on AWS\n- fix IPv6 TLS conditional configuration\n- add conditional checks for IPv6 logic\n- fix missing cloud platform in `dsip_settings` table constraint\n- update STIR/SHAKEN configs to utilize new flags\n- add `IPV6_ENABLED` setting to DB and python settings\n- reset defaults and remove unwanted testing variables\n- fix SSH banner logic\n- fix FQDN aliasing in kamailio config\n\n\n---\n\n[//]: # (END_SECTION fe84f3a2e63bc5190493b469fa2524ddbc585cb7)\n[//]: # (START_SECTION a1d72b29d902e17d92a80db3eeb00b7d0e2d7123)\n### Disabled IPV6 and TLS IPV6\n\n> Commit: [a1d72b29d902e17d92a80db3eeb00b7d0e2d7123](https://github.com/dOpensource/dsiprouter/commit/a1d72b29d902e17d92a80db3eeb00b7d0e2d7123)  \n> Date: Sun, 28 Aug 2022 23:28:40 +0000  \n> Author: root (root@dev-070-dsip-v0.700)  \n> Committer: root (root@dev-070-dsip-v0.700)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a1d72b29d902e17d92a80db3eeb00b7d0e2d7123)\n[//]: # (START_SECTION 1c4ffe41812941046a1cf1c1cb869b5021273c88)\n### Pushed a workaround to disable IPV6 support\n\n> Commit: [1c4ffe41812941046a1cf1c1cb869b5021273c88](https://github.com/dOpensource/dsiprouter/commit/1c4ffe41812941046a1cf1c1cb869b5021273c88)  \n> Date: Sun, 28 Aug 2022 23:02:59 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1c4ffe41812941046a1cf1c1cb869b5021273c88)\n[//]: # (START_SECTION 94b70f189ccb0199130e2abfc9a87cf9c9b193a9)\n### Added IPV6 Enabled Logic to Kamailio\n\n> Commit: [94b70f189ccb0199130e2abfc9a87cf9c9b193a9](https://github.com/dOpensource/dsiprouter/commit/94b70f189ccb0199130e2abfc9a87cf9c9b193a9)  \n> Date: Sun, 28 Aug 2022 20:06:57 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 94b70f189ccb0199130e2abfc9a87cf9c9b193a9)\n[//]: # (START_SECTION f1bd6c453827140e70170b7cd86de21bf5e8aeed)\n### IPV6 Installer Detection: --Added logic to test if traffic can be routed to an IPV6 address - if not disables it\n\n> Commit: [f1bd6c453827140e70170b7cd86de21bf5e8aeed](https://github.com/dOpensource/dsiprouter/commit/f1bd6c453827140e70170b7cd86de21bf5e8aeed)  \n> Date: Sun, 28 Aug 2022 18:20:11 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f1bd6c453827140e70170b7cd86de21bf5e8aeed)\n[//]: # (START_SECTION fe91ef431b330c83f063b8251cbd3fc0bcbb7d4a)\n### Fix Refactoring Bugs\n\n> Commit: [fe91ef431b330c83f063b8251cbd3fc0bcbb7d4a](https://github.com/dOpensource/dsiprouter/commit/fe91ef431b330c83f063b8251cbd3fc0bcbb7d4a)  \n> Date: Fri, 26 Aug 2022 13:32:13 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix a few bugs from code refactoring in [287e9f6](https://github.com/dOpensource/dsiprouter/commit/287e9f6)\n\n\n---\n\n[//]: # (END_SECTION fe91ef431b330c83f063b8251cbd3fc0bcbb7d4a)\n[//]: # (START_SECTION 287e9f6759b3d18f48dbf6ca170fa48be2b00c82)\n### Enhanced IPv6 Support\n\n> Commit: [287e9f6759b3d18f48dbf6ca170fa48be2b00c82](https://github.com/dOpensource/dsiprouter/commit/287e9f6759b3d18f48dbf6ca170fa48be2b00c82)  \n> Date: Fri, 26 Aug 2022 11:22:54 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves [#192](https://github.com/dOpensource/dsiprouter/issues/192)\n- add support for ipv6 routing\n- add support for ipv6 NAT/SERVERNAT environments\n- add support for TLS over ipv6\n- add support for ipv4 <-> ipv6 translation and routing\n- add support for forwarding media over ipv6\n- add support for transcoding over ipv6\n- cleanup kamailio configs and bad formatting in code\n- fix ifdef checks in pre-commit hook\n- add some commenting to areas of codebase\n- add/move networking utility functions\n- refactor mulitiprocessing namespace\n- fix bug in `security.py` preventing credentials being set in some edge cases\n- update networking functions in `dsip_lib.sh`\n- update GUI auth entry functions to use fqdn instead of external ipv4 address\n- add support for ipv6 addresses in domain entries from GUI\n- update signal handling logic to handle SIGINT and SIGQUIT\n\n\n---\n\n[//]: # (END_SECTION 287e9f6759b3d18f48dbf6ca170fa48be2b00c82)\n[//]: # (START_SECTION 460f0c26cd2bb10b4e44f1f62024525d7b1eadf6)\n### Update Credential Setting SQL Statements\n\n> Commit: [460f0c26cd2bb10b4e44f1f62024525d7b1eadf6](https://github.com/dOpensource/dsiprouter/commit/460f0c26cd2bb10b4e44f1f62024525d7b1eadf6)  \n> Date: Tue, 23 Aug 2022 12:51:28 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update SQL statements in `setCredentials()` to use the newer API\n- fix bug in password reset preventing cloud instance pw reset\n- fix rtpengine kernel module loaded check on install\n\n\n---\n\n[//]: # (END_SECTION 460f0c26cd2bb10b4e44f1f62024525d7b1eadf6)\n[//]: # (START_SECTION bb710c9d8412c556055e4f49a406bab60142dee9)\n### Fix Amazon Linux Cloud Image Password Reset\n\n> Commit: [bb710c9d8412c556055e4f49a406bab60142dee9](https://github.com/dOpensource/dsiprouter/commit/bb710c9d8412c556055e4f49a406bab60142dee9)  \n> Date: Mon, 22 Aug 2022 15:53:05 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix a bug where cloud-init would hang on instance first boot\n- add `-q|--quiet` option to `resetpassword` dsiprouter command\n- make cloud instance initial password reset silent\n- refactor IPC function `sendSyncSettingsSignal()`\n- refactor code from `resetPassword()` into `setCredentials()` function\n\n\n---\n\n[//]: # (END_SECTION bb710c9d8412c556055e4f49a406bab60142dee9)\n[//]: # (START_SECTION 93ae11d1afb8dc6b94739a56bcf278ae3ba8c112)\n### Fix Cloud Image Password Reset\n\n> Commit: [93ae11d1afb8dc6b94739a56bcf278ae3ba8c112](https://github.com/dOpensource/dsiprouter/commit/93ae11d1afb8dc6b94739a56bcf278ae3ba8c112)  \n> Date: Fri, 19 Aug 2022 17:20:48 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- make password resetting more reliable by hooking into cloud-init\n\n\n---\n\n[//]: # (END_SECTION 93ae11d1afb8dc6b94739a56bcf278ae3ba8c112)\n[//]: # (START_SECTION d5b61a29a7bf53a7ad91810072e4737d8939c642)\n### Added back in the logic to block incoming calls if the flag is set\n\n> Commit: [d5b61a29a7bf53a7ad91810072e4737d8939c642](https://github.com/dOpensource/dsiprouter/commit/d5b61a29a7bf53a7ad91810072e4737d8939c642)  \n> Date: Tue, 16 Aug 2022 22:12:48 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d5b61a29a7bf53a7ad91810072e4737d8939c642)\n[//]: # (START_SECTION 3323aabfeea77579cf71ce92bc6e615332e5390b)\n### Reload the Key Path from the UI as Well\n\n> Commit: [3323aabfeea77579cf71ce92bc6e615332e5390b](https://github.com/dOpensource/dsiprouter/commit/3323aabfeea77579cf71ce92bc6e615332e5390b)  \n> Date: Tue, 16 Aug 2022 21:50:14 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3323aabfeea77579cf71ce92bc6e615332e5390b)\n[//]: # (START_SECTION d32faad6427e6fe36d3f643fdf8585d01f6452ea)\n### Used the specified signing key to sign the outbound calls\n\n> Commit: [d32faad6427e6fe36d3f643fdf8585d01f6452ea](https://github.com/dOpensource/dsiprouter/commit/d32faad6427e6fe36d3f643fdf8585d01f6452ea)  \n> Date: Tue, 16 Aug 2022 21:19:43 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d32faad6427e6fe36d3f643fdf8585d01f6452ea)\n[//]: # (START_SECTION c028ac3999a88df3c2c62a342e1918f31ea9f0b8)\n### Added an entry for the certificate key path\n\n> Commit: [c028ac3999a88df3c2c62a342e1918f31ea9f0b8](https://github.com/dOpensource/dsiprouter/commit/c028ac3999a88df3c2c62a342e1918f31ea9f0b8)  \n> Date: Tue, 16 Aug 2022 20:33:30 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c028ac3999a88df3c2c62a342e1918f31ea9f0b8)\n[//]: # (START_SECTION 6b1eec58bb6b77641f782b5414f0ba2980fb4317)\n### update kamalio log messages\n\n> Commit: [6b1eec58bb6b77641f782b5414f0ba2980fb4317](https://github.com/dOpensource/dsiprouter/commit/6b1eec58bb6b77641f782b5414f0ba2980fb4317)  \n> Date: Tue, 16 Aug 2022 19:11:12 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6b1eec58bb6b77641f782b5414f0ba2980fb4317)\n[//]: # (START_SECTION 6fa0a5b3ae7d5ac1d267cfbb7937a283ec3c3598)\n### Improve RTPEngine Kernel Module Compilation\n\n> Commit: [6fa0a5b3ae7d5ac1d267cfbb7937a283ec3c3598](https://github.com/dOpensource/dsiprouter/commit/6fa0a5b3ae7d5ac1d267cfbb7937a283ec3c3598)  \n> Date: Tue, 16 Aug 2022 13:24:51 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- installer will now compile `xt_RTPENGINE.ko` for all installed kernels\n\n\n---\n\n[//]: # (END_SECTION 6fa0a5b3ae7d5ac1d267cfbb7937a283ec3c3598)\n[//]: # (START_SECTION e61d6d6e9ff125381d4813a5db0782eff5066e66)\n### Fix Password Resetting Bug\n\n> Commit: [e61d6d6e9ff125381d4813a5db0782eff5066e66](https://github.com/dOpensource/dsiprouter/commit/e61d6d6e9ff125381d4813a5db0782eff5066e66)  \n> Date: Tue, 16 Aug 2022 10:50:45 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fixed bug where `resetpassword` would not default to gui password with `-fid` option\n\n\n---\n\n[//]: # (END_SECTION e61d6d6e9ff125381d4813a5db0782eff5066e66)\n[//]: # (START_SECTION 466bdfbc9a916580d2e70950af6fb54ef4cf8bb5)\n### Fix Cloud Platform Password Reset Boot Hang\n\n> Commit: [466bdfbc9a916580d2e70950af6fb54ef4cf8bb5](https://github.com/dOpensource/dsiprouter/commit/466bdfbc9a916580d2e70950af6fb54ef4cf8bb5)  \n> Date: Tue, 16 Aug 2022 08:51:46 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- reverted password resetting for cloud platforms to use cron instead of systemd\n\n\n---\n\n[//]: # (END_SECTION 466bdfbc9a916580d2e70950af6fb54ef4cf8bb5)\n[//]: # (START_SECTION b5e8df8a4d4b82994d6be2b6a0dbbd26dcb34e5a)\n### Cloud Deployment Fixes\n\n> Commit: [b5e8df8a4d4b82994d6be2b6a0dbbd26dcb34e5a](https://github.com/dOpensource/dsiprouter/commit/b5e8df8a4d4b82994d6be2b6a0dbbd26dcb34e5a)  \n> Date: Mon, 15 Aug 2022 17:49:54 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fixed dependency ordering issue on rhel-based distro's\n- fixed bug in current version of debian-based `cloud-init.service`\n- updated `pre-snapshot.sh` to be slightly faster\n- fixed dnsmasq/cloud-init host resolution conflicts\n- fixed dhclient/cloud-init DNS resolution conflicts\n- standardized `kamailio.service` unit file\n- added STIR/SHAKEN support in debian10/debian9 installation scripts\n\n\n---\n\n[//]: # (END_SECTION b5e8df8a4d4b82994d6be2b6a0dbbd26dcb34e5a)\n[//]: # (START_SECTION 431f37ffd6cb1daba38e2a6f654da35dbd81c3a2)\n### Fix Ordering of Default Carriers\n\n> Commit: [431f37ffd6cb1daba38e2a6f654da35dbd81c3a2](https://github.com/dOpensource/dsiprouter/commit/431f37ffd6cb1daba38e2a6f654da35dbd81c3a2)  \n> Date: Thu, 11 Aug 2022 13:37:14 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 431f37ffd6cb1daba38e2a6f654da35dbd81c3a2)\n[//]: # (START_SECTION d50f15b4c4eb65069dbaae31e3bc5b86075bd57f)\n### Fix pre-commit and requirements.txt\n\n> Commit: [d50f15b4c4eb65069dbaae31e3bc5b86075bd57f](https://github.com/dOpensource/dsiprouter/commit/d50f15b4c4eb65069dbaae31e3bc5b86075bd57f)  \n> Date: Tue, 9 Aug 2022 09:40:24 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION d50f15b4c4eb65069dbaae31e3bc5b86075bd57f)\n[//]: # (START_SECTION 56c39e111ed66326b50447bb6ed12826e5cd74cd)\n### Update pre-commit\n\n> Commit: [56c39e111ed66326b50447bb6ed12826e5cd74cd](https://github.com/dOpensource/dsiprouter/commit/56c39e111ed66326b50447bb6ed12826e5cd74cd)  \n> Date: Mon, 8 Aug 2022 22:22:57 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 56c39e111ed66326b50447bb6ed12826e5cd74cd)\n[//]: # (START_SECTION 96221e574830aea41cfc23c9237cbc6a336a8e95)\n### Bug Fixes for Updated OS Support\n\n> Commit: [96221e574830aea41cfc23c9237cbc6a336a8e95](https://github.com/dOpensource/dsiprouter/commit/96221e574830aea41cfc23c9237cbc6a336a8e95)  \n> Date: Mon, 8 Aug 2022 15:36:23 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update `dsiprouter.service` to stop/restart `nginx.service`\n- move nginx install into seperate directory\n- normalize nginx service for better support across OS\n- fix typo in mysql install\n- remove unsupported debian/ubuntu scripts\n- fix `requirements.txt` regression and update pre-commit hook\n\n\n---\n\n[//]: # (END_SECTION 96221e574830aea41cfc23c9237cbc6a336a8e95)\n[//]: # (START_SECTION ccea19047f2b1959fe0bbc0e6cf70a66576e5d15)\n### Update OS Installation Support\n\n> Commit: [ccea19047f2b1959fe0bbc0e6cf70a66576e5d15](https://github.com/dOpensource/dsiprouter/commit/ccea19047f2b1959fe0bbc0e6cf70a66576e5d15)  \n> Date: Wed, 3 Aug 2022 11:39:54 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update debian11 support to STABLE\n- add STABLE support for amazon linux 2\n- add ALPHA support for redhat linux 8\n- add ALPHA support for alma linux 8\n- add ALPHA support for rocky linux 8\n- add ALPHA support for ubuntu 22.04\n- add ALPHA support for ubuntu 20.04\n- deprecate support for debian 8\n- deprecate support for centos 8\n- deprecate support for centos 7\n- deprecate support for ubuntu 16.04\n- fix cloud-init/dsip-init service integration\n- fix dsiprouter /etc/hosts management\n- fix dsiprouter apt repo management\n- fix dnsmasq installation\n- add WIP for dsiprouter /etc/resolv.conf management\n- add cached DNS name updates to dsip-init and cron\n- add backwards compatibility for systemd v219\n- fix kernel image/header conflicts in image creation\n- move permissions changes to new `chown` dsiprouter command\n- fix `clusterinstall` not copying project files on second run\n- update source installs to reuse source files on additional install attempts\n- add back in support for configuring homer on install\n- fix `-dsipkey` arg parsing when setting dsip private key\n- add comments and deprecation notices\n- fix missing kamailio config settings update on install\n- refactor kamailio config locations\n- move dsiprouter services to `/lib/systemd/system` *aligning with most installers*\n- move dsiprouter script to `/usr/bin` *aligning with most installers*\n- add automatic servernat detection `-servernat` now overrides this\n- update rtpengine version\n- update ip / hostname resolution bash functions to align with python ones\n- add networking functions needed for IPv6 support\n- update `build_image.sh` to support passing args via any cloud provisioner\n- fix locale not set on some cloud images\n- fix dependency loop on `dsip-init` with subsequent install attempts\n- fix vulter cloud install check\n- add WIP libstirshaken openssl 3.x.x support\n- fix `setCredentials` not setting password in some cases\n\n\n---\n\n[//]: # (END_SECTION ccea19047f2b1959fe0bbc0e6cf70a66576e5d15)\n[//]: # (START_SECTION 1e029d26dcbbc3ce204cfe49d25502d1ec23355c)\n### user CRUD checkpoint\n\n> Commit: [1e029d26dcbbc3ce204cfe49d25502d1ec23355c](https://github.com/dOpensource/dsiprouter/commit/1e029d26dcbbc3ce204cfe49d25502d1ec23355c)  \n> Date: Thu, 28 Jul 2022 13:07:23 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1e029d26dcbbc3ce204cfe49d25502d1ec23355c)\n[//]: # (START_SECTION 37b20891477f13d5f7d4bd166cf461329d733e32)\n### Login logic working\n\n> Commit: [37b20891477f13d5f7d4bd166cf461329d733e32](https://github.com/dOpensource/dsiprouter/commit/37b20891477f13d5f7d4bd166cf461329d733e32)  \n> Date: Wed, 27 Jul 2022 23:48:29 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 37b20891477f13d5f7d4bd166cf461329d733e32)\n[//]: # (START_SECTION 781eff6363f2efb44e47ddb863651be491421106)\n### User API Checkpoint\n\n> Commit: [781eff6363f2efb44e47ddb863651be491421106](https://github.com/dOpensource/dsiprouter/commit/781eff6363f2efb44e47ddb863651be491421106)  \n> Date: Wed, 27 Jul 2022 11:36:06 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 781eff6363f2efb44e47ddb863651be491421106)\n[//]: # (START_SECTION 9f3515967f6a05789cce7aba8b03bb107ae5582f)\n### Updated Terraform Scripts\n\n> Commit: [9f3515967f6a05789cce7aba8b03bb107ae5582f](https://github.com/dOpensource/dsiprouter/commit/9f3515967f6a05789cce7aba8b03bb107ae5582f)  \n> Date: Tue, 26 Jul 2022 23:13:10 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9f3515967f6a05789cce7aba8b03bb107ae5582f)\n[//]: # (START_SECTION 99fed4a8840a27dfe1e4de27b95374787874f71f)\n### Added the file keyword\n\n> Commit: [99fed4a8840a27dfe1e4de27b95374787874f71f](https://github.com/dOpensource/dsiprouter/commit/99fed4a8840a27dfe1e4de27b95374787874f71f)  \n> Date: Mon, 25 Jul 2022 11:03:13 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 99fed4a8840a27dfe1e4de27b95374787874f71f)\n[//]: # (START_SECTION dffdbab53a21c0b96b6fca16f5bf772f0921dcf8)\n### Tweaks to the stir shaken setup\n\n> Commit: [dffdbab53a21c0b96b6fca16f5bf772f0921dcf8](https://github.com/dOpensource/dsiprouter/commit/dffdbab53a21c0b96b6fca16f5bf772f0921dcf8)  \n> Date: Sun, 10 Jul 2022 09:31:22 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dffdbab53a21c0b96b6fca16f5bf772f0921dcf8)\n[//]: # (START_SECTION 000e370c48da86b40a2d68259c8f42238ca83df7)\n### relocated apt command for libavcodec-extra\n\n> Commit: [000e370c48da86b40a2d68259c8f42238ca83df7](https://github.com/dOpensource/dsiprouter/commit/000e370c48da86b40a2d68259c8f42238ca83df7)  \n> Date: Thu, 7 Jul 2022 10:34:53 -0700  \n> Author: TuxPowered (vw.jetta.dude@gmail.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Relocaed install of libavcodec-extra to main block to be included in all debian builds.\n\n\n---\n\n[//]: # (END_SECTION 000e370c48da86b40a2d68259c8f42238ca83df7)\n[//]: # (START_SECTION d3b28a7ce77894f343e51cd6a9bbd78cea91c1b5)\n### Added Build dependency for RTPEngine\n\n> Commit: [d3b28a7ce77894f343e51cd6a9bbd78cea91c1b5](https://github.com/dOpensource/dsiprouter/commit/d3b28a7ce77894f343e51cd6a9bbd78cea91c1b5)  \n> Date: Thu, 7 Jul 2022 10:27:49 -0700  \n> Author: TuxPowered (vw.jetta.dude@gmail.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- RTPEngine fails to build on Debian 10. \r\n- Added cmake dependency and libavcodec-extra. \r\n- libavcodec-extra - is recommended by RTPEngine github on debian installs to support wider codecs\r\n\n- You can not build using \r\n- `dsiprouter.sh install -rtp` or `dsiprouter.sh install -all -servernat` without getting the RTPEngine failed to install error.\n\n\n---\n\n[//]: # (END_SECTION d3b28a7ce77894f343e51cd6a9bbd78cea91c1b5)\n[//]: # (START_SECTION 62080422649deb5cda3c5ea671c1deb34e256b1a)\n### Create FUNDING.yml\n\n> Commit: [62080422649deb5cda3c5ea671c1deb34e256b1a](https://github.com/dOpensource/dsiprouter/commit/62080422649deb5cda3c5ea671c1deb34e256b1a)  \n> Date: Thu, 30 Jun 2022 14:38:16 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 62080422649deb5cda3c5ea671c1deb34e256b1a)\n[//]: # (START_SECTION 0391b278289365f96fc6508b6c7da7e49e01706d)\n### Added a tag to the bcg729 repo which forces a specific version\n\n> Commit: [0391b278289365f96fc6508b6c7da7e49e01706d](https://github.com/dOpensource/dsiprouter/commit/0391b278289365f96fc6508b6c7da7e49e01706d)  \n> Date: Wed, 29 Jun 2022 06:30:51 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0391b278289365f96fc6508b6c7da7e49e01706d)\n[//]: # (START_SECTION 48cd1ec6288a0ff4eee7a80b8008f2cd9cd05aa2)\n### Added support for installing the json module\n\n> Commit: [48cd1ec6288a0ff4eee7a80b8008f2cd9cd05aa2](https://github.com/dOpensource/dsiprouter/commit/48cd1ec6288a0ff4eee7a80b8008f2cd9cd05aa2)  \n> Date: Wed, 29 Jun 2022 04:48:15 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 48cd1ec6288a0ff4eee7a80b8008f2cd9cd05aa2)\n[//]: # (START_SECTION 06ca6ce25a2c753d46405b198407cc7db62d2b03)\n### Added Kamailio logic to block invalidated calls\n\n> Commit: [06ca6ce25a2c753d46405b198407cc7db62d2b03](https://github.com/dOpensource/dsiprouter/commit/06ca6ce25a2c753d46405b198407cc7db62d2b03)  \n> Date: Mon, 27 Jun 2022 16:55:44 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 06ca6ce25a2c753d46405b198407cc7db62d2b03)\n[//]: # (START_SECTION 1e1bf37e1dafaf7c366702da190a778a0736ea02)\n### Use the libbcg729 library on Debian Bullseyee\n\n> Commit: [1e1bf37e1dafaf7c366702da190a778a0736ea02](https://github.com/dOpensource/dsiprouter/commit/1e1bf37e1dafaf7c366702da190a778a0736ea02)  \n> Date: Mon, 27 Jun 2022 05:14:35 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1e1bf37e1dafaf7c366702da190a778a0736ea02)\n[//]: # (START_SECTION 11bc68b1196f2232e074eb965496c77182d2155a)\n### Update 11.sh\n\n> Commit: [11bc68b1196f2232e074eb965496c77182d2155a](https://github.com/dOpensource/dsiprouter/commit/11bc68b1196f2232e074eb965496c77182d2155a)  \n> Date: Sun, 26 Jun 2022 23:53:02 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Added support for json\n\n\n---\n\n[//]: # (END_SECTION 11bc68b1196f2232e074eb965496c77182d2155a)\n[//]: # (START_SECTION 383a57bb4bc9ded9ad28942670dcb7a2022fdaf5)\n### Update 11.sh\n\n> Commit: [383a57bb4bc9ded9ad28942670dcb7a2022fdaf5](https://github.com/dOpensource/dsiprouter/commit/383a57bb4bc9ded9ad28942670dcb7a2022fdaf5)  \n> Date: Sun, 26 Jun 2022 23:35:31 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 383a57bb4bc9ded9ad28942670dcb7a2022fdaf5)\n[//]: # (START_SECTION d71a50bd712c066bbd538ecee5a3b089d1e4d829)\n### Refactoring the Transnexus custom logic include\n\n> Commit: [d71a50bd712c066bbd538ecee5a3b089d1e4d829](https://github.com/dOpensource/dsiprouter/commit/d71a50bd712c066bbd538ecee5a3b089d1e4d829)  \n> Date: Thu, 23 Jun 2022 21:00:36 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d71a50bd712c066bbd538ecee5a3b089d1e4d829)\n[//]: # (START_SECTION b4b5cc7f8f826c07956bff054d30c84ff5e8de49)\n### Changed logic that updates the TLS address in tls.cfg\n\n> Commit: [b4b5cc7f8f826c07956bff054d30c84ff5e8de49](https://github.com/dOpensource/dsiprouter/commit/b4b5cc7f8f826c07956bff054d30c84ff5e8de49)  \n> Date: Wed, 22 Jun 2022 19:27:50 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b4b5cc7f8f826c07956bff054d30c84ff5e8de49)\n[//]: # (START_SECTION 9c62c9ed74006af75e9c679498f5e91a39cf9200)\n### Made STIR/SHAKEN and TransNexus STIR/SHAKEN module off by default\n\n> Commit: [9c62c9ed74006af75e9c679498f5e91a39cf9200](https://github.com/dOpensource/dsiprouter/commit/9c62c9ed74006af75e9c679498f5e91a39cf9200)  \n> Date: Wed, 15 Jun 2022 14:12:55 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9c62c9ed74006af75e9c679498f5e91a39cf9200)\n[//]: # (START_SECTION 6f44e913e57a62e8f4d4ecca83fa84cbf35cbd60)\n### Fixed Stir/Shaken: - Fixed the UI - The proper orgination and destination number will be set in the identity header on outbound calls - Fixed the Kamailio Reload\n\n> Commit: [6f44e913e57a62e8f4d4ecca83fa84cbf35cbd60](https://github.com/dOpensource/dsiprouter/commit/6f44e913e57a62e8f4d4ecca83fa84cbf35cbd60)  \n> Date: Wed, 15 Jun 2022 12:56:15 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6f44e913e57a62e8f4d4ecca83fa84cbf35cbd60)\n[//]: # (START_SECTION 5e305d87a1466106c6658c64d1454a0b1e44ad36)\n### Fixed issues with installing the stirshaken libaries\n\n> Commit: [5e305d87a1466106c6658c64d1454a0b1e44ad36](https://github.com/dOpensource/dsiprouter/commit/5e305d87a1466106c6658c64d1454a0b1e44ad36)  \n> Date: Wed, 15 Jun 2022 01:46:44 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5e305d87a1466106c6658c64d1454a0b1e44ad36)\n[//]: # (START_SECTION 912aa73b0dc8f3c8bc684b22e92f0af878ba1a85)\n### Added the VerifyService to TransNexus module\n\n> Commit: [912aa73b0dc8f3c8bc684b22e92f0af878ba1a85](https://github.com/dOpensource/dsiprouter/commit/912aa73b0dc8f3c8bc684b22e92f0af878ba1a85)  \n> Date: Tue, 14 Jun 2022 18:18:18 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 912aa73b0dc8f3c8bc684b22e92f0af878ba1a85)\n[//]: # (START_SECTION 362c144d2ab056c0bfd03d8221ace09767ba5841)\n### Fixed an issue with LRN not selecting the first contact to try\n\n> Commit: [362c144d2ab056c0bfd03d8221ace09767ba5841](https://github.com/dOpensource/dsiprouter/commit/362c144d2ab056c0bfd03d8221ace09767ba5841)  \n> Date: Mon, 6 Jun 2022 14:09:48 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 362c144d2ab056c0bfd03d8221ace09767ba5841)\n[//]: # (START_SECTION 08458ff1258fa668ed48edf0e00b9bf6aa38f07a)\n### Changed LRN support to sequential routing versus parallel routing\n\n> Commit: [08458ff1258fa668ed48edf0e00b9bf6aa38f07a](https://github.com/dOpensource/dsiprouter/commit/08458ff1258fa668ed48edf0e00b9bf6aa38f07a)  \n> Date: Sun, 5 Jun 2022 19:00:48 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 08458ff1258fa668ed48edf0e00b9bf6aa38f07a)\n[//]: # (START_SECTION 42681d1ec76c6b4b64acec9fa44c1c1c8821be56)\n### Fixed a typo that prevented the  UI from loading teh config values\n\n> Commit: [42681d1ec76c6b4b64acec9fa44c1c1c8821be56](https://github.com/dOpensource/dsiprouter/commit/42681d1ec76c6b4b64acec9fa44c1c1c8821be56)  \n> Date: Tue, 17 May 2022 09:24:25 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 42681d1ec76c6b4b64acec9fa44c1c1c8821be56)\n[//]: # (START_SECTION c40702e443e6c3fece31cc66338d051527067d88)\n### UI Updates\n\n> Commit: [c40702e443e6c3fece31cc66338d051527067d88](https://github.com/dOpensource/dsiprouter/commit/c40702e443e6c3fece31cc66338d051527067d88)  \n> Date: Tue, 17 May 2022 08:42:19 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c40702e443e6c3fece31cc66338d051527067d88)\n[//]: # (START_SECTION 6fa6d008767b45244978e9b0c53998f5cb3116d8)\n### Integrated the UI with the Settings  reloading\n\n> Commit: [6fa6d008767b45244978e9b0c53998f5cb3116d8](https://github.com/dOpensource/dsiprouter/commit/6fa6d008767b45244978e9b0c53998f5cb3116d8)  \n> Date: Tue, 17 May 2022 07:44:09 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6fa6d008767b45244978e9b0c53998f5cb3116d8)\n[//]: # (START_SECTION 92689753326c9a96f642d5cda16b08378ea96c7d)\n### Added support to bypass TransNexus STIR/SHAKEN when calling emergency numbers\n\n> Commit: [92689753326c9a96f642d5cda16b08378ea96c7d](https://github.com/dOpensource/dsiprouter/commit/92689753326c9a96f642d5cda16b08378ea96c7d)  \n> Date: Mon, 16 May 2022 06:29:17 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 92689753326c9a96f642d5cda16b08378ea96c7d)\n[//]: # (START_SECTION 7d76b585c66154d3542e92329c53ae313b6d0ae2)\n### STIR SHAKEN inbound call checkpoint\n\n> Commit: [7d76b585c66154d3542e92329c53ae313b6d0ae2](https://github.com/dOpensource/dsiprouter/commit/7d76b585c66154d3542e92329c53ae313b6d0ae2)  \n> Date: Fri, 13 May 2022 18:01:17 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7d76b585c66154d3542e92329c53ae313b6d0ae2)\n[//]: # (START_SECTION 2466ccf4a9638b5428a9e515aa47b52e476b1840)\n### Load the stirshaken module\n\n> Commit: [2466ccf4a9638b5428a9e515aa47b52e476b1840](https://github.com/dOpensource/dsiprouter/commit/2466ccf4a9638b5428a9e515aa47b52e476b1840)  \n> Date: Fri, 13 May 2022 12:54:34 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2466ccf4a9638b5428a9e515aa47b52e476b1840)\n[//]: # (START_SECTION 4a3ad21a37a98251a2412d22294faf1f81df6b1b)\n### Removed unused code\n\n> Commit: [4a3ad21a37a98251a2412d22294faf1f81df6b1b](https://github.com/dOpensource/dsiprouter/commit/4a3ad21a37a98251a2412d22294faf1f81df6b1b)  \n> Date: Fri, 13 May 2022 12:46:24 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4a3ad21a37a98251a2412d22294faf1f81df6b1b)\n[//]: # (START_SECTION 9c2fbcae2aec2174783e03b53e978e7a9fa62a2f)\n### Testing the incoming calls\n\n> Commit: [9c2fbcae2aec2174783e03b53e978e7a9fa62a2f](https://github.com/dOpensource/dsiprouter/commit/9c2fbcae2aec2174783e03b53e978e7a9fa62a2f)  \n> Date: Fri, 13 May 2022 12:31:01 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9c2fbcae2aec2174783e03b53e978e7a9fa62a2f)\n[//]: # (START_SECTION 5cb96769844687f19c809c8b25b37e595256a4bf)\n### Added Support for Native Kamailio STIR/Shaken Support\n\n> Commit: [5cb96769844687f19c809c8b25b37e595256a4bf](https://github.com/dOpensource/dsiprouter/commit/5cb96769844687f19c809c8b25b37e595256a4bf)  \n> Date: Fri, 13 May 2022 06:21:37 -0600  \n> Author: Maurice Rogers (cruzer45@gmail.com)  \n> Committer: Maurice Rogers (cruzer45@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5cb96769844687f19c809c8b25b37e595256a4bf)\n[//]: # (START_SECTION 43da59b8384e66f889ae00548fbbca76c55c6bc6)\n### Added toggle switch to enable or disable transnexus LRN\n\n> Commit: [43da59b8384e66f889ae00548fbbca76c55c6bc6](https://github.com/dOpensource/dsiprouter/commit/43da59b8384e66f889ae00548fbbca76c55c6bc6)  \n> Date: Thu, 12 May 2022 18:08:12 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 43da59b8384e66f889ae00548fbbca76c55c6bc6)\n[//]: # (START_SECTION 56d29640a2d9e59628534fc959b7f66c4eae6673)\n### Fixed Mariadb systemctl service script\n\n> Commit: [56d29640a2d9e59628534fc959b7f66c4eae6673](https://github.com/dOpensource/dsiprouter/commit/56d29640a2d9e59628534fc959b7f66c4eae6673)  \n> Date: Tue, 10 May 2022 13:47:49 +0000  \n> Author: root (root@demo.dsiprouter.org)  \n> Committer: root (root@demo.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 56d29640a2d9e59628534fc959b7f66c4eae6673)\n[//]: # (START_SECTION 34e30b6dc3e50e80f8cfa229416d4c409fc49832)\n### Fixed an issue that caused the BYE from the endpoints not to work properly\n\n> Commit: [34e30b6dc3e50e80f8cfa229416d4c409fc49832](https://github.com/dOpensource/dsiprouter/commit/34e30b6dc3e50e80f8cfa229416d4c409fc49832)  \n> Date: Sat, 7 May 2022 01:43:28 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 34e30b6dc3e50e80f8cfa229416d4c409fc49832)\n[//]: # (START_SECTION 2a362b9331c4ee2e7b5473712ca54d98643586b2)\n### Transnexus Module Updates: - Now supports LCR - Contacts are followed if they are outside of the clearip domain - Fixed an issue that prevented the dSIPRouter carrier route from being followed - Fixed a bug that stopped the Transnexus module from being enabled during dSIPRouter startup\n\n> Commit: [2a362b9331c4ee2e7b5473712ca54d98643586b2](https://github.com/dOpensource/dsiprouter/commit/2a362b9331c4ee2e7b5473712ca54d98643586b2)  \n> Date: Thu, 5 May 2022 02:23:55 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2a362b9331c4ee2e7b5473712ca54d98643586b2)\n[//]: # (START_SECTION 3ce7eca6f9e31f5351260e085a4e720ede16d066)\n### Will automatically setup up the realm properly for Twilio Programmable Voice (sip.twilio.com)\n\n> Commit: [3ce7eca6f9e31f5351260e085a4e720ede16d066](https://github.com/dOpensource/dsiprouter/commit/3ce7eca6f9e31f5351260e085a4e720ede16d066)  \n> Date: Fri, 29 Apr 2022 05:36:59 +0000  \n> Author: root (root@ip-172-31-19-223.ec2.internal)  \n> Committer: root (root@ip-172-31-19-223.ec2.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3ce7eca6f9e31f5351260e085a4e720ede16d066)\n[//]: # (START_SECTION 1ba6465cfffd6c80d1e0f07314cda1eee1dd25ed)\n### Added clarity to debian10 set-hostname\n\n> Commit: [1ba6465cfffd6c80d1e0f07314cda1eee1dd25ed](https://github.com/dOpensource/dsiprouter/commit/1ba6465cfffd6c80d1e0f07314cda1eee1dd25ed)  \n> Date: Wed, 27 Apr 2022 23:43:52 -0700  \n> Author: VOICE1 (voice1me@gmail.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1ba6465cfffd6c80d1e0f07314cda1eee1dd25ed)\n[//]: # (START_SECTION 6a9f9b2de498ad93c7d84e92f7be8c43d7fb146b)\n### Added additional details to outbound routes.\n\n> Commit: [6a9f9b2de498ad93c7d84e92f7be8c43d7fb146b](https://github.com/dOpensource/dsiprouter/commit/6a9f9b2de498ad93c7d84e92f7be8c43d7fb146b)  \n> Date: Wed, 27 Apr 2022 23:20:50 -0700  \n> Author: VOICE1 (voice1me@gmail.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6a9f9b2de498ad93c7d84e92f7be8c43d7fb146b)\n[//]: # (START_SECTION bc32b97a11ff4729a060685a30ae8a019a5491f0)\n### Removed EOL Pops from Flowroute\n\n> Commit: [bc32b97a11ff4729a060685a30ae8a019a5491f0](https://github.com/dOpensource/dsiprouter/commit/bc32b97a11ff4729a060685a30ae8a019a5491f0)  \n> Date: Mon, 25 Apr 2022 16:27:47 -0700  \n> Author: VOICE1 (voice1me@gmail.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bc32b97a11ff4729a060685a30ae8a019a5491f0)\n[//]: # (START_SECTION 6fe3c84fec77ae9ec554c2eea18eb74e1982bcff)\n### Removed EOL PoPs from Flowroute\n\n> Commit: [6fe3c84fec77ae9ec554c2eea18eb74e1982bcff](https://github.com/dOpensource/dsiprouter/commit/6fe3c84fec77ae9ec554c2eea18eb74e1982bcff)  \n> Date: Mon, 25 Apr 2022 16:27:12 -0700  \n> Author: VOICE1 (voice1me@gmail.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6fe3c84fec77ae9ec554c2eea18eb74e1982bcff)\n[//]: # (START_SECTION 2f6b076c27cdb5c324a9ccfeab9960c6b8952ab7)\n### Removed EOL PoPs from Flowroute\n\n> Commit: [2f6b076c27cdb5c324a9ccfeab9960c6b8952ab7](https://github.com/dOpensource/dsiprouter/commit/2f6b076c27cdb5c324a9ccfeab9960c6b8952ab7)  \n> Date: Mon, 25 Apr 2022 16:26:22 -0700  \n> Author: VOICE1 (voice1me@gmail.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2f6b076c27cdb5c324a9ccfeab9960c6b8952ab7)\n[//]: # (START_SECTION e9f3dd7059bd73e4a73ebee4e5eabd7bfaffbdc5)\n### Added our v2 License Manager - Initial commit\n\n> Commit: [e9f3dd7059bd73e4a73ebee4e5eabd7bfaffbdc5](https://github.com/dOpensource/dsiprouter/commit/e9f3dd7059bd73e4a73ebee4e5eabd7bfaffbdc5)  \n> Date: Mon, 25 Apr 2022 05:55:25 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e9f3dd7059bd73e4a73ebee4e5eabd7bfaffbdc5)\n[//]: # (START_SECTION 162e3ed4fdc9e48d0f5e2c061cbb1d44db317735)\n### dSIPRouter Installer: - Silenced a warning message when installing Kamailio\n\n> Commit: [162e3ed4fdc9e48d0f5e2c061cbb1d44db317735](https://github.com/dOpensource/dsiprouter/commit/162e3ed4fdc9e48d0f5e2c061cbb1d44db317735)  \n> Date: Sun, 24 Apr 2022 12:15:18 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 162e3ed4fdc9e48d0f5e2c061cbb1d44db317735)\n[//]: # (START_SECTION 34e29bcb6fef9fea67de067928e27aa861ee4063)\n### dSIPRouter on Debian 11: - Added the python3-mysqldb to the Debian 11 installer so that dSIPRouter installs properly\n\n> Commit: [34e29bcb6fef9fea67de067928e27aa861ee4063](https://github.com/dOpensource/dsiprouter/commit/34e29bcb6fef9fea67de067928e27aa861ee4063)  \n> Date: Sat, 23 Apr 2022 10:42:20 +0000  \n> Author: Mack Hendicks (mack.hendricks@gmail.com)  \n> Committer: Mack Hendicks (mack.hendricks@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 34e29bcb6fef9fea67de067928e27aa861ee4063)\n[//]: # (START_SECTION c879639b5cd8d54da6e96ca9a3f474e734d14594)\n### Fixes for RTPEngine for Debian 11 - Implemented an override for the rtpengine version to mr10.4.1.1 - Added required packages for mr10.4.1.1 - Added logic to ensure rtpengine is installed properly on Debian 11\n\n> Commit: [c879639b5cd8d54da6e96ca9a3f474e734d14594](https://github.com/dOpensource/dsiprouter/commit/c879639b5cd8d54da6e96ca9a3f474e734d14594)  \n> Date: Sat, 23 Apr 2022 09:08:38 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c879639b5cd8d54da6e96ca9a3f474e734d14594)\n[//]: # (START_SECTION c83d719784414c97bbf89b9d6f991582b82dfaa1)\n### Fixed Install for Debian 11 - Changed the Kamailio version to 5.5\n\n> Commit: [c83d719784414c97bbf89b9d6f991582b82dfaa1](https://github.com/dOpensource/dsiprouter/commit/c83d719784414c97bbf89b9d6f991582b82dfaa1)  \n> Date: Sat, 23 Apr 2022 03:18:56 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c83d719784414c97bbf89b9d6f991582b82dfaa1)\n[//]: # (START_SECTION 6eb5efb7f4a3be4515745cdbe8951c5ee5a3b14a)\n### Fixed issue with looping ACK's\n\n> Commit: [6eb5efb7f4a3be4515745cdbe8951c5ee5a3b14a](https://github.com/dOpensource/dsiprouter/commit/6eb5efb7f4a3be4515745cdbe8951c5ee5a3b14a)  \n> Date: Thu, 21 Apr 2022 17:44:40 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6eb5efb7f4a3be4515745cdbe8951c5ee5a3b14a)\n[//]: # (START_SECTION c809094393142acd2ac025f641d920d7182136e5)\n### Twilio Username/Password Auth Fixes: - Hardcoded logic that sets the realm to sip.twilio.com, which allows username/password auth to work - Fixed a bug that caused the password not to be saved when updating the CarrierGroup\n\n> Commit: [c809094393142acd2ac025f641d920d7182136e5](https://github.com/dOpensource/dsiprouter/commit/c809094393142acd2ac025f641d920d7182136e5)  \n> Date: Thu, 21 Apr 2022 03:07:08 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c809094393142acd2ac025f641d920d7182136e5)\n[//]: # (START_SECTION eab2b7b5092a97bfa14369902d5701ac58313944)\n### TransNexus Update - Changed the default outbound server address to sip.clearip.com\n\n> Commit: [eab2b7b5092a97bfa14369902d5701ac58313944](https://github.com/dOpensource/dsiprouter/commit/eab2b7b5092a97bfa14369902d5701ac58313944)  \n> Date: Thu, 21 Apr 2022 03:03:22 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eab2b7b5092a97bfa14369902d5701ac58313944)\n[//]: # (START_SECTION 26294003c2dbb0f98ba0fbacd2498eb91fcbf0ec)\n### Added suppot for TransNexus sending either X-Identity or Identity with the identity information\n\n> Commit: [26294003c2dbb0f98ba0fbacd2498eb91fcbf0ec](https://github.com/dOpensource/dsiprouter/commit/26294003c2dbb0f98ba0fbacd2498eb91fcbf0ec)  \n> Date: Fri, 15 Apr 2022 23:33:16 +0000  \n> Author: root (root@guest.guest)  \n> Committer: root (root@guest.guest)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 26294003c2dbb0f98ba0fbacd2498eb91fcbf0ec)\n[//]: # (START_SECTION f477820eb11f1af06e73b4f022c8059a4e6b78e3)\n### Added logic to install dSIPRouter Kamailio module includes\n\n> Commit: [f477820eb11f1af06e73b4f022c8059a4e6b78e3](https://github.com/dOpensource/dsiprouter/commit/f477820eb11f1af06e73b4f022c8059a4e6b78e3)  \n> Date: Wed, 13 Apr 2022 11:27:11 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f477820eb11f1af06e73b4f022c8059a4e6b78e3)\n[//]: # (START_SECTION c71a5ca053c4deecd0843da72208315964c45569)\n### Added logic to handle updating Transnexus Settings via the UI and during startup of dSIPRouter\n\n> Commit: [c71a5ca053c4deecd0843da72208315964c45569](https://github.com/dOpensource/dsiprouter/commit/c71a5ca053c4deecd0843da72208315964c45569)  \n> Date: Sun, 10 Apr 2022 19:52:56 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c71a5ca053c4deecd0843da72208315964c45569)\n[//]: # (START_SECTION 2a2878e14b3a04aefd74efd015a86ff7d4efacc0)\n### Added support for Transnexus UI\n\n> Commit: [2a2878e14b3a04aefd74efd015a86ff7d4efacc0](https://github.com/dOpensource/dsiprouter/commit/2a2878e14b3a04aefd74efd015a86ff7d4efacc0)  \n> Date: Sun, 10 Apr 2022 15:20:46 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2a2878e14b3a04aefd74efd015a86ff7d4efacc0)\n[//]: # (START_SECTION 6b90261785a05f79a7ef429d66e15302409f9d78)\n### Updated CarrierGroups - Added logic to handle updating the Name of the carrier group - Updated CarrierGroup Javascript to handle updates from the UI - Configured CarrierGroup page so that it reloads after an update\n\n> Commit: [6b90261785a05f79a7ef429d66e15302409f9d78](https://github.com/dOpensource/dsiprouter/commit/6b90261785a05f79a7ef429d66e15302409f9d78)  \n> Date: Sun, 10 Apr 2022 00:02:15 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6b90261785a05f79a7ef429d66e15302409f9d78)\n[//]: # (START_SECTION a5c9d5b01d592c3d619cf7eac6ac86978a2a159c)\n### Fixes: - Pinned the version of Jinja2 and http server Fixes #429 - Debian Sources for the current version of the OS will be added to the system versus for all versions Fixes #406\n\n> Commit: [a5c9d5b01d592c3d619cf7eac6ac86978a2a159c](https://github.com/dOpensource/dsiprouter/commit/a5c9d5b01d592c3d619cf7eac6ac86978a2a159c)  \n> Date: Sun, 3 Apr 2022 00:30:45 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a5c9d5b01d592c3d619cf7eac6ac86978a2a159c)\n[//]: # (START_SECTION d7733ecb8cefd4f46b082145e7dad84b3d675389)\n### Added libffi-dev to Debian 10 install file. Fixes #86\n\n> Commit: [d7733ecb8cefd4f46b082145e7dad84b3d675389](https://github.com/dOpensource/dsiprouter/commit/d7733ecb8cefd4f46b082145e7dad84b3d675389)  \n> Date: Sat, 2 Apr 2022 21:52:08 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d7733ecb8cefd4f46b082145e7dad84b3d675389)\n[//]: # (START_SECTION 4d4db52fab645f470698e36b2c3580dcd6f7ad8f)\n### Update main.tf\n\n> Commit: [4d4db52fab645f470698e36b2c3580dcd6f7ad8f](https://github.com/dOpensource/dsiprouter/commit/4d4db52fab645f470698e36b2c3580dcd6f7ad8f)  \n> Date: Fri, 1 Apr 2022 21:59:57 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4d4db52fab645f470698e36b2c3580dcd6f7ad8f)\n[//]: # (START_SECTION e2eb9514adda76bfa18b23a58038507d9030f5c5)\n### Update main.tf\n\n> Commit: [e2eb9514adda76bfa18b23a58038507d9030f5c5](https://github.com/dOpensource/dsiprouter/commit/e2eb9514adda76bfa18b23a58038507d9030f5c5)  \n> Date: Fri, 1 Apr 2022 21:59:38 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e2eb9514adda76bfa18b23a58038507d9030f5c5)\n[//]: # (START_SECTION c2a9e73dd7a7fb9a04aee38b259f189f5d0bc610)\n### Update main.tf\n\n> Commit: [c2a9e73dd7a7fb9a04aee38b259f189f5d0bc610](https://github.com/dOpensource/dsiprouter/commit/c2a9e73dd7a7fb9a04aee38b259f189f5d0bc610)  \n> Date: Fri, 1 Apr 2022 21:53:19 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c2a9e73dd7a7fb9a04aee38b259f189f5d0bc610)\n[//]: # (START_SECTION de7ec6b999347b86b156ef40014234372912dfac)\n### Added support for Transnexus\n\n> Commit: [de7ec6b999347b86b156ef40014234372912dfac](https://github.com/dOpensource/dsiprouter/commit/de7ec6b999347b86b156ef40014234372912dfac)  \n> Date: Tue, 29 Mar 2022 07:11:02 +0000  \n> Author: root (root@kam-test.us-central1-a.c.detection-calls.internal)  \n> Committer: root (root@kam-test.us-central1-a.c.detection-calls.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION de7ec6b999347b86b156ef40014234372912dfac)\n[//]: # (START_SECTION f8fca5557f1734f828e5f8f876a4f200c7993d37)\n### Updated the CarrierGroup API to support updates via PUT\n\n> Commit: [f8fca5557f1734f828e5f8f876a4f200c7993d37](https://github.com/dOpensource/dsiprouter/commit/f8fca5557f1734f828e5f8f876a4f200c7993d37)  \n> Date: Tue, 29 Mar 2022 02:36:32 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f8fca5557f1734f828e5f8f876a4f200c7993d37)\n[//]: # (START_SECTION f09171cd444f0f5b0c5570d484f18526283e6cdc)\n### Fixed the CarrierGroup Add model - The authentication for the CarrierGroup is delegated to the Plugin so the CarrierGroup creation is not responsible for setting up IP Auth of UserPassword auth\n\n> Commit: [f09171cd444f0f5b0c5570d484f18526283e6cdc](https://github.com/dOpensource/dsiprouter/commit/f09171cd444f0f5b0c5570d484f18526283e6cdc)  \n> Date: Sun, 20 Mar 2022 22:18:43 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f09171cd444f0f5b0c5570d484f18526283e6cdc)\n[//]: # (START_SECTION 4c0d9220766f3bc7794972a5796ad898ec6ad879)\n### Fixed a JavaScript issue with the CarrierGroup UI\n\n> Commit: [4c0d9220766f3bc7794972a5796ad898ec6ad879](https://github.com/dOpensource/dsiprouter/commit/4c0d9220766f3bc7794972a5796ad898ec6ad879)  \n> Date: Sun, 20 Mar 2022 16:03:30 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4c0d9220766f3bc7794972a5796ad898ec6ad879)\n[//]: # (START_SECTION a3b873763dc004f2739099f55161d3e3a5ec671c)\n### Fixed the CarrierGroup API - Endpoints can now be created during the inital creation of a CarrierGroup - A Global Strip and Prefix can be set and applied during the initial configuration of a CarrierGroup - Fixed a bug that prevented the Username/Password auth from working - Fixed an issue with the Plugin architecture that caused an error when the Plugin name was not specified when the plugin stanza was present\n\n> Commit: [a3b873763dc004f2739099f55161d3e3a5ec671c](https://github.com/dOpensource/dsiprouter/commit/a3b873763dc004f2739099f55161d3e3a5ec671c)  \n> Date: Sat, 19 Mar 2022 23:58:32 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a3b873763dc004f2739099f55161d3e3a5ec671c)\n[//]: # (START_SECTION 6b717430ef4cc75169b4de8573ba9222dfa32d13)\n### Backported PJSIP Fix\n\n> Commit: [6b717430ef4cc75169b4de8573ba9222dfa32d13](https://github.com/dOpensource/dsiprouter/commit/6b717430ef4cc75169b4de8573ba9222dfa32d13)  \n> Date: Wed, 9 Mar 2022 08:37:16 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6b717430ef4cc75169b4de8573ba9222dfa32d13)\n[//]: # (START_SECTION 11659a3b1a6d9c951a61c812e63d73de4a264270)\n### Backporting Fix for #421 Microsoft Teams Silent Ringback\n\n> Commit: [11659a3b1a6d9c951a61c812e63d73de4a264270](https://github.com/dOpensource/dsiprouter/commit/11659a3b1a6d9c951a61c812e63d73de4a264270)  \n> Date: Tue, 22 Feb 2022 23:08:22 -0800  \n> Author: Dan Ryan (dan@acceleratenetworks.com)  \n> Committer: Dan Ryan (dan@acceleratenetworks.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 11659a3b1a6d9c951a61c812e63d73de4a264270)\n[//]: # (START_SECTION 4ded45556e2c896781186e19c843cc298e0cf140)\n### Fix for #421 Microsoft Teams Silent Ringback\n\n> Commit: [4ded45556e2c896781186e19c843cc298e0cf140](https://github.com/dOpensource/dsiprouter/commit/4ded45556e2c896781186e19c843cc298e0cf140)  \n> Date: Tue, 22 Feb 2022 22:52:19 -0800  \n> Author: Dan Ryan (dan@acceleratenetworks.com)  \n> Committer: Dan Ryan (dan@acceleratenetworks.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4ded45556e2c896781186e19c843cc298e0cf140)\n[//]: # (START_SECTION a7a3355724e86989dbe1d985a63274ccf29251f2)\n### Fix for #421 Microsoft Teams Silent Ringback\n\n> Commit: [a7a3355724e86989dbe1d985a63274ccf29251f2](https://github.com/dOpensource/dsiprouter/commit/a7a3355724e86989dbe1d985a63274ccf29251f2)  \n> Date: Tue, 22 Feb 2022 22:42:04 -0800  \n> Author: Dan Ryan (dan@acceleratenetworks.com)  \n> Committer: Dan Ryan (dan@acceleratenetworks.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a7a3355724e86989dbe1d985a63274ccf29251f2)\n[//]: # (START_SECTION de781ab923da6bea54d6e3998d63fd8ceac1291d)\n### fix #418 Error itsdangerous== 2.1.0 version with Flask==1.1.2\n\n> Commit: [de781ab923da6bea54d6e3998d63fd8ceac1291d](https://github.com/dOpensource/dsiprouter/commit/de781ab923da6bea54d6e3998d63fd8ceac1291d)  \n> Date: Mon, 21 Feb 2022 13:39:03 -0300  \n> Author: Asiel Lara (asiel.lb@stic.cl)  \n> Committer: Asiel Lara (asiel.lb@stic.cl)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION de781ab923da6bea54d6e3998d63fd8ceac1291d)\n[//]: # (START_SECTION 74b38a9eaa123bb6e97aebf90d2d186f27bc6a4e)\n### Fixed MSTeams Issue - The ACK from the PSTN was not reaching Microsoft Teams\n\n> Commit: [74b38a9eaa123bb6e97aebf90d2d186f27bc6a4e](https://github.com/dOpensource/dsiprouter/commit/74b38a9eaa123bb6e97aebf90d2d186f27bc6a4e)  \n> Date: Mon, 21 Feb 2022 04:40:09 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 74b38a9eaa123bb6e97aebf90d2d186f27bc6a4e)\n[//]: # (START_SECTION 0303d05981d4e72ff831f582e0cf232ec804d2ae)\n### Updated the version to reflect v0.643\n\n> Commit: [0303d05981d4e72ff831f582e0cf232ec804d2ae](https://github.com/dOpensource/dsiprouter/commit/0303d05981d4e72ff831f582e0cf232ec804d2ae)  \n> Date: Sat, 12 Feb 2022 02:53:20 +0000  \n> Author: root (root@dev-dsip-v0.6430)  \n> Committer: root (root@dev-dsip-v0.6430)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0303d05981d4e72ff831f582e0cf232ec804d2ae)\n[//]: # (START_SECTION 2d053086bfd08f11997514781f8949c9fb4a017b)\n### Update api_routes.py\n\n> Commit: [2d053086bfd08f11997514781f8949c9fb4a017b](https://github.com/dOpensource/dsiprouter/commit/2d053086bfd08f11997514781f8949c9fb4a017b)  \n> Date: Tue, 1 Feb 2022 19:02:17 +0100  \n> Author: TheGolg (98840856+TheGolg@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- fix bug to upload certificates via post\n\n\n---\n\n[//]: # (END_SECTION 2d053086bfd08f11997514781f8949c9fb4a017b)\n[//]: # (START_SECTION 467e0353d1ed92c2cdd297b4070e833f8f269313)\n### Updates: - Changed the max_expires header so that the SIP UA and SIP UC controls this without having a max of 1 hour - Fixed the UI so that CarrierGroup UI works with the new CarrierGroup API\n\n> Commit: [467e0353d1ed92c2cdd297b4070e833f8f269313](https://github.com/dOpensource/dsiprouter/commit/467e0353d1ed92c2cdd297b4070e833f8f269313)  \n> Date: Fri, 21 Jan 2022 18:34:38 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 467e0353d1ed92c2cdd297b4070e833f8f269313)\n[//]: # (START_SECTION 1398c9f37f6e2aee88fd6d62ab0a1b50ca4529d9)\n### Update terraform.tfvars.sample\n\n> Commit: [1398c9f37f6e2aee88fd6d62ab0a1b50ca4529d9](https://github.com/dOpensource/dsiprouter/commit/1398c9f37f6e2aee88fd6d62ab0a1b50ca4529d9)  \n> Date: Sat, 15 Jan 2022 07:47:37 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1398c9f37f6e2aee88fd6d62ab0a1b50ca4529d9)\n[//]: # (START_SECTION ffa050d4259d859128d75f34368e0430005967a0)\n### Updated pvt_key_path variable\n\n> Commit: [ffa050d4259d859128d75f34368e0430005967a0](https://github.com/dOpensource/dsiprouter/commit/ffa050d4259d859128d75f34368e0430005967a0)  \n> Date: Sun, 9 Jan 2022 23:49:28 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ffa050d4259d859128d75f34368e0430005967a0)\n[//]: # (START_SECTION 868444e2dcfad98ad93fba8bafced1bb0428ae39)\n### Updated variables\n\n> Commit: [868444e2dcfad98ad93fba8bafced1bb0428ae39](https://github.com/dOpensource/dsiprouter/commit/868444e2dcfad98ad93fba8bafced1bb0428ae39)  \n> Date: Sun, 9 Jan 2022 23:42:20 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 868444e2dcfad98ad93fba8bafced1bb0428ae39)\n[//]: # (START_SECTION 6f7e10ef918323380c8f2f1147c443608206d92d)\n### Fixed the CarrierGroup UI to allow it to work properly when a carrier plugin is not used\n\n> Commit: [6f7e10ef918323380c8f2f1147c443608206d92d](https://github.com/dOpensource/dsiprouter/commit/6f7e10ef918323380c8f2f1147c443608206d92d)  \n> Date: Sun, 9 Jan 2022 22:59:56 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6f7e10ef918323380c8f2f1147c443608206d92d)\n[//]: # (START_SECTION d098638cebee6123d756893963770e245dc96d6a)\n### Fixed Carriergroup API - Added exception handling that prevents a carriergroup from being created if the plugin doesn't work\n\n> Commit: [d098638cebee6123d756893963770e245dc96d6a](https://github.com/dOpensource/dsiprouter/commit/d098638cebee6123d756893963770e245dc96d6a)  \n> Date: Sun, 9 Jan 2022 03:40:52 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d098638cebee6123d756893963770e245dc96d6a)\n[//]: # (START_SECTION 60afad7298ae532c4d51a6214fe118b0c0442a0a)\n### Added support for the Carriergroup API Issue #413\n\n> Commit: [60afad7298ae532c4d51a6214fe118b0c0442a0a](https://github.com/dOpensource/dsiprouter/commit/60afad7298ae532c4d51a6214fe118b0c0442a0a)  \n> Date: Sat, 8 Jan 2022 23:36:20 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 60afad7298ae532c4d51a6214fe118b0c0442a0a)\n[//]: # (START_SECTION dfafb9953c70e936ccb9a3c6772993d43c9be4cb)\n### Added support to deal with Zoiper Softphone on Android Fixes #411\n\n> Commit: [dfafb9953c70e936ccb9a3c6772993d43c9be4cb](https://github.com/dOpensource/dsiprouter/commit/dfafb9953c70e936ccb9a3c6772993d43c9be4cb)  \n> Date: Thu, 30 Dec 2021 15:41:02 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dfafb9953c70e936ccb9a3c6772993d43c9be4cb)\n[//]: # (START_SECTION f5ba6be88f6da2d66c52bb039f224b274cb68ca8)\n### Update use-cases.rst\n\n> Commit: [f5ba6be88f6da2d66c52bb039f224b274cb68ca8](https://github.com/dOpensource/dsiprouter/commit/f5ba6be88f6da2d66c52bb039f224b274cb68ca8)  \n> Date: Wed, 29 Dec 2021 17:05:24 +0300  \n> Author: James Peru Mmbono (jmsperu@gmail.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Corrected spelling from Authenication to Authentication\n\n\n---\n\n[//]: # (END_SECTION f5ba6be88f6da2d66c52bb039f224b274cb68ca8)\n[//]: # (START_SECTION fe8e22a60b89b11d803880aba12a3bbfd43d37a7)\n### Updated carriergroups UI to work with the new API\n\n> Commit: [fe8e22a60b89b11d803880aba12a3bbfd43d37a7](https://github.com/dOpensource/dsiprouter/commit/fe8e22a60b89b11d803880aba12a3bbfd43d37a7)  \n> Date: Mon, 22 Nov 2021 02:41:56 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fe8e22a60b89b11d803880aba12a3bbfd43d37a7)\n[//]: # (START_SECTION 197eb7aa1b572796a9a1b9fc5d8e8bf52c85684b)\n### Carriergroup Enhancements - Created a module to isolate logic for carriergroups - Added a plugin architecture for carriers - Added support for the carrier plugin architecture to the UI\n\n> Commit: [197eb7aa1b572796a9a1b9fc5d8e8bf52c85684b](https://github.com/dOpensource/dsiprouter/commit/197eb7aa1b572796a9a1b9fc5d8e8bf52c85684b)  \n> Date: Fri, 19 Nov 2021 04:12:02 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 197eb7aa1b572796a9a1b9fc5d8e8bf52c85684b)\n[//]: # (START_SECTION 5f442e34c294153e1db463c432a0d73464aca3f2)\n### Integrated new carriergroup API with Twilio Plugin\n\n> Commit: [5f442e34c294153e1db463c432a0d73464aca3f2](https://github.com/dOpensource/dsiprouter/commit/5f442e34c294153e1db463c432a0d73464aca3f2)  \n> Date: Sun, 31 Oct 2021 17:30:03 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5f442e34c294153e1db463c432a0d73464aca3f2)\n[//]: # (START_SECTION 54d48156e8c9953a74121afa7173cde21f3f466c)\n### Initial commit of the CarrierGroups API with Carrier Plugin Architecture\n\n> Commit: [54d48156e8c9953a74121afa7173cde21f3f466c](https://github.com/dOpensource/dsiprouter/commit/54d48156e8c9953a74121afa7173cde21f3f466c)  \n> Date: Sun, 24 Oct 2021 22:54:42 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 54d48156e8c9953a74121afa7173cde21f3f466c)\n[//]: # (START_SECTION f00b06f414e6708f9381a16a2db46fe4e60e4ab8)\n### Fixed an issue with route headers on ACK and BYE's\n\n> Commit: [f00b06f414e6708f9381a16a2db46fe4e60e4ab8](https://github.com/dOpensource/dsiprouter/commit/f00b06f414e6708f9381a16a2db46fe4e60e4ab8)  \n> Date: Sun, 17 Oct 2021 15:44:01 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f00b06f414e6708f9381a16a2db46fe4e60e4ab8)\n[//]: # (START_SECTION d8fbb36687663b2b5fd474043b63732bae8980fd)\n### Updated the version to v0.642\n\n> Commit: [d8fbb36687663b2b5fd474043b63732bae8980fd](https://github.com/dOpensource/dsiprouter/commit/d8fbb36687663b2b5fd474043b63732bae8980fd)  \n> Date: Mon, 27 Sep 2021 03:33:23 +0000  \n> Author: Mack hendricks (mack@dopensource.com)  \n> Committer: Mack hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d8fbb36687663b2b5fd474043b63732bae8980fd)\n[//]: # (START_SECTION 28a6cec3232c02ad1a55b856fb667a17f16e157a)\n### UI Fixes - Login screen looks correct on a mobile device - Dashboard screen displays the widgets vertically on a mobile device\n\n> Commit: [28a6cec3232c02ad1a55b856fb667a17f16e157a](https://github.com/dOpensource/dsiprouter/commit/28a6cec3232c02ad1a55b856fb667a17f16e157a)  \n> Date: Mon, 27 Sep 2021 03:29:09 +0000  \n> Author: Mack hendricks (mack@dopensource.com)  \n> Committer: Mack hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 28a6cec3232c02ad1a55b856fb667a17f16e157a)\n[//]: # (START_SECTION 660a32a135b8bac31aa59c495ae68f9467193bb4)\n### Fixes - Looping ACK when in SERVERNAT mode - Added support for International Emergency Numbers and US National Suicide prevention - Created a generic test for removing the Route header when it matches the ip external ip address of dSIP\n\n> Commit: [660a32a135b8bac31aa59c495ae68f9467193bb4](https://github.com/dOpensource/dsiprouter/commit/660a32a135b8bac31aa59c495ae68f9467193bb4)  \n> Date: Fri, 24 Sep 2021 15:03:57 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 660a32a135b8bac31aa59c495ae68f9467193bb4)\n[//]: # (START_SECTION 87d4aed54ceb44e31c22a0cf0e6902d3a09ec84e)\n### Fixed Issues: - The SDP was not being written correctly when in a Servernat configuration - The Route header from the carrier in the WITHINDLG was configued to track the ip address of the selected media server and route the call back correctly\n\n> Commit: [87d4aed54ceb44e31c22a0cf0e6902d3a09ec84e](https://github.com/dOpensource/dsiprouter/commit/87d4aed54ceb44e31c22a0cf0e6902d3a09ec84e)  \n> Date: Tue, 21 Sep 2021 06:38:07 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 87d4aed54ceb44e31c22a0cf0e6902d3a09ec84e)\n[//]: # (START_SECTION 323f38bf35f4078e9896a8f01a0535832c68ecaa)\n### Fixes Issue #400\n\n> Commit: [323f38bf35f4078e9896a8f01a0535832c68ecaa](https://github.com/dOpensource/dsiprouter/commit/323f38bf35f4078e9896a8f01a0535832c68ecaa)  \n> Date: Sun, 29 Aug 2021 16:18:33 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 323f38bf35f4078e9896a8f01a0535832c68ecaa)\n[//]: # (START_SECTION e8f4ae98679f93c18759fb23153dcc61060356cc)\n### Fix up formatting errors in MS Teams Docs, #388 and #395\n\n> Commit: [e8f4ae98679f93c18759fb23153dcc61060356cc](https://github.com/dOpensource/dsiprouter/commit/e8f4ae98679f93c18759fb23153dcc61060356cc)  \n> Date: Sat, 17 Jul 2021 22:00:51 -0700  \n> Author: Dan Ryan (dan@acceleratenetworks.com)  \n> Committer: Dan Ryan (dan@acceleratenetworks.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e8f4ae98679f93c18759fb23153dcc61060356cc)\n[//]: # (START_SECTION cd7860138957fad20e630957d3352d31166c42b6)\n### MediaServer API - A tab spacing caused the GET request not to be executed when not in DEBUG mode\n\n> Commit: [cd7860138957fad20e630957d3352d31166c42b6](https://github.com/dOpensource/dsiprouter/commit/cd7860138957fad20e630957d3352d31166c42b6)  \n> Date: Fri, 25 Jun 2021 20:21:51 +0000  \n> Author: root (root@demo.dsiprouter.org)  \n> Committer: root (root@demo.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cd7860138957fad20e630957d3352d31166c42b6)\n[//]: # (START_SECTION 8049a7005ab453e7d10c9f5cdd1e0831ed5374b7)\n### Update supported OS list and centos7 reference per pull request #388\n\n> Commit: [8049a7005ab453e7d10c9f5cdd1e0831ed5374b7](https://github.com/dOpensource/dsiprouter/commit/8049a7005ab453e7d10c9f5cdd1e0831ed5374b7)  \n> Date: Tue, 22 Jun 2021 14:11:03 -0700  \n> Author: Dan Ryan (dan@acceleratenetworks.com)  \n> Committer: Dan Ryan (dan@acceleratenetworks.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8049a7005ab453e7d10c9f5cdd1e0831ed5374b7)\n[//]: # (START_SECTION 9bd7986c4952c2f473c2fae2eee8ff0aaea06a4c)\n### Add references to PowerShell Script sources\n\n> Commit: [9bd7986c4952c2f473c2fae2eee8ff0aaea06a4c](https://github.com/dOpensource/dsiprouter/commit/9bd7986c4952c2f473c2fae2eee8ff0aaea06a4c)  \n> Date: Sun, 13 Jun 2021 15:05:20 -0700  \n> Author: Dan Ryan (dan@acceleratenetworks.com)  \n> Committer: Dan Ryan (dan@acceleratenetworks.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9bd7986c4952c2f473c2fae2eee8ff0aaea06a4c)\n[//]: # (START_SECTION 9355e5408640fa412254ed37ce3de0789cfcbade)\n### Added Direct Routing use case\n\n> Commit: [9355e5408640fa412254ed37ce3de0789cfcbade](https://github.com/dOpensource/dsiprouter/commit/9355e5408640fa412254ed37ce3de0789cfcbade)  \n> Date: Sun, 13 Jun 2021 15:02:10 -0700  \n> Author: Dan Ryan (dan@acceleratenetworks.com)  \n> Committer: Dan Ryan (dan@acceleratenetworks.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9355e5408640fa412254ed37ce3de0789cfcbade)\n[//]: # (START_SECTION 24f390f44aec775aceef7578ff75809c03142b39)\n### Remove CentOS 7 Duplicate\n\n> Commit: [24f390f44aec775aceef7578ff75809c03142b39](https://github.com/dOpensource/dsiprouter/commit/24f390f44aec775aceef7578ff75809c03142b39)  \n> Date: Sun, 13 Jun 2021 13:11:35 -0700  \n> Author: Dan (dan@acceleratenetworks.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- This page looks to be an old duplicate not used in the published ReadTheDocs\n\n\n---\n\n[//]: # (END_SECTION 24f390f44aec775aceef7578ff75809c03142b39)\n[//]: # (START_SECTION f5017f5afa9e2f2bfea9fa9fb20ee8989a51ab2d)\n### Add Debian 10 install section\n\n> Commit: [f5017f5afa9e2f2bfea9fa9fb20ee8989a51ab2d](https://github.com/dOpensource/dsiprouter/commit/f5017f5afa9e2f2bfea9fa9fb20ee8989a51ab2d)  \n> Date: Sun, 13 Jun 2021 13:06:25 -0700  \n> Author: Dan (dan@acceleratenetworks.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f5017f5afa9e2f2bfea9fa9fb20ee8989a51ab2d)\n[//]: # (START_SECTION 1254e8e3d5cd29b3ae67ff993a0c110e76fac270)\n### Updated installation documentation for 0.641 and master\n\n> Commit: [1254e8e3d5cd29b3ae67ff993a0c110e76fac270](https://github.com/dOpensource/dsiprouter/commit/1254e8e3d5cd29b3ae67ff993a0c110e76fac270)  \n> Date: Sun, 13 Jun 2021 12:52:21 -0700  \n> Author: Dan (dan@acceleratenetworks.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Added Debian 10.9 installation instructions reference\n\n\n---\n\n[//]: # (END_SECTION 1254e8e3d5cd29b3ae67ff993a0c110e76fac270)\n[//]: # (START_SECTION a8c0496fa2525513d8cf81668c57d783eee299d7)\n### Allow the installer to continue working if the repo public key is not available\n\n> Commit: [a8c0496fa2525513d8cf81668c57d783eee299d7](https://github.com/dOpensource/dsiprouter/commit/a8c0496fa2525513d8cf81668c57d783eee299d7)  \n> Date: Thu, 27 May 2021 05:21:42 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@demo.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a8c0496fa2525513d8cf81668c57d783eee299d7)\n[//]: # (START_SECTION 5713f323069a50de8d0dce8bd2953e1d142b46bf)\n### Pinned Flask to 1.1.2. Fixes #376\n\n> Commit: [5713f323069a50de8d0dce8bd2953e1d142b46bf](https://github.com/dOpensource/dsiprouter/commit/5713f323069a50de8d0dce8bd2953e1d142b46bf)  \n> Date: Thu, 13 May 2021 10:38:37 +0000  \n> Author: root (root@demo.dsiprouter.org)  \n> Committer: root (root@demo.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5713f323069a50de8d0dce8bd2953e1d142b46bf)\n[//]: # (START_SECTION cdecaf5d747f681ee6addf2b2d6e58479e36da91)\n### Fixed issue with CDR's not working\n\n> Commit: [cdecaf5d747f681ee6addf2b2d6e58479e36da91](https://github.com/dOpensource/dsiprouter/commit/cdecaf5d747f681ee6addf2b2d6e58479e36da91)  \n> Date: Wed, 31 Mar 2021 15:00:44 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cdecaf5d747f681ee6addf2b2d6e58479e36da91)\n[//]: # (START_SECTION ec4219de84f9d346f082fc668d5979aba519c047)\n### Fixed issue that prevented CDR's from being created when loose routing is not being used\n\n> Commit: [ec4219de84f9d346f082fc668d5979aba519c047](https://github.com/dOpensource/dsiprouter/commit/ec4219de84f9d346f082fc668d5979aba519c047)  \n> Date: Tue, 30 Mar 2021 23:59:25 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ec4219de84f9d346f082fc668d5979aba519c047)\n[//]: # (START_SECTION d7d03e0bf8481ce48fc6bc5c8d7c1b56fc46575f)\n### Fixed Endpoint Group UI Bug - Removed entries from the dispatcher table when the weight was 0\n\n> Commit: [d7d03e0bf8481ce48fc6bc5c8d7c1b56fc46575f](https://github.com/dOpensource/dsiprouter/commit/d7d03e0bf8481ce48fc6bc5c8d7c1b56fc46575f)  \n> Date: Thu, 11 Mar 2021 19:38:16 +0000  \n> Author: root (root@ip-172-31-5-106.ec2.internal)  \n> Committer: root (root@ip-172-31-5-106.ec2.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d7d03e0bf8481ce48fc6bc5c8d7c1b56fc46575f)\n[//]: # (START_SECTION 6c7532ccec112d6faf8ce47f1d89151e4a46f316)\n### Turned SETTINGS.DEBUG to False\n\n> Commit: [6c7532ccec112d6faf8ce47f1d89151e4a46f316](https://github.com/dOpensource/dsiprouter/commit/6c7532ccec112d6faf8ce47f1d89151e4a46f316)  \n> Date: Sun, 21 Feb 2021 17:13:01 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6c7532ccec112d6faf8ce47f1d89151e4a46f316)\n[//]: # (START_SECTION 1fb969abeb8eefc633be50e11e3905d688526cdf)\n### Fixed the carrier group endpoints - Some of the endpoints were not tied to the correct carrier group\n\n> Commit: [1fb969abeb8eefc633be50e11e3905d688526cdf](https://github.com/dOpensource/dsiprouter/commit/1fb969abeb8eefc633be50e11e3905d688526cdf)  \n> Date: Sat, 20 Feb 2021 23:31:55 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1fb969abeb8eefc633be50e11e3905d688526cdf)\n[//]: # (START_SECTION 64f141ba6ced44e6a0b9f1b1680a8edb5755415f)\n### Fixed the carrier group endpoints - Some of the endpoints were not tied to the correct carrier group\n\n> Commit: [64f141ba6ced44e6a0b9f1b1680a8edb5755415f](https://github.com/dOpensource/dsiprouter/commit/64f141ba6ced44e6a0b9f1b1680a8edb5755415f)  \n> Date: Sat, 20 Feb 2021 23:28:35 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 64f141ba6ced44e6a0b9f1b1680a8edb5755415f)\n[//]: # (START_SECTION 813dcdc3199fa5157a3b4a83dd52580c5570e659)\n### Updated the lease API\n\n> Commit: [813dcdc3199fa5157a3b4a83dd52580c5570e659](https://github.com/dOpensource/dsiprouter/commit/813dcdc3199fa5157a3b4a83dd52580c5570e659)  \n> Date: Sat, 20 Feb 2021 19:27:22 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 813dcdc3199fa5157a3b4a83dd52580c5570e659)\n[//]: # (START_SECTION a77ffb5e2988d7ebb01f59464707cebf33a0adb4)\n### Fixed issue with Record Routes so that a single NLB can be used\n\n> Commit: [a77ffb5e2988d7ebb01f59464707cebf33a0adb4](https://github.com/dOpensource/dsiprouter/commit/a77ffb5e2988d7ebb01f59464707cebf33a0adb4)  \n> Date: Tue, 16 Feb 2021 19:38:31 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a77ffb5e2988d7ebb01f59464707cebf33a0adb4)\n[//]: # (START_SECTION 863343903605d610f5b42f72805f993c401cc862)\n### Fixed the crontab\n\n> Commit: [863343903605d610f5b42f72805f993c401cc862](https://github.com/dOpensource/dsiprouter/commit/863343903605d610f5b42f72805f993c401cc862)  \n> Date: Tue, 16 Feb 2021 19:26:31 +0000  \n> Author: root (root@ip-172-31-8-204.ec2.internal)  \n> Committer: root (root@ip-172-31-8-204.ec2.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 863343903605d610f5b42f72805f993c401cc862)\n[//]: # (START_SECTION 26f16646507d81a840cce049069872c431905ca9)\n### Added support for writing multiple record routes for an INBOUND and OUTBOUND load balancer\n\n> Commit: [26f16646507d81a840cce049069872c431905ca9](https://github.com/dOpensource/dsiprouter/commit/26f16646507d81a840cce049069872c431905ca9)  \n> Date: Fri, 12 Feb 2021 17:45:24 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 26f16646507d81a840cce049069872c431905ca9)\n[//]: # (START_SECTION 3b41901eb9baad1139d245b6220fb502c7c546ca)\n### Added support for creating the Kamailio DB user\n\n> Commit: [3b41901eb9baad1139d245b6220fb502c7c546ca](https://github.com/dOpensource/dsiprouter/commit/3b41901eb9baad1139d245b6220fb502c7c546ca)  \n> Date: Fri, 12 Feb 2021 00:21:07 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3b41901eb9baad1139d245b6220fb502c7c546ca)\n[//]: # (START_SECTION e44d74fb9848cb5812512da7e216e73c6bbbdad3)\n### Added support for creating the Kamailio DB user\n\n> Commit: [e44d74fb9848cb5812512da7e216e73c6bbbdad3](https://github.com/dOpensource/dsiprouter/commit/e44d74fb9848cb5812512da7e216e73c6bbbdad3)  \n> Date: Fri, 12 Feb 2021 00:07:27 +0000  \n> Author: root (root@demo.dsiprouter.org)  \n> Committer: root (root@demo.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e44d74fb9848cb5812512da7e216e73c6bbbdad3)\n[//]: # (START_SECTION 9203d051d69e9176f2bb659c70c29cceab90b084)\n### Fixes: - Documentation generation - Moved the Message of The Day creation to after dSIP is started the first time\n\n> Commit: [9203d051d69e9176f2bb659c70c29cceab90b084](https://github.com/dOpensource/dsiprouter/commit/9203d051d69e9176f2bb659c70c29cceab90b084)  \n> Date: Wed, 10 Feb 2021 17:31:40 +0000  \n> Author: root (root@ip-172-31-58-119.ec2.internal)  \n> Committer: root (root@ip-172-31-58-119.ec2.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9203d051d69e9176f2bb659c70c29cceab90b084)\n[//]: # (START_SECTION 7e4b557bccfd166ac746c73e98e7c76e6c2c9d57)\n### Fix Ordering Issues\n\n> Commit: [7e4b557bccfd166ac746c73e98e7c76e6c2c9d57](https://github.com/dOpensource/dsiprouter/commit/7e4b557bccfd166ac746c73e98e7c76e6c2c9d57)  \n> Date: Wed, 10 Feb 2021 12:00:19 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix ssh banner update order to get current IP changes\n- re-add doc generation\n\n\n---\n\n[//]: # (END_SECTION 7e4b557bccfd166ac746c73e98e7c76e6c2c9d57)\n[//]: # (START_SECTION 6757683347f947e730ff668bac4a3d578b8044db)\n### Add Cryptography Conflict Resolution In Pre-Commit Hook\n\n> Commit: [6757683347f947e730ff668bac4a3d578b8044db](https://github.com/dOpensource/dsiprouter/commit/6757683347f947e730ff668bac4a3d578b8044db)  \n> Date: Wed, 10 Feb 2021 11:35:43 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 6757683347f947e730ff668bac4a3d578b8044db)\n[//]: # (START_SECTION 1bf0cc47ceaa158aa24880be5e7f9b66dd67c956)\n### Fixed issues with updatekamconfig\n\n> Commit: [1bf0cc47ceaa158aa24880be5e7f9b66dd67c956](https://github.com/dOpensource/dsiprouter/commit/1bf0cc47ceaa158aa24880be5e7f9b66dd67c956)  \n> Date: Wed, 10 Feb 2021 16:25:57 +0000  \n> Author: root (root@ip-172-31-50-23.ec2.internal)  \n> Committer: root (root@ip-172-31-50-23.ec2.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1bf0cc47ceaa158aa24880be5e7f9b66dd67c956)\n[//]: # (START_SECTION 063aa3646d4e6a39baeaa5fa26bc329139f05914)\n### Fixes - Resolved error message during Kamailio install - Resolved issue with MOTD - Disabled Docs from being created for right now\n\n> Commit: [063aa3646d4e6a39baeaa5fa26bc329139f05914](https://github.com/dOpensource/dsiprouter/commit/063aa3646d4e6a39baeaa5fa26bc329139f05914)  \n> Date: Wed, 10 Feb 2021 12:19:47 +0000  \n> Author: root (root@ip-172-31-14-13.ec2.internal)  \n> Committer: root (root@ip-172-31-14-13.ec2.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 063aa3646d4e6a39baeaa5fa26bc329139f05914)\n[//]: # (START_SECTION a1f4a720a824ed2e11d64e7fa22bfaa6a2da7eab)\n### Changed the order in the requirements file so that the cryptograpy package is installed with a pinned version before the certbot package\n\n> Commit: [a1f4a720a824ed2e11d64e7fa22bfaa6a2da7eab](https://github.com/dOpensource/dsiprouter/commit/a1f4a720a824ed2e11d64e7fa22bfaa6a2da7eab)  \n> Date: Wed, 10 Feb 2021 10:22:42 +0000  \n> Author: root (root@ip-172-31-15-70.ec2.internal)  \n> Committer: root (root@ip-172-31-15-70.ec2.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a1f4a720a824ed2e11d64e7fa22bfaa6a2da7eab)\n[//]: # (START_SECTION 01042e110ecc152ce807322aeb53baba5c06bc2f)\n### Pinned the cryptography package to version >3.3 and <3.4 to deal with a new requirement for rust\n\n> Commit: [01042e110ecc152ce807322aeb53baba5c06bc2f](https://github.com/dOpensource/dsiprouter/commit/01042e110ecc152ce807322aeb53baba5c06bc2f)  \n> Date: Wed, 10 Feb 2021 09:31:17 +0000  \n> Author: root (root@ip-172-31-8-5.ec2.internal)  \n> Committer: root (root@ip-172-31-8-5.ec2.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 01042e110ecc152ce807322aeb53baba5c06bc2f)\n[//]: # (START_SECTION a495613e9c6d52bdf921c9d005d4b7206ed6feb2)\n### Fixed WebRTC Support - Added a TLS server certificate settings for the WebRTC port, which is 4443 - Update the Kam configuration script to update the settings\n\n> Commit: [a495613e9c6d52bdf921c9d005d4b7206ed6feb2](https://github.com/dOpensource/dsiprouter/commit/a495613e9c6d52bdf921c9d005d4b7206ed6feb2)  \n> Date: Wed, 10 Feb 2021 02:23:23 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a495613e9c6d52bdf921c9d005d4b7206ed6feb2)\n[//]: # (START_SECTION f59ff1f05c8742ceabc0903bf453732976268ca2)\n### Added support for setting the language for the domain\n\n> Commit: [f59ff1f05c8742ceabc0903bf453732976268ca2](https://github.com/dOpensource/dsiprouter/commit/f59ff1f05c8742ceabc0903bf453732976268ca2)  \n> Date: Wed, 10 Feb 2021 01:30:18 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f59ff1f05c8742ceabc0903bf453732976268ca2)\n[//]: # (START_SECTION 500a9c4cbe3531658e7b6fad0c9298dc38e504ca)\n### Fix RTPEngine Errors When Not Installed\n\n> Commit: [500a9c4cbe3531658e7b6fad0c9298dc38e504ca](https://github.com/dOpensource/dsiprouter/commit/500a9c4cbe3531658e7b6fad0c9298dc38e504ca)  \n> Date: Mon, 8 Feb 2021 17:45:18 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- pre-process out rtpengine parts of kam config when not installed\n- add `WITH_RTPENGINE` generated param to install script\n\n\n---\n\n[//]: # (END_SECTION 500a9c4cbe3531658e7b6fad0c9298dc38e504ca)\n[//]: # (START_SECTION da5a1a010e9c2950043daeca6766b5c1e5f01e9a)\n### Fixed the permissions error when using backup and restore\n\n> Commit: [da5a1a010e9c2950043daeca6766b5c1e5f01e9a](https://github.com/dOpensource/dsiprouter/commit/da5a1a010e9c2950043daeca6766b5c1e5f01e9a)  \n> Date: Mon, 8 Feb 2021 21:26:07 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION da5a1a010e9c2950043daeca6766b5c1e5f01e9a)\n[//]: # (START_SECTION 9bd78475c38ea4e4a2e91fbdd3d35e79ec292cf2)\n### All domains and domain attributes will be deleted when deleting an EndPoint Group\n\n> Commit: [9bd78475c38ea4e4a2e91fbdd3d35e79ec292cf2](https://github.com/dOpensource/dsiprouter/commit/9bd78475c38ea4e4a2e91fbdd3d35e79ec292cf2)  \n> Date: Mon, 8 Feb 2021 00:43:00 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9bd78475c38ea4e4a2e91fbdd3d35e79ec292cf2)\n[//]: # (START_SECTION 00512092012a8774bd3e8bb1d98da705f3ec6515)\n### Fixed bug that was removing the inbound route each time an endpoint was updated\n\n> Commit: [00512092012a8774bd3e8bb1d98da705f3ec6515](https://github.com/dOpensource/dsiprouter/commit/00512092012a8774bd3e8bb1d98da705f3ec6515)  \n> Date: Sun, 7 Feb 2021 16:45:22 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 00512092012a8774bd3e8bb1d98da705f3ec6515)\n[//]: # (START_SECTION fb295b01df70c24b149e0ba5e4dbe2078acd2c46)\n### Fixed bug that was removing the inbound route each time an endpoint was updated\n\n> Commit: [fb295b01df70c24b149e0ba5e4dbe2078acd2c46](https://github.com/dOpensource/dsiprouter/commit/fb295b01df70c24b149e0ba5e4dbe2078acd2c46)  \n> Date: Sun, 7 Feb 2021 16:41:55 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fb295b01df70c24b149e0ba5e4dbe2078acd2c46)\n[//]: # (START_SECTION 302f209612217053c796a452b904062542326532)\n### Made the Network Load Balancer features more generic\n\n> Commit: [302f209612217053c796a452b904062542326532](https://github.com/dOpensource/dsiprouter/commit/302f209612217053c796a452b904062542326532)  \n> Date: Sat, 6 Feb 2021 22:36:04 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 302f209612217053c796a452b904062542326532)\n[//]: # (START_SECTION 8ac9a1d9a2c1966591ab441bbc75958c09949a0c)\n### Added a check to prevent the EXTERNAL_IP from not being set\n\n> Commit: [8ac9a1d9a2c1966591ab441bbc75958c09949a0c](https://github.com/dOpensource/dsiprouter/commit/8ac9a1d9a2c1966591ab441bbc75958c09949a0c)  \n> Date: Sat, 6 Feb 2021 20:47:14 +0000  \n> Author: root (root@ip-172-31-37-179.ec2.internal)  \n> Committer: root (root@ip-172-31-37-179.ec2.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8ac9a1d9a2c1966591ab441bbc75958c09949a0c)\n[//]: # (START_SECTION da57ae2e35af3f04ea35987b06bb5becc0801f9f)\n### Bug fixes for setCredentials\n\n> Commit: [da57ae2e35af3f04ea35987b06bb5becc0801f9f](https://github.com/dOpensource/dsiprouter/commit/da57ae2e35af3f04ea35987b06bb5becc0801f9f)  \n> Date: Wed, 3 Feb 2021 20:27:00 +0000  \n> Author: root (root@ip-172-31-54-216.ec2.internal)  \n> Committer: root (root@ip-172-31-54-216.ec2.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION da57ae2e35af3f04ea35987b06bb5becc0801f9f)\n[//]: # (START_SECTION 55ad40f4723e2ac65f197f32e7f00983ee0a464a)\n### Fixed Inbound Load Balancing\n\n> Commit: [55ad40f4723e2ac65f197f32e7f00983ee0a464a](https://github.com/dOpensource/dsiprouter/commit/55ad40f4723e2ac65f197f32e7f00983ee0a464a)  \n> Date: Wed, 3 Feb 2021 12:52:21 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 55ad40f4723e2ac65f197f32e7f00983ee0a464a)\n[//]: # (START_SECTION 34b7d1942f56355684c8dc5b0cc3d0100c72184b)\n### Fixed Inbound Load Balancing to use localhost\n\n> Commit: [34b7d1942f56355684c8dc5b0cc3d0100c72184b](https://github.com/dOpensource/dsiprouter/commit/34b7d1942f56355684c8dc5b0cc3d0100c72184b)  \n> Date: Wed, 3 Feb 2021 12:51:35 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 34b7d1942f56355684c8dc5b0cc3d0100c72184b)\n[//]: # (START_SECTION e4539774b3e07c22694fc62b945868db06ce4458)\n### Fixes issue #307\n\n> Commit: [e4539774b3e07c22694fc62b945868db06ce4458](https://github.com/dOpensource/dsiprouter/commit/e4539774b3e07c22694fc62b945868db06ce4458)  \n> Date: Wed, 3 Feb 2021 02:12:31 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e4539774b3e07c22694fc62b945868db06ce4458)\n[//]: # (START_SECTION eff0125c1711790734301fa41e4c4d9fa7bb9d85)\n### Added support for an INBOUND and OUTBOUND AWS Network Load Balancers\n\n> Commit: [eff0125c1711790734301fa41e4c4d9fa7bb9d85](https://github.com/dOpensource/dsiprouter/commit/eff0125c1711790734301fa41e4c4d9fa7bb9d85)  \n> Date: Tue, 2 Feb 2021 22:49:38 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eff0125c1711790734301fa41e4c4d9fa7bb9d85)\n[//]: # (START_SECTION b62c3850ef95ec00a9ee8b3fb27686002999c8fb)\n### Added support for advertising the AWS NLB when Registering to a carrier\n\n> Commit: [b62c3850ef95ec00a9ee8b3fb27686002999c8fb](https://github.com/dOpensource/dsiprouter/commit/b62c3850ef95ec00a9ee8b3fb27686002999c8fb)  \n> Date: Sat, 30 Jan 2021 20:43:48 +0000  \n> Author: root (root@ip-172-31-57-186.ec2.internal)  \n> Committer: root (root@ip-172-31-57-186.ec2.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b62c3850ef95ec00a9ee8b3fb27686002999c8fb)\n[//]: # (START_SECTION ab11ffdb7eaf4219bd35fc99e2c62029736991fc)\n### Added a health endpoint\n\n> Commit: [ab11ffdb7eaf4219bd35fc99e2c62029736991fc](https://github.com/dOpensource/dsiprouter/commit/ab11ffdb7eaf4219bd35fc99e2c62029736991fc)  \n> Date: Thu, 28 Jan 2021 21:53:12 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ab11ffdb7eaf4219bd35fc99e2c62029736991fc)\n[//]: # (START_SECTION 7105186a5f9a22d9a990f683ce7774ce2c585674)\n### Added an additional check to get hostname\n\n> Commit: [7105186a5f9a22d9a990f683ce7774ce2c585674](https://github.com/dOpensource/dsiprouter/commit/7105186a5f9a22d9a990f683ce7774ce2c585674)  \n> Date: Wed, 27 Jan 2021 13:52:54 +0000  \n> Author: Mack Hendricks (mack@dopensource.net)  \n> Committer: Mack Hendricks (mack@dopensource.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7105186a5f9a22d9a990f683ce7774ce2c585674)\n[//]: # (START_SECTION c3f5eb7aaa55c037dc5b466cdd1aab3a6fd6b98f)\n### Updated Kamailio\n\n> Commit: [c3f5eb7aaa55c037dc5b466cdd1aab3a6fd6b98f](https://github.com/dOpensource/dsiprouter/commit/c3f5eb7aaa55c037dc5b466cdd1aab3a6fd6b98f)  \n> Date: Wed, 27 Jan 2021 13:10:34 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c3f5eb7aaa55c037dc5b466cdd1aab3a6fd6b98f)\n[//]: # (START_SECTION 2a2757d6a09801fd30a29af2e0b05bb8352c1f1b)\n### Fixed issues: - LetsEncrypt certificate generation was fixed - Fixed the abililty to add MSTeams domain\n\n> Commit: [2a2757d6a09801fd30a29af2e0b05bb8352c1f1b](https://github.com/dOpensource/dsiprouter/commit/2a2757d6a09801fd30a29af2e0b05bb8352c1f1b)  \n> Date: Fri, 22 Jan 2021 02:13:58 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2a2757d6a09801fd30a29af2e0b05bb8352c1f1b)\n[//]: # (START_SECTION 3c66751cd46caf6e8744767793d15ede88a9f5f2)\n### Fixed an issue where Sync'd FusionPBX Domains were not deleted within dSIP when the Endpoint Group associated with the FusionPBX was deleted\n\n> Commit: [3c66751cd46caf6e8744767793d15ede88a9f5f2](https://github.com/dOpensource/dsiprouter/commit/3c66751cd46caf6e8744767793d15ede88a9f5f2)  \n> Date: Thu, 21 Jan 2021 15:44:29 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3c66751cd46caf6e8744767793d15ede88a9f5f2)\n[//]: # (START_SECTION 046abc390bf18c6bb5fcbd178ed711d32e507e77)\n### Renabled client side NAT Detection\n\n> Commit: [046abc390bf18c6bb5fcbd178ed711d32e507e77](https://github.com/dOpensource/dsiprouter/commit/046abc390bf18c6bb5fcbd178ed711d32e507e77)  \n> Date: Thu, 21 Jan 2021 11:52:40 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 046abc390bf18c6bb5fcbd178ed711d32e507e77)\n[//]: # (START_SECTION bd4c682017cf089392a47c3f0976649c11ba39d4)\n### Updated locatation of settings.py\n\n> Commit: [bd4c682017cf089392a47c3f0976649c11ba39d4](https://github.com/dOpensource/dsiprouter/commit/bd4c682017cf089392a47c3f0976649c11ba39d4)  \n> Date: Wed, 20 Jan 2021 18:26:02 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bd4c682017cf089392a47c3f0976649c11ba39d4)\n[//]: # (START_SECTION 9d0f4034155f29da792dad6cfe72365fdca2ba1c)\n### Changed the permissions to the backup directory\n\n> Commit: [9d0f4034155f29da792dad6cfe72365fdca2ba1c](https://github.com/dOpensource/dsiprouter/commit/9d0f4034155f29da792dad6cfe72365fdca2ba1c)  \n> Date: Wed, 20 Jan 2021 19:16:42 +0000  \n> Author: root (root@sbc4.customers.dsiprouter.net)  \n> Committer: root (root@sbc4.customers.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9d0f4034155f29da792dad6cfe72365fdca2ba1c)\n[//]: # (START_SECTION 1c3fbd9772d7e68cf002a41646dc519343a180e0)\n### Updating UI components\n\n> Commit: [1c3fbd9772d7e68cf002a41646dc519343a180e0](https://github.com/dOpensource/dsiprouter/commit/1c3fbd9772d7e68cf002a41646dc519343a180e0)  \n> Date: Wed, 20 Jan 2021 18:34:31 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1c3fbd9772d7e68cf002a41646dc519343a180e0)\n[//]: # (START_SECTION a5ecbd575c741b310e885f2c83e4027a934e6f64)\n### Fixed issue with settings.py not updating correctly\n\n> Commit: [a5ecbd575c741b310e885f2c83e4027a934e6f64](https://github.com/dOpensource/dsiprouter/commit/a5ecbd575c741b310e885f2c83e4027a934e6f64)  \n> Date: Wed, 20 Jan 2021 12:19:11 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a5ecbd575c741b310e885f2c83e4027a934e6f64)\n[//]: # (START_SECTION 0bf43b396c17bffd42906533d61fdd0d4e8711fc)\n### Fixed installer issue\n\n> Commit: [0bf43b396c17bffd42906533d61fdd0d4e8711fc](https://github.com/dOpensource/dsiprouter/commit/0bf43b396c17bffd42906533d61fdd0d4e8711fc)  \n> Date: Wed, 20 Jan 2021 11:00:18 +0000  \n> Author: root (root@sbc4.customers.dsiprouter.net)  \n> Committer: root (root@sbc4.customers.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0bf43b396c17bffd42906533d61fdd0d4e8711fc)\n[//]: # (START_SECTION 6ad575e55af96adcf468272a6c6387db9f866505)\n### Fixed installer issues\n\n> Commit: [6ad575e55af96adcf468272a6c6387db9f866505](https://github.com/dOpensource/dsiprouter/commit/6ad575e55af96adcf468272a6c6387db9f866505)  \n> Date: Wed, 20 Jan 2021 04:00:10 +0000  \n> Author: root (root@sbc4.customers.dsiprouter.net)  \n> Committer: root (root@sbc4.customers.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6ad575e55af96adcf468272a6c6387db9f866505)\n[//]: # (START_SECTION 9be69cb27221c22a01dc51da7640edd5ccac0ef6)\n### Updates - Fixed a bug that causes the motd to have the wrong external IP - Fixed an issue that prevented the Teleblock from being enabled at the Kamailio level\n\n> Commit: [9be69cb27221c22a01dc51da7640edd5ccac0ef6](https://github.com/dOpensource/dsiprouter/commit/9be69cb27221c22a01dc51da7640edd5ccac0ef6)  \n> Date: Wed, 20 Jan 2021 03:16:05 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9be69cb27221c22a01dc51da7640edd5ccac0ef6)\n[//]: # (START_SECTION 8e471cae6e6bf94b9d4feb1675242458bade2b5a)\n### Updated VI Carrier Lists\n\n> Commit: [8e471cae6e6bf94b9d4feb1675242458bade2b5a](https://github.com/dOpensource/dsiprouter/commit/8e471cae6e6bf94b9d4feb1675242458bade2b5a)  \n> Date: Mon, 18 Jan 2021 14:27:12 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8e471cae6e6bf94b9d4feb1675242458bade2b5a)\n[//]: # (START_SECTION abf8f8e90d573342aa0e8ea3bb13d507fc31ee07)\n### Fixed regression with FusionPBX Sync\n\n> Commit: [abf8f8e90d573342aa0e8ea3bb13d507fc31ee07](https://github.com/dOpensource/dsiprouter/commit/abf8f8e90d573342aa0e8ea3bb13d507fc31ee07)  \n> Date: Sun, 17 Jan 2021 04:14:05 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION abf8f8e90d573342aa0e8ea3bb13d507fc31ee07)\n[//]: # (START_SECTION 9d65dde56b73b840e1091ecbcfd9c52d3905359d)\n### Update api_routes.py\n\n> Commit: [9d65dde56b73b840e1091ecbcfd9c52d3905359d](https://github.com/dOpensource/dsiprouter/commit/9d65dde56b73b840e1091ecbcfd9c52d3905359d)  \n> Date: Sat, 16 Jan 2021 15:13:07 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9d65dde56b73b840e1091ecbcfd9c52d3905359d)\n[//]: # (START_SECTION 63d11cedc8133bbc611e4943463ba8c8bff88951)\n### GUI Updates: - Updated the sidebar so that it extends up to 2000px, which will make the UI look like it fills the screen - Fixed the idention on the backup and restore page\n\n> Commit: [63d11cedc8133bbc611e4943463ba8c8bff88951](https://github.com/dOpensource/dsiprouter/commit/63d11cedc8133bbc611e4943463ba8c8bff88951)  \n> Date: Sat, 16 Jan 2021 13:37:42 +0000  \n> Author: root (root@sbc4.customers.dsiprouter.net)  \n> Committer: root (root@sbc4.customers.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 63d11cedc8133bbc611e4943463ba8c8bff88951)\n[//]: # (START_SECTION 4f4a0f41d268ce510724e61ed4c9f77a1e23d666)\n### Fixed resetpassword - Changed the SQL command so that it included the hostname from where the connection will be coming from\n\n> Commit: [4f4a0f41d268ce510724e61ed4c9f77a1e23d666](https://github.com/dOpensource/dsiprouter/commit/4f4a0f41d268ce510724e61ed4c9f77a1e23d666)  \n> Date: Sat, 16 Jan 2021 02:28:59 +0000  \n> Author: root (root@sbc4.customers.dsiprouter.net)  \n> Committer: root (root@sbc4.customers.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4f4a0f41d268ce510724e61ed4c9f77a1e23d666)\n[//]: # (START_SECTION d6332da4c26f86cba02a24ba1cf9085a6ac49a77)\n### Added support for deleting class of service when the domain is deleted\n\n> Commit: [d6332da4c26f86cba02a24ba1cf9085a6ac49a77](https://github.com/dOpensource/dsiprouter/commit/d6332da4c26f86cba02a24ba1cf9085a6ac49a77)  \n> Date: Fri, 15 Jan 2021 20:29:35 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d6332da4c26f86cba02a24ba1cf9085a6ac49a77)\n[//]: # (START_SECTION 2f3505cebb8048a3ad9eb3aa238a4527ee8a354d)\n### Added the ability to add a voicemail during the creation of an extension\n\n> Commit: [2f3505cebb8048a3ad9eb3aa238a4527ee8a354d](https://github.com/dOpensource/dsiprouter/commit/2f3505cebb8048a3ad9eb3aa238a4527ee8a354d)  \n> Date: Fri, 15 Jan 2021 03:30:22 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2f3505cebb8048a3ad9eb3aa238a4527ee8a354d)\n[//]: # (START_SECTION eac246c60ddf8f940dc40da34fa8b458ede41004)\n### V0.641 Bug Fixes\n\n> Commit: [eac246c60ddf8f940dc40da34fa8b458ede41004](https://github.com/dOpensource/dsiprouter/commit/eac246c60ddf8f940dc40da34fa8b458ede41004)  \n> Date: Thu, 14 Jan 2021 17:47:38 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix various typos\n- fix defaults in kamcfg and settings.py\n- update settings.py to be generated on install\n- update documentation to use new settings.py path\n- update contributing guide to address settings change\n- update tests to use new settings location\n- force gzip to overwrite man pages if already created\n- add missing Vultr check for image build\n- fix reset passwords not being re-exported\n- fix error message on sync signal when dsiprouter not running\n- update rtpengine version (compilation issues on debian w/ current)\n- add rtpengine kernel module check to debian-based scripts\n\n\n---\n\n[//]: # (END_SECTION eac246c60ddf8f940dc40da34fa8b458ede41004)\n[//]: # (START_SECTION 6b8cf1705666660973df8888803d5f0c132227c1)\n### Update api_routes.py\n\n> Commit: [6b8cf1705666660973df8888803d5f0c132227c1](https://github.com/dOpensource/dsiprouter/commit/6b8cf1705666660973df8888803d5f0c132227c1)  \n> Date: Thu, 14 Jan 2021 16:51:05 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6b8cf1705666660973df8888803d5f0c132227c1)\n[//]: # (START_SECTION 13ede23fedae0ca8a411d7adb75bffbe4d2e9dd0)\n### Update api_routes.py\n\n> Commit: [13ede23fedae0ca8a411d7adb75bffbe4d2e9dd0](https://github.com/dOpensource/dsiprouter/commit/13ede23fedae0ca8a411d7adb75bffbe4d2e9dd0)  \n> Date: Thu, 14 Jan 2021 15:48:20 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 13ede23fedae0ca8a411d7adb75bffbe4d2e9dd0)\n[//]: # (START_SECTION 8871255c04351ed846eda1ecbdada488c32b6d38)\n### update to match some of our conventions\n\n> Commit: [8871255c04351ed846eda1ecbdada488c32b6d38](https://github.com/dOpensource/dsiprouter/commit/8871255c04351ed846eda1ecbdada488c32b6d38)  \n> Date: Wed, 13 Jan 2021 17:49:00 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- just some prep for merge\n\n\n---\n\n[//]: # (END_SECTION 8871255c04351ed846eda1ecbdada488c32b6d38)\n[//]: # (START_SECTION 4d2d5345c335af336b00653a4b8f727bac942837)\n### Update dsiprouter.sh\n\n> Commit: [4d2d5345c335af336b00653a4b8f727bac942837](https://github.com/dOpensource/dsiprouter/commit/4d2d5345c335af336b00653a4b8f727bac942837)  \n> Date: Wed, 13 Jan 2021 16:59:30 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4d2d5345c335af336b00653a4b8f727bac942837)\n[//]: # (START_SECTION 4ffe5caab5d0835e7783d7c552b03c4b55d15524)\n### Update dsiprouter.sh\n\n> Commit: [4ffe5caab5d0835e7783d7c552b03c4b55d15524](https://github.com/dOpensource/dsiprouter/commit/4ffe5caab5d0835e7783d7c552b03c4b55d15524)  \n> Date: Wed, 13 Jan 2021 16:56:52 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4ffe5caab5d0835e7783d7c552b03c4b55d15524)\n[//]: # (START_SECTION fcd92513a0a2aee5c64764aa5caec08946459fdf)\n### Finished implementing update and delete of extensions\n\n> Commit: [fcd92513a0a2aee5c64764aa5caec08946459fdf](https://github.com/dOpensource/dsiprouter/commit/fcd92513a0a2aee5c64764aa5caec08946459fdf)  \n> Date: Tue, 12 Jan 2021 11:58:23 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fcd92513a0a2aee5c64764aa5caec08946459fdf)\n[//]: # (START_SECTION 1b213a4b393285804fb16547590ff56e8f573544)\n### Update dsiprouter.sh\n\n> Commit: [1b213a4b393285804fb16547590ff56e8f573544](https://github.com/dOpensource/dsiprouter/commit/1b213a4b393285804fb16547590ff56e8f573544)  \n> Date: Mon, 11 Jan 2021 20:33:11 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1b213a4b393285804fb16547590ff56e8f573544)\n[//]: # (START_SECTION f34291c8c07d1a11226e6d0d6261b32954cb4779)\n### Fixed db host name character issue\n\n> Commit: [f34291c8c07d1a11226e6d0d6261b32954cb4779](https://github.com/dOpensource/dsiprouter/commit/f34291c8c07d1a11226e6d0d6261b32954cb4779)  \n> Date: Mon, 11 Jan 2021 12:49:51 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f34291c8c07d1a11226e6d0d6261b32954cb4779)\n[//]: # (START_SECTION b2facf0143258ffabd83998501dc324ea0d70b2e)\n### Update dsiprouter.sh\n\n> Commit: [b2facf0143258ffabd83998501dc324ea0d70b2e](https://github.com/dOpensource/dsiprouter/commit/b2facf0143258ffabd83998501dc324ea0d70b2e)  \n> Date: Mon, 11 Jan 2021 11:30:17 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b2facf0143258ffabd83998501dc324ea0d70b2e)\n[//]: # (START_SECTION 056aa54a151d5dd84b0dbba14764dd9689db4bb4)\n### Update dsiprouter.sh\n\n> Commit: [056aa54a151d5dd84b0dbba14764dd9689db4bb4](https://github.com/dOpensource/dsiprouter/commit/056aa54a151d5dd84b0dbba14764dd9689db4bb4)  \n> Date: Mon, 11 Jan 2021 11:27:23 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 056aa54a151d5dd84b0dbba14764dd9689db4bb4)\n[//]: # (START_SECTION ef947db13678ba04d0871075388ce2ddbd29d3e8)\n### Update dsiprouter.sh\n\n> Commit: [ef947db13678ba04d0871075388ce2ddbd29d3e8](https://github.com/dOpensource/dsiprouter/commit/ef947db13678ba04d0871075388ce2ddbd29d3e8)  \n> Date: Mon, 11 Jan 2021 11:05:04 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ef947db13678ba04d0871075388ce2ddbd29d3e8)\n[//]: # (START_SECTION b94d77600ff45d7546888426cab34a326f0c4d45)\n### Update dsiprouter.sh\n\n> Commit: [b94d77600ff45d7546888426cab34a326f0c4d45](https://github.com/dOpensource/dsiprouter/commit/b94d77600ff45d7546888426cab34a326f0c4d45)  \n> Date: Mon, 11 Jan 2021 11:01:34 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b94d77600ff45d7546888426cab34a326f0c4d45)\n[//]: # (START_SECTION e36f27762b584aace9c899de266e8a9e9cfcd4a6)\n### Update dsiprouter.sh\n\n> Commit: [e36f27762b584aace9c899de266e8a9e9cfcd4a6](https://github.com/dOpensource/dsiprouter/commit/e36f27762b584aace9c899de266e8a9e9cfcd4a6)  \n> Date: Mon, 11 Jan 2021 10:54:19 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e36f27762b584aace9c899de266e8a9e9cfcd4a6)\n[//]: # (START_SECTION c5a6ef31fab26d2bd21351959a0527bde9540736)\n### Update dsiprouter.1\n\n> Commit: [c5a6ef31fab26d2bd21351959a0527bde9540736](https://github.com/dOpensource/dsiprouter/commit/c5a6ef31fab26d2bd21351959a0527bde9540736)  \n> Date: Sat, 9 Jan 2021 12:38:13 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c5a6ef31fab26d2bd21351959a0527bde9540736)\n[//]: # (START_SECTION 257da44dfc069b910558e7fdc365ca204e44f9a0)\n### Update dsiprouter.sh\n\n> Commit: [257da44dfc069b910558e7fdc365ca204e44f9a0](https://github.com/dOpensource/dsiprouter/commit/257da44dfc069b910558e7fdc365ca204e44f9a0)  \n> Date: Sat, 9 Jan 2021 12:29:27 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 257da44dfc069b910558e7fdc365ca204e44f9a0)\n[//]: # (START_SECTION 1957202b3c57103ff6a2ecf9dba1b7caa59741ec)\n### Create dsiprouter.1\n\n> Commit: [1957202b3c57103ff6a2ecf9dba1b7caa59741ec](https://github.com/dOpensource/dsiprouter/commit/1957202b3c57103ff6a2ecf9dba1b7caa59741ec)  \n> Date: Sat, 9 Jan 2021 12:25:23 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1957202b3c57103ff6a2ecf9dba1b7caa59741ec)\n[//]: # (START_SECTION 24d2dd3c20990f6c38362e96cd31b7905ed72c60)\n### Set the setting.py file back to the defaults\n\n> Commit: [24d2dd3c20990f6c38362e96cd31b7905ed72c60](https://github.com/dOpensource/dsiprouter/commit/24d2dd3c20990f6c38362e96cd31b7905ed72c60)  \n> Date: Tue, 5 Jan 2021 03:26:02 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 24d2dd3c20990f6c38362e96cd31b7905ed72c60)\n[//]: # (START_SECTION e7c81b1ddb71e72ae3f9fbe77c3447020240e308)\n### Fixed an issue that prevented the credentials from being set when installing dSIP\n\n> Commit: [e7c81b1ddb71e72ae3f9fbe77c3447020240e308](https://github.com/dOpensource/dsiprouter/commit/e7c81b1ddb71e72ae3f9fbe77c3447020240e308)  \n> Date: Tue, 5 Jan 2021 03:24:10 +0000  \n> Author: root (root@sbc4.customers.dsiprouter.net)  \n> Committer: root (root@sbc4.customers.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e7c81b1ddb71e72ae3f9fbe77c3447020240e308)\n[//]: # (START_SECTION 59d25e84e0d747cb7e8fd10cd60df6689fe0b3bc)\n### Disabld Fraud Detection module\n\n> Commit: [59d25e84e0d747cb7e8fd10cd60df6689fe0b3bc](https://github.com/dOpensource/dsiprouter/commit/59d25e84e0d747cb7e8fd10cd60df6689fe0b3bc)  \n> Date: Mon, 4 Jan 2021 22:14:44 +0000  \n> Author: root (root@sbc4.customers.dsiprouter.net)  \n> Committer: root (root@sbc4.customers.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 59d25e84e0d747cb7e8fd10cd60df6689fe0b3bc)\n[//]: # (START_SECTION f80ef93a71dff7ae697bff7264a3cebf74c28163)\n### Fixes For Remote DB Configuration\n\n> Commit: [f80ef93a71dff7ae697bff7264a3cebf74c28163](https://github.com/dOpensource/dsiprouter/commit/f80ef93a71dff7ae697bff7264a3cebf74c28163)  \n> Date: Wed, 30 Dec 2020 23:58:05 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- # WIP for #326\n- # WIP for #307\n- add dsip version to kam Server header\n- remove `setkamdbconfig` command\n- update `setcredentials` command w/ root DB support\n- update `install` command w/ root DB support\n- change `DOMAIN` in `settings.py` to `DEFAULT_AUTH_DOMAIN`\n- fix root db access for dsip module installation\n- fix root db access on other CLI commands after install\n- move mysql install to before kamailio install\n- various command parsing improvements\n- renaming and better formatting for script variables\n- add rootdb credntial support to `security.setCreds()`\n- add some DB interfacing functions to `dsip_lib.sh`\n\n\n---\n\n[//]: # (END_SECTION f80ef93a71dff7ae697bff7264a3cebf74c28163)\n[//]: # (START_SECTION f4a00e67519f6fbd9399db44a880634489676e5e)\n### Fixed\n\n> Commit: [f4a00e67519f6fbd9399db44a880634489676e5e](https://github.com/dOpensource/dsiprouter/commit/f4a00e67519f6fbd9399db44a880634489676e5e)  \n> Date: Wed, 30 Dec 2020 21:36:30 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f4a00e67519f6fbd9399db44a880634489676e5e)\n[//]: # (START_SECTION 537136db933b369ff0dadd0a35b0f8483fd1f5dc)\n### Fixed Issue #292 - Changed the logic to check the status of MSTeams Option Messages using kamcmd to using the local Kamailio API endpoint\n\n> Commit: [537136db933b369ff0dadd0a35b0f8483fd1f5dc](https://github.com/dOpensource/dsiprouter/commit/537136db933b369ff0dadd0a35b0f8483fd1f5dc)  \n> Date: Wed, 30 Dec 2020 21:20:54 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 537136db933b369ff0dadd0a35b0f8483fd1f5dc)\n[//]: # (START_SECTION e40a3e109cbaff6d5f4ae9b1cdae253f96b3d407)\n### Updates - Added support for reading one or more extension from a FusionPBX domain\n\n> Commit: [e40a3e109cbaff6d5f4ae9b1cdae253f96b3d407](https://github.com/dOpensource/dsiprouter/commit/e40a3e109cbaff6d5f4ae9b1cdae253f96b3d407)  \n> Date: Wed, 30 Dec 2020 12:20:36 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e40a3e109cbaff6d5f4ae9b1cdae253f96b3d407)\n[//]: # (START_SECTION d6d27ab8bb68fe69433a04eb5854656dc88e7ef3)\n### Updating Media Service API Domains - Added support for updating and deleting domains\n\n> Commit: [d6d27ab8bb68fe69433a04eb5854656dc88e7ef3](https://github.com/dOpensource/dsiprouter/commit/d6d27ab8bb68fe69433a04eb5854656dc88e7ef3)  \n> Date: Tue, 29 Dec 2020 05:09:21 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d6d27ab8bb68fe69433a04eb5854656dc88e7ef3)\n[//]: # (START_SECTION 5a193675fb7907390631c620e2f98a12266347e8)\n### Added support for provisioning extensions\n\n> Commit: [5a193675fb7907390631c620e2f98a12266347e8](https://github.com/dOpensource/dsiprouter/commit/5a193675fb7907390631c620e2f98a12266347e8)  \n> Date: Thu, 24 Dec 2020 02:00:37 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5a193675fb7907390631c620e2f98a12266347e8)\n[//]: # (START_SECTION 7ddc87fff9a6e124d2ccb1ec772352c4932446f9)\n### Update Location Routing\n\n> Commit: [7ddc87fff9a6e124d2ccb1ec772352c4932446f9](https://github.com/dOpensource/dsiprouter/commit/7ddc87fff9a6e124d2ccb1ec772352c4932446f9)  \n> Date: Wed, 23 Dec 2020 11:18:34 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add check for request domain for non-compliant UA's\n\n\n---\n\n[//]: # (END_SECTION 7ddc87fff9a6e124d2ccb1ec772352c4932446f9)\n[//]: # (START_SECTION 48df26485429a3c1e785dfa82a1d64a4f3adda73)\n### Fixed DO Name Error In Terraform\n\n> Commit: [48df26485429a3c1e785dfa82a1d64a4f3adda73](https://github.com/dOpensource/dsiprouter/commit/48df26485429a3c1e785dfa82a1d64a4f3adda73)  \n> Date: Wed, 23 Dec 2020 10:25:20 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 48df26485429a3c1e785dfa82a1d64a4f3adda73)\n[//]: # (START_SECTION 2ffa80464ecb90ee77856939114833c074327cb0)\n### Media Server API - Added support for creating a domain\n\n> Commit: [2ffa80464ecb90ee77856939114833c074327cb0](https://github.com/dOpensource/dsiprouter/commit/2ffa80464ecb90ee77856939114833c074327cb0)  \n> Date: Wed, 23 Dec 2020 06:02:58 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2ffa80464ecb90ee77856939114833c074327cb0)\n[//]: # (START_SECTION 0dc523c62986c94ec14f59cf522b4d539420f5f5)\n### Fix Typo In Location Route\n\n> Commit: [0dc523c62986c94ec14f59cf522b4d539420f5f5](https://github.com/dOpensource/dsiprouter/commit/0dc523c62986c94ec14f59cf522b4d539420f5f5)  \n> Date: Tue, 22 Dec 2020 14:33:50 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- change location routing to use from domain\n\n\n---\n\n[//]: # (END_SECTION 0dc523c62986c94ec14f59cf522b4d539420f5f5)\n[//]: # (START_SECTION 646167fb8dc29f3b080ddcf112b5e75dfbdbb9f4)\n### Fix Record Routing For SERVERNAT\n\n> Commit: [646167fb8dc29f3b080ddcf112b5e75dfbdbb9f4](https://github.com/dOpensource/dsiprouter/commit/646167fb8dc29f3b080ddcf112b5e75dfbdbb9f4)  \n> Date: Tue, 22 Dec 2020 10:46:00 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix NAT translation in record routes\n- remove extraneous file `script.sh`\n\n\n---\n\n[//]: # (END_SECTION 646167fb8dc29f3b080ddcf112b5e75dfbdbb9f4)\n[//]: # (START_SECTION 191cbc48d3813ccbf1d12681fe1084fab3c36696)\n### Fixed issue with certificate permissions\n\n> Commit: [191cbc48d3813ccbf1d12681fe1084fab3c36696](https://github.com/dOpensource/dsiprouter/commit/191cbc48d3813ccbf1d12681fe1084fab3c36696)  \n> Date: Mon, 21 Dec 2020 18:27:08 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 191cbc48d3813ccbf1d12681fe1084fab3c36696)\n[//]: # (START_SECTION d4dbff15c352ceded8459413be316c62bdcb58ad)\n### FusionPBX Plugin for Media Server - Added logic to dynamically load the FusionPBX plugin - Added the basic interface for a MediaServer Plugin - Added specific logic for the FusionPBX MediaServer Plugin\n\n> Commit: [d4dbff15c352ceded8459413be316c62bdcb58ad](https://github.com/dOpensource/dsiprouter/commit/d4dbff15c352ceded8459413be316c62bdcb58ad)  \n> Date: Sun, 20 Dec 2020 17:14:43 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- Issue #316\n\n\n---\n\n[//]: # (END_SECTION d4dbff15c352ceded8459413be316c62bdcb58ad)\n[//]: # (START_SECTION f8c73f821435298e7e6c388b26a98028b03d9ce3)\n### Add to Media Server API - Added Configuration Checks - Added GET Action for Domain - Added the basis layout of the media server plugin architecture\n\n> Commit: [f8c73f821435298e7e6c388b26a98028b03d9ce3](https://github.com/dOpensource/dsiprouter/commit/f8c73f821435298e7e6c388b26a98028b03d9ce3)  \n> Date: Sat, 19 Dec 2020 02:31:27 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f8c73f821435298e7e6c388b26a98028b03d9ce3)\n[//]: # (START_SECTION a77d93c7735658c3a04a7b6650607bb126ee2b53)\n### Fixed an issue that prevented the Endpoint Group name from being presented in the Inbound Mapping page\n\n> Commit: [a77d93c7735658c3a04a7b6650607bb126ee2b53](https://github.com/dOpensource/dsiprouter/commit/a77d93c7735658c3a04a7b6650607bb126ee2b53)  \n> Date: Wed, 16 Dec 2020 17:34:22 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a77d93c7735658c3a04a7b6650607bb126ee2b53)\n[//]: # (START_SECTION b72dc72adcf40fb62553db673ff70eef39220c96)\n### Fixed logic for handling inbound calling thats routing to a DISPATCHER set\n\n> Commit: [b72dc72adcf40fb62553db673ff70eef39220c96](https://github.com/dOpensource/dsiprouter/commit/b72dc72adcf40fb62553db673ff70eef39220c96)  \n> Date: Wed, 16 Dec 2020 11:25:34 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b72dc72adcf40fb62553db673ff70eef39220c96)\n[//]: # (START_SECTION 17f2e8702863666dd08403219099347711724310)\n### FusionPBX Sync Support - Added cluster support to FusionPBX sync\n\n> Commit: [17f2e8702863666dd08403219099347711724310](https://github.com/dOpensource/dsiprouter/commit/17f2e8702863666dd08403219099347711724310)  \n> Date: Tue, 15 Dec 2020 18:09:12 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 17f2e8702863666dd08403219099347711724310)\n[//]: # (START_SECTION 8f0d229dd89916ebe800228e94518a6ff2beda3f)\n### Fixes issue #53 amd #297\n\n> Commit: [8f0d229dd89916ebe800228e94518a6ff2beda3f](https://github.com/dOpensource/dsiprouter/commit/8f0d229dd89916ebe800228e94518a6ff2beda3f)  \n> Date: Tue, 15 Dec 2020 12:08:26 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8f0d229dd89916ebe800228e94518a6ff2beda3f)\n[//]: # (START_SECTION 58df9f2d81b8094ff3f29a1c22b4de1b6d7f9eeb)\n### Fixed syntax error\n\n> Commit: [58df9f2d81b8094ff3f29a1c22b4de1b6d7f9eeb](https://github.com/dOpensource/dsiprouter/commit/58df9f2d81b8094ff3f29a1c22b4de1b6d7f9eeb)  \n> Date: Tue, 15 Dec 2020 11:34:05 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 58df9f2d81b8094ff3f29a1c22b4de1b6d7f9eeb)\n[//]: # (START_SECTION eeeb6cf0977ed3ada7ae47fd5607d0e22536b191)\n### Media Server API - Inital commit for issue #316 - Moved the API security decorator to a shared function - Added instructions on how to add a new API and a sample API python script\n\n> Commit: [eeeb6cf0977ed3ada7ae47fd5607d0e22536b191](https://github.com/dOpensource/dsiprouter/commit/eeeb6cf0977ed3ada7ae47fd5607d0e22536b191)  \n> Date: Sun, 13 Dec 2020 12:57:11 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eeeb6cf0977ed3ada7ae47fd5607d0e22536b191)\n[//]: # (START_SECTION c92729b89c5365414978f70453704d099e491eb9)\n### Key / Certificate Handling Updates\n\n> Commit: [c92729b89c5365414978f70453704d099e491eb9](https://github.com/dOpensource/dsiprouter/commit/c92729b89c5365414978f70453704d099e491eb9)  \n> Date: Wed, 9 Dec 2020 18:50:08 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #314\n- fix fusionpbx sync not deleting old domains\n- fix host:port parsing on fusionpbx sync\n- add certificate detection on upload\n- add cert / key handling utility functions\n- add cert / key validation on upload\n- change default key / cert naming convention\n- add `DSIP_SSL_CA` parameter to modifiable settings\n- fix subscriber delete DB error on contact expiration\n- update default key lengths to be more secure\n- update cert generation to use correct permissions\n- fixup namespace clobbering in `api_routes.py`\n- remove option to disable SSL on install\n- fix nginx not starting on debug startup\n- fix some misc path names\n\n\n---\n\n[//]: # (END_SECTION c92729b89c5365414978f70453704d099e491eb9)\n[//]: # (START_SECTION eff1a89624a28f73f080b92d3569a746933518b0)\n### Fixed - Updated and deleting from Endpoint Groups that contain Weights\n\n> Commit: [eff1a89624a28f73f080b92d3569a746933518b0](https://github.com/dOpensource/dsiprouter/commit/eff1a89624a28f73f080b92d3569a746933518b0)  \n> Date: Wed, 9 Dec 2020 22:56:48 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eff1a89624a28f73f080b92d3569a746933518b0)\n[//]: # (START_SECTION b52e1ef20efd0bae3bb2749d28ce01bc59f8c84d)\n### Restructured Terraform Configuration\n\n> Commit: [b52e1ef20efd0bae3bb2749d28ce01bc59f8c84d](https://github.com/dOpensource/dsiprouter/commit/b52e1ef20efd0bae3bb2749d28ce01bc59f8c84d)  \n> Date: Tue, 8 Dec 2020 09:49:27 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b52e1ef20efd0bae3bb2749d28ce01bc59f8c84d)\n[//]: # (START_SECTION 32581310a59093e773751cd4753a3e156e89e7e8)\n### Fixed Inbound Mapping to allow FusionPBX Load Balancing\n\n> Commit: [32581310a59093e773751cd4753a3e156e89e7e8](https://github.com/dOpensource/dsiprouter/commit/32581310a59093e773751cd4753a3e156e89e7e8)  \n> Date: Mon, 7 Dec 2020 19:38:19 +0000  \n> Author: root (root@mack-dsip-deb10.dsiprouter.org)  \n> Committer: root (root@mack-dsip-deb10.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 32581310a59093e773751cd4753a3e156e89e7e8)\n[//]: # (START_SECTION 33535467399804add0ea69b5e7faf71c695014be)\n### v0.641 Bug Fixes\n\n> Commit: [33535467399804add0ea69b5e7faf71c695014be](https://github.com/dOpensource/dsiprouter/commit/33535467399804add0ea69b5e7faf71c695014be)  \n> Date: Mon, 7 Dec 2020 10:47:54 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Fixes [#310](https://github.com/dOpensource/dsiprouter/issues/310)\n- fix DNID enrichment enabled before tables installed\n- enable DMQ by default\n- increase external IP resolution timeout\n- add libwebsockets-dev dependency for next release of RTPENGINE\n- fix permissions issues with certificate upload\n\n\n---\n\n[//]: # (END_SECTION 33535467399804add0ea69b5e7faf71c695014be)\n[//]: # (START_SECTION 21798511e6d33e40efea39d6ab68c640693e81cb)\n### Updated the Endpoint Group and Inbound Mapping sections with more specific detail on how to use the new features\n\n> Commit: [21798511e6d33e40efea39d6ab68c640693e81cb](https://github.com/dOpensource/dsiprouter/commit/21798511e6d33e40efea39d6ab68c640693e81cb)  \n> Date: Sat, 5 Dec 2020 09:04:26 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 21798511e6d33e40efea39d6ab68c640693e81cb)\n[//]: # (START_SECTION 886a9550c56d9317dd85494e1719a3229348fb3d)\n### Updated doc's\n\n> Commit: [886a9550c56d9317dd85494e1719a3229348fb3d](https://github.com/dOpensource/dsiprouter/commit/886a9550c56d9317dd85494e1719a3229348fb3d)  \n> Date: Fri, 4 Dec 2020 12:31:22 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 886a9550c56d9317dd85494e1719a3229348fb3d)\n[//]: # (START_SECTION d877f331b9bc248b9880c2edb099104cea420fac)\n### Update conf.py\n\n> Commit: [d877f331b9bc248b9880c2edb099104cea420fac](https://github.com/dOpensource/dsiprouter/commit/d877f331b9bc248b9880c2edb099104cea420fac)  \n> Date: Fri, 4 Dec 2020 08:31:30 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d877f331b9bc248b9880c2edb099104cea420fac)\n[//]: # (START_SECTION d585223c6a69512f4cb56d522d460296599a69e6)\n### Update conf.py\n\n> Commit: [d585223c6a69512f4cb56d522d460296599a69e6](https://github.com/dOpensource/dsiprouter/commit/d585223c6a69512f4cb56d522d460296599a69e6)  \n> Date: Fri, 4 Dec 2020 08:28:36 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d585223c6a69512f4cb56d522d460296599a69e6)\n[//]: # (START_SECTION 2a2131710df199e88189091ab16f307bb1089988)\n### Update conf.py\n\n> Commit: [2a2131710df199e88189091ab16f307bb1089988](https://github.com/dOpensource/dsiprouter/commit/2a2131710df199e88189091ab16f307bb1089988)  \n> Date: Fri, 4 Dec 2020 07:21:15 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2a2131710df199e88189091ab16f307bb1089988)\n[//]: # (START_SECTION 7da872d4edc18917deeedc4dde12f9bbe57a500d)\n### Update .readthedocs.yml\n\n> Commit: [7da872d4edc18917deeedc4dde12f9bbe57a500d](https://github.com/dOpensource/dsiprouter/commit/7da872d4edc18917deeedc4dde12f9bbe57a500d)  \n> Date: Fri, 4 Dec 2020 07:07:46 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7da872d4edc18917deeedc4dde12f9bbe57a500d)\n[//]: # (START_SECTION 025e5ec18575f08d2306a2c9ca99cc87981e9ee9)\n### Create conf.py\n\n> Commit: [025e5ec18575f08d2306a2c9ca99cc87981e9ee9](https://github.com/dOpensource/dsiprouter/commit/025e5ec18575f08d2306a2c9ca99cc87981e9ee9)  \n> Date: Thu, 3 Dec 2020 19:05:46 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 025e5ec18575f08d2306a2c9ca99cc87981e9ee9)\n[//]: # (START_SECTION e809c5c57ffdf627915db95e5f94e895f90291bb)\n### Update .readthedocs.yml\n\n> Commit: [e809c5c57ffdf627915db95e5f94e895f90291bb](https://github.com/dOpensource/dsiprouter/commit/e809c5c57ffdf627915db95e5f94e895f90291bb)  \n> Date: Thu, 3 Dec 2020 19:04:58 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e809c5c57ffdf627915db95e5f94e895f90291bb)\n[//]: # (START_SECTION 3db150d5c0f6dd107902b3656026e18711f7b959)\n### Update .readthedocs.yml\n\n> Commit: [3db150d5c0f6dd107902b3656026e18711f7b959](https://github.com/dOpensource/dsiprouter/commit/3db150d5c0f6dd107902b3656026e18711f7b959)  \n> Date: Thu, 3 Dec 2020 19:00:54 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3db150d5c0f6dd107902b3656026e18711f7b959)\n[//]: # (START_SECTION 2371850c27408aea29c0e98b792db5b548a32eb9)\n### Update .readthedocs.yml\n\n> Commit: [2371850c27408aea29c0e98b792db5b548a32eb9](https://github.com/dOpensource/dsiprouter/commit/2371850c27408aea29c0e98b792db5b548a32eb9)  \n> Date: Thu, 3 Dec 2020 18:40:44 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2371850c27408aea29c0e98b792db5b548a32eb9)\n[//]: # (START_SECTION 03493c243c12f606f1a5716841c2e7eb4185f32c)\n### Delete requirements.txt\n\n> Commit: [03493c243c12f606f1a5716841c2e7eb4185f32c](https://github.com/dOpensource/dsiprouter/commit/03493c243c12f606f1a5716841c2e7eb4185f32c)  \n> Date: Thu, 3 Dec 2020 18:38:59 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 03493c243c12f606f1a5716841c2e7eb4185f32c)\n[//]: # (START_SECTION 916e120ec3bbf7ea7089e93b8386b0facf00c4a0)\n### Update requirements.txt\n\n> Commit: [916e120ec3bbf7ea7089e93b8386b0facf00c4a0](https://github.com/dOpensource/dsiprouter/commit/916e120ec3bbf7ea7089e93b8386b0facf00c4a0)  \n> Date: Thu, 3 Dec 2020 18:32:22 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 916e120ec3bbf7ea7089e93b8386b0facf00c4a0)\n[//]: # (START_SECTION 76c7c679a947122e08cc5985623bbefbeb0681c7)\n### Update .readthedocs.yml\n\n> Commit: [76c7c679a947122e08cc5985623bbefbeb0681c7](https://github.com/dOpensource/dsiprouter/commit/76c7c679a947122e08cc5985623bbefbeb0681c7)  \n> Date: Thu, 3 Dec 2020 18:28:37 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 76c7c679a947122e08cc5985623bbefbeb0681c7)\n[//]: # (START_SECTION 085931328e9be893c65b674c9e491924afa88ca4)\n### Update requirements.txt\n\n> Commit: [085931328e9be893c65b674c9e491924afa88ca4](https://github.com/dOpensource/dsiprouter/commit/085931328e9be893c65b674c9e491924afa88ca4)  \n> Date: Thu, 3 Dec 2020 18:24:52 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 085931328e9be893c65b674c9e491924afa88ca4)\n[//]: # (START_SECTION c569928e35f25ab269b0aeab25423093f9db96b5)\n### Delete requirements.txt\n\n> Commit: [c569928e35f25ab269b0aeab25423093f9db96b5](https://github.com/dOpensource/dsiprouter/commit/c569928e35f25ab269b0aeab25423093f9db96b5)  \n> Date: Thu, 3 Dec 2020 18:24:16 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c569928e35f25ab269b0aeab25423093f9db96b5)\n[//]: # (START_SECTION 195bd8e7cc20468054182bb08fac2e80b80a0bea)\n### Create requirements.txt\n\n> Commit: [195bd8e7cc20468054182bb08fac2e80b80a0bea](https://github.com/dOpensource/dsiprouter/commit/195bd8e7cc20468054182bb08fac2e80b80a0bea)  \n> Date: Thu, 3 Dec 2020 18:23:23 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 195bd8e7cc20468054182bb08fac2e80b80a0bea)\n[//]: # (START_SECTION 14d26b51fa983c686b95a60afdfb145ea61c1bef)\n### Update .readthedocs.yml\n\n> Commit: [14d26b51fa983c686b95a60afdfb145ea61c1bef](https://github.com/dOpensource/dsiprouter/commit/14d26b51fa983c686b95a60afdfb145ea61c1bef)  \n> Date: Thu, 3 Dec 2020 12:13:55 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 14d26b51fa983c686b95a60afdfb145ea61c1bef)\n[//]: # (START_SECTION cff105fd47bada8ce5bafe07a5e6f85ac2b8579e)\n### Create requirements.txt\n\n> Commit: [cff105fd47bada8ce5bafe07a5e6f85ac2b8579e](https://github.com/dOpensource/dsiprouter/commit/cff105fd47bada8ce5bafe07a5e6f85ac2b8579e)  \n> Date: Thu, 3 Dec 2020 12:08:58 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cff105fd47bada8ce5bafe07a5e6f85ac2b8579e)\n[//]: # (START_SECTION 85874fade92d329fb0f6d7f824e06035f83eee97)\n### Create .readthedocs.yml\n\n> Commit: [85874fade92d329fb0f6d7f824e06035f83eee97](https://github.com/dOpensource/dsiprouter/commit/85874fade92d329fb0f6d7f824e06035f83eee97)  \n> Date: Thu, 3 Dec 2020 12:07:35 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 85874fade92d329fb0f6d7f824e06035f83eee97)\n[//]: # (START_SECTION 681a80c258d2011826c12d52874924843f17728e)\n### Updates to FusionPBX Cluster Support: - Added support to inbound routes to map a DID to the Internal or External interface of a FusionPBX system\n\n> Commit: [681a80c258d2011826c12d52874924843f17728e](https://github.com/dOpensource/dsiprouter/commit/681a80c258d2011826c12d52874924843f17728e)  \n> Date: Wed, 2 Dec 2020 12:15:23 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 681a80c258d2011826c12d52874924843f17728e)\n[//]: # (START_SECTION 9fc1b9c95e1c930caa9a711477e29f8ac94f8534)\n### Merge Number Enrichment Into Current Build\n\n> Commit: [9fc1b9c95e1c930caa9a711477e29f8ac94f8534](https://github.com/dOpensource/dsiprouter/commit/9fc1b9c95e1c930caa9a711477e29f8ac94f8534)  \n> Date: Tue, 1 Dec 2020 19:15:55 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves [#310](https://github.com/dOpensource/dsiprouter/issues/310)\n- merge in number enrichment feature\n- move teleblock settings to system settings menu\n- fix a few bash completions for dsiprouter commands\n- update upgrade command syntax to match others\n- misc CSS merges from enterprise repo\n- make module install scripts propagate return code\n- cleanup various scripts / configs / source files\n- update version in `settings.py`\n\n\n---\n\n[//]: # (END_SECTION 9fc1b9c95e1c930caa9a711477e29f8ac94f8534)\n[//]: # (START_SECTION 78bb2e6ffb5d7e3d4d78ef3128bddb5f1f83104f)\n### Updates to FusionPBX Cluster Support: - Added the ability to define an Endpoint Group as a FusionPBX Cluster versus a standalone FusionPBX Server - 2 dispatcher set's are created for each endpoint group, one for internal calls and another external calls on port 5080\n\n> Commit: [78bb2e6ffb5d7e3d4d78ef3128bddb5f1f83104f](https://github.com/dOpensource/dsiprouter/commit/78bb2e6ffb5d7e3d4d78ef3128bddb5f1f83104f)  \n> Date: Tue, 1 Dec 2020 23:14:51 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 78bb2e6ffb5d7e3d4d78ef3128bddb5f1f83104f)\n[//]: # (START_SECTION f74db4f6542d0f008fef614440e195ff724a7879)\n### Allow TCP UAC SIP Connections By Default\n\n> Commit: [f74db4f6542d0f008fef614440e195ff724a7879](https://github.com/dOpensource/dsiprouter/commit/f74db4f6542d0f008fef614440e195ff724a7879)  \n> Date: Tue, 1 Dec 2020 17:08:50 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves [#255](https://github.com/dOpensource/dsiprouter/issues/255)\n- add listen directives on tcp socket to kamailio config\n\n\n---\n\n[//]: # (END_SECTION f74db4f6542d0f008fef614440e195ff724a7879)\n[//]: # (START_SECTION a409f4049744053e5958ea885b5d68524f15c6c4)\n### Update NAT Handling\n\n> Commit: [a409f4049744053e5958ea885b5d68524f15c6c4](https://github.com/dOpensource/dsiprouter/commit/a409f4049744053e5958ea885b5d68524f15c6c4)  \n> Date: Tue, 1 Dec 2020 17:04:19 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves [#311](https://github.com/dOpensource/dsiprouter/issues/311)\n- add client-side NAT translation for UAC Contact\n\n\n---\n\n[//]: # (END_SECTION a409f4049744053e5958ea885b5d68524f15c6c4)\n[//]: # (START_SECTION d182417156ac4af142138abe951aefd6dea35259)\n### FusionPBX Cluster Support Enhancements - Define a 30 second expiration to the pass_thru htable that keeps track of 401 and 407 auth request made by a media server and sends the request back to the same backend media server - Changed the address group that Kamailio uses to perform an internal route for DR_ROUTING to DISPATCHER use cases\n\n> Commit: [d182417156ac4af142138abe951aefd6dea35259](https://github.com/dOpensource/dsiprouter/commit/d182417156ac4af142138abe951aefd6dea35259)  \n> Date: Mon, 30 Nov 2020 05:03:28 +0000  \n> Author: root (root@nightly-deb9.dsiprouter.org)  \n> Committer: root (root@nightly-deb9.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d182417156ac4af142138abe951aefd6dea35259)\n[//]: # (START_SECTION 9e45548eef592b97967607829e844913b6ced577)\n### Added support for routing to multiple FusionPBX systems\n\n> Commit: [9e45548eef592b97967607829e844913b6ced577](https://github.com/dOpensource/dsiprouter/commit/9e45548eef592b97967607829e844913b6ced577)  \n> Date: Mon, 23 Nov 2020 10:14:56 +0000  \n> Author: root (root@testing-dsiprouter0.b3paytcmeseudeqwsc2313xkvc.ex.internal.cloudapp.net)  \n> Committer: root (root@nightly-deb9.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9e45548eef592b97967607829e844913b6ced577)\n[//]: # (START_SECTION 86f9fc51b25f749858999ded032aebda17e7aa14)\n### Added a restart of Kamailio after installing dSIPRouter to ensure it has the latest database connection info\n\n> Commit: [86f9fc51b25f749858999ded032aebda17e7aa14](https://github.com/dOpensource/dsiprouter/commit/86f9fc51b25f749858999ded032aebda17e7aa14)  \n> Date: Fri, 27 Nov 2020 13:22:34 +0000  \n> Author: root (root@nightly-deb9.dsiprouter.org)  \n> Committer: root (root@nightly-deb9.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 86f9fc51b25f749858999ded032aebda17e7aa14)\n[//]: # (START_SECTION 24a5c2115889e874b9c95a9ca1ffc59c80932d13)\n### Added the default auth domain to the username/password auth screen\n\n> Commit: [24a5c2115889e874b9c95a9ca1ffc59c80932d13](https://github.com/dOpensource/dsiprouter/commit/24a5c2115889e874b9c95a9ca1ffc59c80932d13)  \n> Date: Thu, 26 Nov 2020 14:22:36 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 24a5c2115889e874b9c95a9ca1ffc59c80932d13)\n[//]: # (START_SECTION 3333a6ec58d3c2530f33cf8132c18862c29d74ce)\n### Improve Endpoint Group Address Entry Mapping\n\n> Commit: [3333a6ec58d3c2530f33cf8132c18862c29d74ce](https://github.com/dOpensource/dsiprouter/commit/3333a6ec58d3c2530f33cf8132c18862c29d74ce)  \n> Date: Wed, 25 Nov 2020 16:59:23 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix address not deleted when endpoint group deleted\n- fix address not updated when changing from ip->user/pw auth and vice versa\n- fix bash auto completion for centos\n- fix dsiprouter auto completion not in effect until re-login\n\n\n---\n\n[//]: # (END_SECTION 3333a6ec58d3c2530f33cf8132c18862c29d74ce)\n[//]: # (START_SECTION 702a0ac1811870197e64aa3dbaa04b9d9afea29f)\n### Added ThinkTel as carrier\n\n> Commit: [702a0ac1811870197e64aa3dbaa04b9d9afea29f](https://github.com/dOpensource/dsiprouter/commit/702a0ac1811870197e64aa3dbaa04b9d9afea29f)  \n> Date: Wed, 25 Nov 2020 17:37:03 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 702a0ac1811870197e64aa3dbaa04b9d9afea29f)\n[//]: # (START_SECTION 349e8b733e16eebb7ed693ff5bc0588a3b12cce4)\n### Update CONTRIBUTORS.md\n\n> Commit: [349e8b733e16eebb7ed693ff5bc0588a3b12cce4](https://github.com/dOpensource/dsiprouter/commit/349e8b733e16eebb7ed693ff5bc0588a3b12cce4)  \n> Date: Wed, 25 Nov 2020 08:35:39 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 349e8b733e16eebb7ed693ff5bc0588a3b12cce4)\n[//]: # (START_SECTION 04f2417b5cdc29bc08e217f8bf6f2a0cf07fa3a8)\n### Fix Outbound Route Update Edge Case\n\n> Commit: [04f2417b5cdc29bc08e217f8bf6f2a0cf07fa3a8](https://github.com/dOpensource/dsiprouter/commit/04f2417b5cdc29bc08e217f8bf6f2a0cf07fa3a8)  \n> Date: Tue, 24 Nov 2020 19:20:08 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix bug updating outbound routes when LCR routing entry exists\n- fix centos check for rtpengine recording RPM on rtpengine install\n- add centos fixes to amazon linux rtpengine install\n\n\n---\n\n[//]: # (END_SECTION 04f2417b5cdc29bc08e217f8bf6f2a0cf07fa3a8)\n[//]: # (START_SECTION 5793453bc1f2476b4361c762611da08325fded96)\n### Fix Fail2ban\n\n> Commit: [5793453bc1f2476b4361c762611da08325fded96](https://github.com/dOpensource/dsiprouter/commit/5793453bc1f2476b4361c762611da08325fded96)  \n> Date: Tue, 24 Nov 2020 11:44:18 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- -Used correct quotes\n\n\n---\n\n[//]: # (END_SECTION 5793453bc1f2476b4361c762611da08325fded96)\n[//]: # (START_SECTION 62ba43bb6af91568e53e5ddcfe29f4740ae41788)\n### fix broken rsyslog.service - only remove ; comment tags\n\n> Commit: [62ba43bb6af91568e53e5ddcfe29f4740ae41788](https://github.com/dOpensource/dsiprouter/commit/62ba43bb6af91568e53e5ddcfe29f4740ae41788)  \n> Date: Tue, 24 Nov 2020 01:40:37 -0700  \n> Author: reqlez (6512602+reqlez@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Disable removing \"#\" comments tags and only remove \";\" comment tags instead, otherwise, installing RTPENGINE under CentOS breaks rsyslog.service\r\n\n- By the way, dsiprouter logging is still broken even after this fix, I suspect there is another issue, possible with sysloginit.py  but have not been able to figure out yet.\n\n\n---\n\n[//]: # (END_SECTION 62ba43bb6af91568e53e5ddcfe29f4740ae41788)\n[//]: # (START_SECTION 9ef7eabc2075f31510565bf75226e46d30511cf6)\n### Patch Old Host Variables\n\n> Commit: [9ef7eabc2075f31510565bf75226e46d30511cf6](https://github.com/dOpensource/dsiprouter/commit/9ef7eabc2075f31510565bf75226e46d30511cf6)  \n> Date: Mon, 23 Nov 2020 16:25:46 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 9ef7eabc2075f31510565bf75226e46d30511cf6)\n[//]: # (START_SECTION ba3af5641e1244e52b148dd008e544d4631c86f5)\n### dSIPRouter Install Fixes Patch\n\n> Commit: [ba3af5641e1244e52b148dd008e544d4631c86f5](https://github.com/dOpensource/dsiprouter/commit/ba3af5641e1244e52b148dd008e544d4631c86f5)  \n> Date: Mon, 23 Nov 2020 13:38:16 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- patch systemd bug from [#295](https://github.com/dOpensource/dsiprouter/pull/295)\n- cleanup kamailio config\n- reset kamailio config defaults\n\n\n---\n\n[//]: # (END_SECTION ba3af5641e1244e52b148dd008e544d4631c86f5)\n[//]: # (START_SECTION 24b32c7b053e73a0a3646854c998a2fc6c653ee4)\n### VULTR Cloud Install Fixes\n\n> Commit: [24b32c7b053e73a0a3646854c998a2fc6c653ee4](https://github.com/dOpensource/dsiprouter/commit/24b32c7b053e73a0a3646854c998a2fc6c653ee4)  \n> Date: Mon, 23 Nov 2020 12:25:42 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add vultr to cloud deployment checks\n- update cloud deployment checks\n- update `checkConn()` lib function\n- update same in `common` tests lib\n- fix external fqdn resolution for non-dns deployments\n- update `resetpassword()` cloud deployment check\n\n\n---\n\n[//]: # (END_SECTION 24b32c7b053e73a0a3646854c998a2fc6c653ee4)\n[//]: # (START_SECTION a39fe323baf2b64f2b79c021b2767e05b2f98f7a)\n### Update Pre-Push Git Hook\n\n> Commit: [a39fe323baf2b64f2b79c021b2767e05b2f98f7a](https://github.com/dOpensource/dsiprouter/commit/a39fe323baf2b64f2b79c021b2767e05b2f98f7a)  \n> Date: Fri, 20 Nov 2020 17:09:25 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- lookup `REMOTE_NAME` when using default remote\n\n\n---\n\n[//]: # (END_SECTION a39fe323baf2b64f2b79c021b2767e05b2f98f7a)\n[//]: # (START_SECTION 383bc1773a09bd9d2356e788fcf7bebadee489d8)\n### dSIPRouter GUI Server Upgrade\n\n> Commit: [383bc1773a09bd9d2356e788fcf7bebadee489d8](https://github.com/dOpensource/dsiprouter/commit/383bc1773a09bd9d2356e788fcf7bebadee489d8)  \n> Date: Fri, 20 Nov 2020 13:20:15 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix certbot not installed on RHEL-based OS\n- fix /var/run/dsiprouter/dsiprouter.pid not created on startup\n- fix hot reloading broken\n- fix http not redirected to https\n- fix install bug when `.git/` is not present\n- switch WSGI app server to bjoern\n- switch dsiprouter to using UNIX domain sockets\n- add various nginx performance improvements\n- add security improvements to flask app\n\n\n---\n\n[//]: # (END_SECTION 383bc1773a09bd9d2356e788fcf7bebadee489d8)\n[//]: # (START_SECTION 0487fdf7ecc14fb4eea84f2ca8dd77d62191df0d)\n### Nginx update: - Enabled Nginx after installation on CentOS 7 and CentOS 8\n\n> Commit: [0487fdf7ecc14fb4eea84f2ca8dd77d62191df0d](https://github.com/dOpensource/dsiprouter/commit/0487fdf7ecc14fb4eea84f2ca8dd77d62191df0d)  \n> Date: Fri, 20 Nov 2020 16:37:41 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0487fdf7ecc14fb4eea84f2ca8dd77d62191df0d)\n[//]: # (START_SECTION e29447636011bce47e75b8943840e87ef4db9ee3)\n### Kamailio Module Load Ordering\n\n> Commit: [e29447636011bce47e75b8943840e87ef4db9ee3](https://github.com/dOpensource/dsiprouter/commit/e29447636011bce47e75b8943840e87ef4db9ee3)  \n> Date: Thu, 19 Nov 2020 11:21:50 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix load order for tls module\n- make sipdump conditionally load\n\n\n---\n\n[//]: # (END_SECTION e29447636011bce47e75b8943840e87ef4db9ee3)\n[//]: # (START_SECTION 442594103964d1441361071ae1f367dce714bda9)\n### Add Missing Dependencies\n\n> Commit: [442594103964d1441361071ae1f367dce714bda9](https://github.com/dOpensource/dsiprouter/commit/442594103964d1441361071ae1f367dce714bda9)  \n> Date: Wed, 18 Nov 2020 17:29:02 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add `sphinx-rtd-theme` and `uwsgi` dependency\n\n\n---\n\n[//]: # (END_SECTION 442594103964d1441361071ae1f367dce714bda9)\n[//]: # (START_SECTION d39844eda0194d17dc47d205c5bc80ac1b4bb296)\n### Fix RPM Search For CentOS Kernel Headers\n\n> Commit: [d39844eda0194d17dc47d205c5bc80ac1b4bb296](https://github.com/dOpensource/dsiprouter/commit/d39844eda0194d17dc47d205c5bc80ac1b4bb296)  \n> Date: Wed, 18 Nov 2020 16:49:11 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add searching centos archives to `rpmSearch()`\n- add dnf support to `setVerbosityLevel()`\n\n\n---\n\n[//]: # (END_SECTION d39844eda0194d17dc47d205c5bc80ac1b4bb296)\n[//]: # (START_SECTION a6c9c5a3fd93382c758002354e5e362516e70be7)\n### Missing uwsgi and sphinx_rtd_theme python packages\n\n> Commit: [a6c9c5a3fd93382c758002354e5e362516e70be7](https://github.com/dOpensource/dsiprouter/commit/a6c9c5a3fd93382c758002354e5e362516e70be7)  \n> Date: Wed, 18 Nov 2020 09:58:56 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a6c9c5a3fd93382c758002354e5e362516e70be7)\n[//]: # (START_SECTION b883b914b2ca6c0e6d9908c4611e9c4251c2eb0a)\n### dsiprouter installation fixes\n\n> Commit: [b883b914b2ca6c0e6d9908c4611e9c4251c2eb0a](https://github.com/dOpensource/dsiprouter/commit/b883b914b2ca6c0e6d9908c4611e9c4251c2eb0a)  \n> Date: Tue, 17 Nov 2020 19:40:41 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix psycopg2 dependency issue on centos\n- fix flask dependency issue on centos\n- fixup printing commands in sub-processes\n- fix kernel header lookups for rtpengine install\n- fixup centos rtpengine install code\n- change rtpengine to use kernel packet forwarding by default\n- improve efficiency by exporting sub-process required funcs\n- update rtpengine version to latest release\n- add nginx changes to centos8\n- update centos to use kamailio 5.3\n- fixed grep file checks outputting text during install\n- add some utility functions to `dsip_lib.sh`\n- revert centos8 to using mariadb instead of mysql\n- fix dependency conflicts between mysql-common and mariadb-common\n- fix centos8 python dependencies\n- various fixes for centos8 service install scripts\n\n\n---\n\n[//]: # (END_SECTION b883b914b2ca6c0e6d9908c4611e9c4251c2eb0a)\n[//]: # (START_SECTION 348d856841f14014714bc55206cdbbfb4c849445)\n### Fixes issue #291 and removes the default Nginx server\n\n> Commit: [348d856841f14014714bc55206cdbbfb4c849445](https://github.com/dOpensource/dsiprouter/commit/348d856841f14014714bc55206cdbbfb4c849445)  \n> Date: Tue, 17 Nov 2020 00:56:39 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 348d856841f14014714bc55206cdbbfb4c849445)\n[//]: # (START_SECTION e17b362a94175aaab6ffc9e711e95fa80df08acc)\n### Fixed Debian 9 dSIPRouter install\n\n> Commit: [e17b362a94175aaab6ffc9e711e95fa80df08acc](https://github.com/dOpensource/dsiprouter/commit/e17b362a94175aaab6ffc9e711e95fa80df08acc)  \n> Date: Sun, 15 Nov 2020 04:17:36 +0000  \n> Author: root (root@testing-vm.5sbaumxjh52uxarymuyxislztd.ex.internal.cloudapp.net)  \n> Committer: root (root@testing-vm.5sbaumxjh52uxarymuyxislztd.ex.internal.cloudapp.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e17b362a94175aaab6ffc9e711e95fa80df08acc)\n[//]: # (START_SECTION b04ed0097dd4ba5867062c16ec2d654e72ac6d6e)\n### Dynamically obtain the username that nginx is running under\n\n> Commit: [b04ed0097dd4ba5867062c16ec2d654e72ac6d6e](https://github.com/dOpensource/dsiprouter/commit/b04ed0097dd4ba5867062c16ec2d654e72ac6d6e)  \n> Date: Sun, 15 Nov 2020 01:17:27 +0000  \n> Author: root (root@testing-vm.4fyalwlohqaujmzfo3kwxhyerh.ex.internal.cloudapp.net)  \n> Committer: root (root@testing-vm.4fyalwlohqaujmzfo3kwxhyerh.ex.internal.cloudapp.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b04ed0097dd4ba5867062c16ec2d654e72ac6d6e)\n[//]: # (START_SECTION 315cbdcde908c5e94fc7f283c2b9d0c76e63f4a6)\n### Update 9.sh\n\n> Commit: [315cbdcde908c5e94fc7f283c2b9d0c76e63f4a6](https://github.com/dOpensource/dsiprouter/commit/315cbdcde908c5e94fc7f283c2b9d0c76e63f4a6)  \n> Date: Sat, 14 Nov 2020 19:38:47 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 315cbdcde908c5e94fc7f283c2b9d0c76e63f4a6)\n[//]: # (START_SECTION 7f50034e56da5e6562eb3e03c813263a5c13fbb7)\n### Update 10.sh\n\n> Commit: [7f50034e56da5e6562eb3e03c813263a5c13fbb7](https://github.com/dOpensource/dsiprouter/commit/7f50034e56da5e6562eb3e03c813263a5c13fbb7)  \n> Date: Sat, 14 Nov 2020 19:37:48 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7f50034e56da5e6562eb3e03c813263a5c13fbb7)\n[//]: # (START_SECTION 52f64d507d7c5d867f3d8fbeb97aa52ad664ec36)\n### Updates\n\n> Commit: [52f64d507d7c5d867f3d8fbeb97aa52ad664ec36](https://github.com/dOpensource/dsiprouter/commit/52f64d507d7c5d867f3d8fbeb97aa52ad664ec36)  \n> Date: Sat, 14 Nov 2020 22:22:21 +0000  \n> Author: root (root@nightly-centos7.dsiprouter.org)  \n> Committer: root (root@nightly-centos7.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 52f64d507d7c5d867f3d8fbeb97aa52ad664ec36)\n[//]: # (START_SECTION 302a6275995e2398f18aa59ad871554456d4b68c)\n### Changed the order in which firewall rules are applied first and then firewalld is started\n\n> Commit: [302a6275995e2398f18aa59ad871554456d4b68c](https://github.com/dOpensource/dsiprouter/commit/302a6275995e2398f18aa59ad871554456d4b68c)  \n> Date: Sat, 14 Nov 2020 21:29:37 +0000  \n> Author: root (root@nightly-centos7.dsiprouter.org)  \n> Committer: root (root@nightly-centos7.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 302a6275995e2398f18aa59ad871554456d4b68c)\n[//]: # (START_SECTION 979e7f28b2c2efbf113f898b689d6603bfd274e0)\n### dSIP Documentation - Re-enabled documentation to be generated\n\n> Commit: [979e7f28b2c2efbf113f898b689d6603bfd274e0](https://github.com/dOpensource/dsiprouter/commit/979e7f28b2c2efbf113f898b689d6603bfd274e0)  \n> Date: Sat, 14 Nov 2020 19:33:28 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 979e7f28b2c2efbf113f898b689d6603bfd274e0)\n[//]: # (START_SECTION 7507cd91dffe48de1b52cd1060610332753dc890)\n### Updates to support Kamailio on CentOS\n\n> Commit: [7507cd91dffe48de1b52cd1060610332753dc890](https://github.com/dOpensource/dsiprouter/commit/7507cd91dffe48de1b52cd1060610332753dc890)  \n> Date: Sat, 14 Nov 2020 19:31:35 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7507cd91dffe48de1b52cd1060610332753dc890)\n[//]: # (START_SECTION bad92e9378ef8164cd37f1c190f92e08ec4c5836)\n### CentOS Updated - Added kamailio-sipdump package to the installer\n\n> Commit: [bad92e9378ef8164cd37f1c190f92e08ec4c5836](https://github.com/dOpensource/dsiprouter/commit/bad92e9378ef8164cd37f1c190f92e08ec4c5836)  \n> Date: Sat, 14 Nov 2020 19:24:39 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bad92e9378ef8164cd37f1c190f92e08ec4c5836)\n[//]: # (START_SECTION e8480a8c7036b9965c4a7424fc438aa35e4d423b)\n### Updates to support Nginx on CentOS 7\n\n> Commit: [e8480a8c7036b9965c4a7424fc438aa35e4d423b](https://github.com/dOpensource/dsiprouter/commit/e8480a8c7036b9965c4a7424fc438aa35e4d423b)  \n> Date: Sat, 14 Nov 2020 19:00:33 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e8480a8c7036b9965c4a7424fc438aa35e4d423b)\n[//]: # (START_SECTION a2e9980ec557656d248823fc4366c7fa0f8edb24)\n### Added missing library for Sphinx\n\n> Commit: [a2e9980ec557656d248823fc4366c7fa0f8edb24](https://github.com/dOpensource/dsiprouter/commit/a2e9980ec557656d248823fc4366c7fa0f8edb24)  \n> Date: Sat, 14 Nov 2020 02:30:38 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a2e9980ec557656d248823fc4366c7fa0f8edb24)\n[//]: # (START_SECTION 7b65855389c4be45c90bfb866eddc9451a8e1d9b)\n### Update requirements to add back in the uwsgi server\n\n> Commit: [7b65855389c4be45c90bfb866eddc9451a8e1d9b](https://github.com/dOpensource/dsiprouter/commit/7b65855389c4be45c90bfb866eddc9451a8e1d9b)  \n> Date: Sat, 14 Nov 2020 01:17:49 +0000  \n> Author: root (root@nightly.dsiprouter.org)  \n> Committer: root (root@nightly.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7b65855389c4be45c90bfb866eddc9451a8e1d9b)\n[//]: # (START_SECTION 16d2a8ea03bd62fbce4078450c714bab9625175d)\n### Update kamailio_dsiprouter.cfg\n\n> Commit: [16d2a8ea03bd62fbce4078450c714bab9625175d](https://github.com/dOpensource/dsiprouter/commit/16d2a8ea03bd62fbce4078450c714bab9625175d)  \n> Date: Fri, 13 Nov 2020 18:08:30 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Adding support for servernat\n\n\n---\n\n[//]: # (END_SECTION 16d2a8ea03bd62fbce4078450c714bab9625175d)\n[//]: # (START_SECTION 3fd6ffb00ebd98f4e93fe0e8db7a8c4121a6c670)\n### Update dsiprouter.sh\n\n> Commit: [3fd6ffb00ebd98f4e93fe0e8db7a8c4121a6c670](https://github.com/dOpensource/dsiprouter/commit/3fd6ffb00ebd98f4e93fe0e8db7a8c4121a6c670)  \n> Date: Fri, 13 Nov 2020 16:32:12 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3fd6ffb00ebd98f4e93fe0e8db7a8c4121a6c670)\n[//]: # (START_SECTION 46667fae98f6c8b5ac627d4e0ab717413c9abb08)\n### Update kamailio_dsiprouter.cfg\n\n> Commit: [46667fae98f6c8b5ac627d4e0ab717413c9abb08](https://github.com/dOpensource/dsiprouter/commit/46667fae98f6c8b5ac627d4e0ab717413c9abb08)  \n> Date: Fri, 13 Nov 2020 16:08:42 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 46667fae98f6c8b5ac627d4e0ab717413c9abb08)\n[//]: # (START_SECTION 31515bd149e609f6f676bb357d461d6e83a01608)\n### Update kamailio_dsiprouter.cfg\n\n> Commit: [31515bd149e609f6f676bb357d461d6e83a01608](https://github.com/dOpensource/dsiprouter/commit/31515bd149e609f6f676bb357d461d6e83a01608)  \n> Date: Fri, 13 Nov 2020 15:51:28 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 31515bd149e609f6f676bb357d461d6e83a01608)\n[//]: # (START_SECTION 373b3b573a04bc6dfa83cab99bd9d881e799f741)\n### Removed Sphinx due to causing build to fail\n\n> Commit: [373b3b573a04bc6dfa83cab99bd9d881e799f741](https://github.com/dOpensource/dsiprouter/commit/373b3b573a04bc6dfa83cab99bd9d881e799f741)  \n> Date: Fri, 13 Nov 2020 20:45:24 +0000  \n> Author: root (root@nightly.dsiprouter.org)  \n> Committer: root (root@nightly.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 373b3b573a04bc6dfa83cab99bd9d881e799f741)\n[//]: # (START_SECTION a4dff281c4621b70e411a6013293396a1b673c70)\n### Add placeholders for Docs Dirs\n\n> Commit: [a4dff281c4621b70e411a6013293396a1b673c70](https://github.com/dOpensource/dsiprouter/commit/a4dff281c4621b70e411a6013293396a1b673c70)  \n> Date: Fri, 13 Nov 2020 13:28:29 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix sphinx missing dir by adding placeholders\n\n\n---\n\n[//]: # (END_SECTION a4dff281c4621b70e411a6013293396a1b673c70)\n[//]: # (START_SECTION aeb39781690a8641f1164d35fe1651f1fbfe355c)\n### Added Nginx support - CentOS 7 - Debian 10\n\n> Commit: [aeb39781690a8641f1164d35fe1651f1fbfe355c](https://github.com/dOpensource/dsiprouter/commit/aeb39781690a8641f1164d35fe1651f1fbfe355c)  \n> Date: Fri, 13 Nov 2020 16:55:13 +0000  \n> Author: root (root@nightly.dsiprouter.org)  \n> Committer: root (root@nightly.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION aeb39781690a8641f1164d35fe1651f1fbfe355c)\n[//]: # (START_SECTION 07639d4e4af2b8232ad4835ca890f2c7e1ad7414)\n### Add Rendered Docs to GUI\n\n> Commit: [07639d4e4af2b8232ad4835ca890f2c7e1ad7414](https://github.com/dOpensource/dsiprouter/commit/07639d4e4af2b8232ad4835ca890f2c7e1ad7414)  \n> Date: Fri, 13 Nov 2020 11:21:22 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- move old docs to user documentation\n- add developer docs\n- add routing docs\n- create local docs on install\n- add sphinx deps to git hooks\n\n\n---\n\n[//]: # (END_SECTION 07639d4e4af2b8232ad4835ca890f2c7e1ad7414)\n[//]: # (START_SECTION 9c97ea3d520064dde2bd147946e9181b3145dcc2)\n### Fixed permissions\n\n> Commit: [9c97ea3d520064dde2bd147946e9181b3145dcc2](https://github.com/dOpensource/dsiprouter/commit/9c97ea3d520064dde2bd147946e9181b3145dcc2)  \n> Date: Fri, 13 Nov 2020 14:50:00 +0000  \n> Author: root (root@nightly.dsiprouter.org)  \n> Committer: root (root@nightly.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9c97ea3d520064dde2bd147946e9181b3145dcc2)\n[//]: # (START_SECTION dd45f9e65664bc3f4bf324e6bfbd5053f4d72e99)\n### Fixed permissions - Changed kamailio_dsiprouter.cfg permissions to allow the dSIPRouter UI to access it\n\n> Commit: [dd45f9e65664bc3f4bf324e6bfbd5053f4d72e99](https://github.com/dOpensource/dsiprouter/commit/dd45f9e65664bc3f4bf324e6bfbd5053f4d72e99)  \n> Date: Fri, 13 Nov 2020 13:48:18 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dd45f9e65664bc3f4bf324e6bfbd5053f4d72e99)\n[//]: # (START_SECTION 5b6ff83b93434abffcc48d57b1b716d5194cb2a2)\n### Changed the owner of settings.py to dsiprouter\n\n> Commit: [5b6ff83b93434abffcc48d57b1b716d5194cb2a2](https://github.com/dOpensource/dsiprouter/commit/5b6ff83b93434abffcc48d57b1b716d5194cb2a2)  \n> Date: Fri, 13 Nov 2020 00:32:25 +0000  \n> Author: root (root@nightly.dsiprouter.org)  \n> Committer: root (root@nightly.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5b6ff83b93434abffcc48d57b1b716d5194cb2a2)\n[//]: # (START_SECTION bf8e7fc878dd464ca797d2eea74dad58332261ca)\n### Updated dSIP version\n\n> Commit: [bf8e7fc878dd464ca797d2eea74dad58332261ca](https://github.com/dOpensource/dsiprouter/commit/bf8e7fc878dd464ca797d2eea74dad58332261ca)  \n> Date: Fri, 13 Nov 2020 00:04:09 +0000  \n> Author: root (root@nightly.dsiprouter.org)  \n> Committer: root (root@nightly.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bf8e7fc878dd464ca797d2eea74dad58332261ca)\n[//]: # (START_SECTION 3f3bb7416475875a443b77aa98f7f4886458c207)\n### Nginx fixes - Changed the permissions of /etc/dsiprouter/privkey to user dsiprouter\n\n> Commit: [3f3bb7416475875a443b77aa98f7f4886458c207](https://github.com/dOpensource/dsiprouter/commit/3f3bb7416475875a443b77aa98f7f4886458c207)  \n> Date: Thu, 12 Nov 2020 19:40:31 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3f3bb7416475875a443b77aa98f7f4886458c207)\n[//]: # (START_SECTION 73ed951374c89b5aacc089e88710118f4fdfc203)\n### Update Nginx configuration\n\n> Commit: [73ed951374c89b5aacc089e88710118f4fdfc203](https://github.com/dOpensource/dsiprouter/commit/73ed951374c89b5aacc089e88710118f4fdfc203)  \n> Date: Thu, 12 Nov 2020 19:25:59 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 73ed951374c89b5aacc089e88710118f4fdfc203)\n[//]: # (START_SECTION e2c2b6d70dcad60da704a37e9800948dbbe469e2)\n### Nginx update: Issues #60\n\n> Commit: [e2c2b6d70dcad60da704a37e9800948dbbe469e2](https://github.com/dOpensource/dsiprouter/commit/e2c2b6d70dcad60da704a37e9800948dbbe469e2)  \n> Date: Thu, 12 Nov 2020 17:59:36 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e2c2b6d70dcad60da704a37e9800948dbbe469e2)\n[//]: # (START_SECTION 9cf083098084c9e450154138455c57cc2843952f)\n### Nginx Support - Initial commit for Issue #60 - Debian 9 support\n\n> Commit: [9cf083098084c9e450154138455c57cc2843952f](https://github.com/dOpensource/dsiprouter/commit/9cf083098084c9e450154138455c57cc2843952f)  \n> Date: Thu, 12 Nov 2020 17:36:23 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9cf083098084c9e450154138455c57cc2843952f)\n[//]: # (START_SECTION 2687815c353e29a9251ff41bdf334757d5376daf)\n### Update install.sh\n\n> Commit: [2687815c353e29a9251ff41bdf334757d5376daf](https://github.com/dOpensource/dsiprouter/commit/2687815c353e29a9251ff41bdf334757d5376daf)  \n> Date: Thu, 12 Nov 2020 10:30:35 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2687815c353e29a9251ff41bdf334757d5376daf)\n[//]: # (START_SECTION d72d5a8a07999ad84d0a1197db00e979b588f054)\n### Update dsiprouter.sh\n\n> Commit: [d72d5a8a07999ad84d0a1197db00e979b588f054](https://github.com/dOpensource/dsiprouter/commit/d72d5a8a07999ad84d0a1197db00e979b588f054)  \n> Date: Wed, 11 Nov 2020 12:40:54 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d72d5a8a07999ad84d0a1197db00e979b588f054)\n[//]: # (START_SECTION 22cab0ab976068d4b49ecd7d4fa02e65bea4849a)\n### Update 9.sh\n\n> Commit: [22cab0ab976068d4b49ecd7d4fa02e65bea4849a](https://github.com/dOpensource/dsiprouter/commit/22cab0ab976068d4b49ecd7d4fa02e65bea4849a)  \n> Date: Wed, 11 Nov 2020 12:38:44 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 22cab0ab976068d4b49ecd7d4fa02e65bea4849a)\n[//]: # (START_SECTION 10c95ab999173de41a257a626d3cd89017b52b1f)\n### Resolves #268\n\n> Commit: [10c95ab999173de41a257a626d3cd89017b52b1f](https://github.com/dOpensource/dsiprouter/commit/10c95ab999173de41a257a626d3cd89017b52b1f)  \n> Date: Wed, 11 Nov 2020 06:38:02 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 10c95ab999173de41a257a626d3cd89017b52b1f)\n[//]: # (START_SECTION a1d693a7da90ba8eaaca8134f36260485f2f051c)\n### Fix ServerNAT Aliasing For External-Internal Address\n\n> Commit: [a1d693a7da90ba8eaaca8134f36260485f2f051c](https://github.com/dOpensource/dsiprouter/commit/a1d693a7da90ba8eaaca8134f36260485f2f051c)  \n> Date: Mon, 9 Nov 2020 17:20:44 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add r2=on record route param to domain routing and outbound routing\n\n\n---\n\n[//]: # (END_SECTION a1d693a7da90ba8eaaca8134f36260485f2f051c)\n[//]: # (START_SECTION 9243cade3cdb3c1cbbe77a3ca0a46ef8cc593d47)\n### Update install.sh\n\n> Commit: [9243cade3cdb3c1cbbe77a3ca0a46ef8cc593d47](https://github.com/dOpensource/dsiprouter/commit/9243cade3cdb3c1cbbe77a3ca0a46ef8cc593d47)  \n> Date: Thu, 5 Nov 2020 10:12:30 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9243cade3cdb3c1cbbe77a3ca0a46ef8cc593d47)\n[//]: # (START_SECTION dfcc241369d1b0f5abbbc163493bfbd3273bbc66)\n### Update install.sh\n\n> Commit: [dfcc241369d1b0f5abbbc163493bfbd3273bbc66](https://github.com/dOpensource/dsiprouter/commit/dfcc241369d1b0f5abbbc163493bfbd3273bbc66)  \n> Date: Thu, 5 Nov 2020 09:32:33 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dfcc241369d1b0f5abbbc163493bfbd3273bbc66)\n[//]: # (START_SECTION 6258abfd7c1817492550798d663e3e5a5c3a54c8)\n### CentOS 8 RTPEngine Fix\n\n> Commit: [6258abfd7c1817492550798d663e3e5a5c3a54c8](https://github.com/dOpensource/dsiprouter/commit/6258abfd7c1817492550798d663e3e5a5c3a54c8)  \n> Date: Wed, 4 Nov 2020 11:29:50 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6258abfd7c1817492550798d663e3e5a5c3a54c8)\n[//]: # (START_SECTION 247eb3fbb0690de397e3283df37eeee8dd53a6ea)\n### Update install.sh\n\n> Commit: [247eb3fbb0690de397e3283df37eeee8dd53a6ea](https://github.com/dOpensource/dsiprouter/commit/247eb3fbb0690de397e3283df37eeee8dd53a6ea)  \n> Date: Wed, 4 Nov 2020 10:35:49 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 247eb3fbb0690de397e3283df37eeee8dd53a6ea)\n[//]: # (START_SECTION 2b6ee47fc7611438ab7f2b0584cce3cf086ca6f4)\n### CentOS 8 RTPEngine\n\n> Commit: [2b6ee47fc7611438ab7f2b0584cce3cf086ca6f4](https://github.com/dOpensource/dsiprouter/commit/2b6ee47fc7611438ab7f2b0584cce3cf086ca6f4)  \n> Date: Tue, 3 Nov 2020 16:19:19 +0000  \n> Author: richard (richstorm781@gmail.com)  \n> Committer: richard (richstorm781@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2b6ee47fc7611438ab7f2b0584cce3cf086ca6f4)\n[//]: # (START_SECTION 20c240e9c512c3d4966f0f7ca4142b80e8de896f)\n### API Updated - Updated the inboundmapping API - Added a Postman collection of the API to make it easier for developers to test the API\n\n> Commit: [20c240e9c512c3d4966f0f7ca4142b80e8de896f](https://github.com/dOpensource/dsiprouter/commit/20c240e9c512c3d4966f0f7ca4142b80e8de896f)  \n> Date: Mon, 2 Nov 2020 19:31:59 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 20c240e9c512c3d4966f0f7ca4142b80e8de896f)\n[//]: # (START_SECTION 9c57c6f953bed6e9348cd47ab06a132357ddf573)\n### Update install.sh\n\n> Commit: [9c57c6f953bed6e9348cd47ab06a132357ddf573](https://github.com/dOpensource/dsiprouter/commit/9c57c6f953bed6e9348cd47ab06a132357ddf573)  \n> Date: Mon, 2 Nov 2020 12:45:04 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9c57c6f953bed6e9348cd47ab06a132357ddf573)\n[//]: # (START_SECTION db071da5fc4d62ffcb9e97ab8b5ed4a040c01de7)\n### Update install.sh\n\n> Commit: [db071da5fc4d62ffcb9e97ab8b5ed4a040c01de7](https://github.com/dOpensource/dsiprouter/commit/db071da5fc4d62ffcb9e97ab8b5ed4a040c01de7)  \n> Date: Mon, 2 Nov 2020 11:50:11 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION db071da5fc4d62ffcb9e97ab8b5ed4a040c01de7)\n[//]: # (START_SECTION c43e91b217077be43e04f48a2d014d096f274d59)\n### Centos 8 RtpEngine fix\n\n> Commit: [c43e91b217077be43e04f48a2d014d096f274d59](https://github.com/dOpensource/dsiprouter/commit/c43e91b217077be43e04f48a2d014d096f274d59)  \n> Date: Mon, 2 Nov 2020 15:46:51 +0000  \n> Author: Richard (richstorm781@gmail.com)  \n> Committer: Richard (richstorm781@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c43e91b217077be43e04f48a2d014d096f274d59)\n[//]: # (START_SECTION 19c08a7c0a935506b65ae130bb5c1edb214cfbbf)\n### Fixed issue with inbound calls coming from Carrier or PBX to MSTeams\n\n> Commit: [19c08a7c0a935506b65ae130bb5c1edb214cfbbf](https://github.com/dOpensource/dsiprouter/commit/19c08a7c0a935506b65ae130bb5c1edb214cfbbf)  \n> Date: Mon, 2 Nov 2020 12:00:13 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 19c08a7c0a935506b65ae130bb5c1edb214cfbbf)\n[//]: # (START_SECTION 4c4110991857ec7ecbf5f1ce0280d3057b4ed9c7)\n### Fixed issue with messages not routing back to MSTeams from a Carrier or PBX\n\n> Commit: [4c4110991857ec7ecbf5f1ce0280d3057b4ed9c7](https://github.com/dOpensource/dsiprouter/commit/4c4110991857ec7ecbf5f1ce0280d3057b4ed9c7)  \n> Date: Mon, 2 Nov 2020 10:50:17 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4c4110991857ec7ecbf5f1ce0280d3057b4ed9c7)\n[//]: # (START_SECTION f595b16e67adcf5278e9bdcec065422cbe5c9b4b)\n### Update install.sh\n\n> Commit: [f595b16e67adcf5278e9bdcec065422cbe5c9b4b](https://github.com/dOpensource/dsiprouter/commit/f595b16e67adcf5278e9bdcec065422cbe5c9b4b)  \n> Date: Sun, 1 Nov 2020 22:39:26 -0500  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f595b16e67adcf5278e9bdcec065422cbe5c9b4b)\n[//]: # (START_SECTION 0f60305db7c4f3476af529ea7b778540e06a9958)\n### Update install.sh\n\n> Commit: [0f60305db7c4f3476af529ea7b778540e06a9958](https://github.com/dOpensource/dsiprouter/commit/0f60305db7c4f3476af529ea7b778540e06a9958)  \n> Date: Wed, 28 Oct 2020 11:56:49 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0f60305db7c4f3476af529ea7b778540e06a9958)\n[//]: # (START_SECTION 51efbde55b88dcd80688a6055608daa5fdb30a27)\n### Debian 10 Fixes\n\n> Commit: [51efbde55b88dcd80688a6055608daa5fdb30a27](https://github.com/dOpensource/dsiprouter/commit/51efbde55b88dcd80688a6055608daa5fdb30a27)  \n> Date: Wed, 28 Oct 2020 10:55:08 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add linux headers to rtpengine deps for deb10\n- change deb10 install to use legacy iptables\n- remove BETA header for deb10 install\n- add deb11 symlink for BETA testers\n- increase size of multidomain mapping columns\n\n\n---\n\n[//]: # (END_SECTION 51efbde55b88dcd80688a6055608daa5fdb30a27)\n[//]: # (START_SECTION e7e33f0ca3224bab3d75b40cdce93d3da36ccac6)\n### Update install.sh\n\n> Commit: [e7e33f0ca3224bab3d75b40cdce93d3da36ccac6](https://github.com/dOpensource/dsiprouter/commit/e7e33f0ca3224bab3d75b40cdce93d3da36ccac6)  \n> Date: Wed, 28 Oct 2020 10:52:38 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e7e33f0ca3224bab3d75b40cdce93d3da36ccac6)\n[//]: # (START_SECTION ed99fde349f38f1f1f5250ff4d9a02faecab9c3b)\n### Update install.sh\n\n> Commit: [ed99fde349f38f1f1f5250ff4d9a02faecab9c3b](https://github.com/dOpensource/dsiprouter/commit/ed99fde349f38f1f1f5250ff4d9a02faecab9c3b)  \n> Date: Tue, 27 Oct 2020 12:55:55 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ed99fde349f38f1f1f5250ff4d9a02faecab9c3b)\n[//]: # (START_SECTION b3f1701f70ea9a610e81a4bc08fa5e1ff3ac9a93)\n### Update install.sh\n\n> Commit: [b3f1701f70ea9a610e81a4bc08fa5e1ff3ac9a93](https://github.com/dOpensource/dsiprouter/commit/b3f1701f70ea9a610e81a4bc08fa5e1ff3ac9a93)  \n> Date: Tue, 27 Oct 2020 11:50:15 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b3f1701f70ea9a610e81a4bc08fa5e1ff3ac9a93)\n[//]: # (START_SECTION 220ec7f1827267091059abd467b88137e717ad94)\n### Update install.sh\n\n> Commit: [220ec7f1827267091059abd467b88137e717ad94](https://github.com/dOpensource/dsiprouter/commit/220ec7f1827267091059abd467b88137e717ad94)  \n> Date: Tue, 27 Oct 2020 10:24:15 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 220ec7f1827267091059abd467b88137e717ad94)\n[//]: # (START_SECTION 81a951367ee99d623c35d9ecad79e57cc24869c0)\n### Added support for dispatcher to Kamailio\n\n> Commit: [81a951367ee99d623c35d9ecad79e57cc24869c0](https://github.com/dOpensource/dsiprouter/commit/81a951367ee99d623c35d9ecad79e57cc24869c0)  \n> Date: Mon, 26 Oct 2020 21:45:13 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 81a951367ee99d623c35d9ecad79e57cc24869c0)\n[//]: # (START_SECTION 52a4b5eb30342d16b5272d769b033ad90cd4d196)\n### Added logic to update/delete inbound routes and endpont groups with load balancing configured\n\n> Commit: [52a4b5eb30342d16b5272d769b033ad90cd4d196](https://github.com/dOpensource/dsiprouter/commit/52a4b5eb30342d16b5272d769b033ad90cd4d196)  \n> Date: Mon, 26 Oct 2020 21:25:16 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 52a4b5eb30342d16b5272d769b033ad90cd4d196)\n[//]: # (START_SECTION 8976863ab2f8aee31a6222a0010e62d9b08a3be2)\n### Patch Pre-commit Hook\n\n> Commit: [8976863ab2f8aee31a6222a0010e62d9b08a3be2](https://github.com/dOpensource/dsiprouter/commit/8976863ab2f8aee31a6222a0010e62d9b08a3be2)  \n> Date: Mon, 26 Oct 2020 15:43:36 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix pre-commit check for failing pipreqs\n\n\n---\n\n[//]: # (END_SECTION 8976863ab2f8aee31a6222a0010e62d9b08a3be2)\n[//]: # (START_SECTION 2ebaca3cb6894916c37e7746e7f9b4c95473cbe0)\n### Update install.sh\n\n> Commit: [2ebaca3cb6894916c37e7746e7f9b4c95473cbe0](https://github.com/dOpensource/dsiprouter/commit/2ebaca3cb6894916c37e7746e7f9b4c95473cbe0)  \n> Date: Mon, 26 Oct 2020 15:37:14 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2ebaca3cb6894916c37e7746e7f9b4c95473cbe0)\n[//]: # (START_SECTION e3673ab836946e34bc759df32499a89bad460878)\n### Cleaning up commits\n\n> Commit: [e3673ab836946e34bc759df32499a89bad460878](https://github.com/dOpensource/dsiprouter/commit/e3673ab836946e34bc759df32499a89bad460878)  \n> Date: Mon, 26 Oct 2020 12:58:06 -0400  \n> Author: Richard Bolaji (richard@goflyball.com)  \n> Committer: Richard Bolaji (richard@goflyball.com)  \n> Signed:   \n\n\n- Changed buster debian install\n- Edited rtp engine install script\n- # Please enter the commit message for your changes. Lines starting\n- # with '#' will be ignored, and an empty message aborts the commit.\n- #\n- # On branch v0.64\n- # Your branch is up to date with 'origin/v0.64'.\n- #\n- # Changes to be committed:\n- #\tdeleted:    .rtpengineinstalled\n- #\tmodified:   CONTRIBUTORS.md\n- #\tmodified:   dsiprouter.sh\n- #\tmodified:   gui/requirements.txt\n- #\tmodified:   rtpengine/debian/install.sh\n- #\n\n\n---\n\n[//]: # (END_SECTION e3673ab836946e34bc759df32499a89bad460878)\n[//]: # (START_SECTION 5387bc1373db5438e5a06c0c81b80ab717398163)\n### Added logic for updating weights in endpoint groups\n\n> Commit: [5387bc1373db5438e5a06c0c81b80ab717398163](https://github.com/dOpensource/dsiprouter/commit/5387bc1373db5438e5a06c0c81b80ab717398163)  \n> Date: Sun, 25 Oct 2020 13:12:41 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5387bc1373db5438e5a06c0c81b80ab717398163)\n[//]: # (START_SECTION aa88b8fc61ed0bac537901196b0a7b7fedc79ad4)\n### Update 8.sh\n\n> Commit: [aa88b8fc61ed0bac537901196b0a7b7fedc79ad4](https://github.com/dOpensource/dsiprouter/commit/aa88b8fc61ed0bac537901196b0a7b7fedc79ad4)  \n> Date: Wed, 21 Oct 2020 09:27:37 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION aa88b8fc61ed0bac537901196b0a7b7fedc79ad4)\n[//]: # (START_SECTION 1693e7fa706fe6c8bb414712ace18f90b86c3585)\n### Inbound DID LB - Added logic to add the hostname to the address table - Added logic to build the dispatcher set; '\n\n> Commit: [1693e7fa706fe6c8bb414712ace18f90b86c3585](https://github.com/dOpensource/dsiprouter/commit/1693e7fa706fe6c8bb414712ace18f90b86c3585)  \n> Date: Tue, 20 Oct 2020 05:56:27 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1693e7fa706fe6c8bb414712ace18f90b86c3585)\n[//]: # (START_SECTION 662ef335d84401bb58c2cefcd88b41533e359a1d)\n### Added support for Inbound Load Balancing from Carrier to Endpoint Groups\n\n> Commit: [662ef335d84401bb58c2cefcd88b41533e359a1d](https://github.com/dOpensource/dsiprouter/commit/662ef335d84401bb58c2cefcd88b41533e359a1d)  \n> Date: Sun, 18 Oct 2020 23:30:51 +0000  \n> Author: root (root@v0621lb0.localdomain)  \n> Committer: root (root@v0621lb0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 662ef335d84401bb58c2cefcd88b41533e359a1d)\n[//]: # (START_SECTION 2c71440a60bee65291c6a88981857eb86279d958)\n### Update install.sh\n\n> Commit: [2c71440a60bee65291c6a88981857eb86279d958](https://github.com/dOpensource/dsiprouter/commit/2c71440a60bee65291c6a88981857eb86279d958)  \n> Date: Fri, 16 Oct 2020 13:17:54 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2c71440a60bee65291c6a88981857eb86279d958)\n[//]: # (START_SECTION 3525c446bb0056b05fe6745085ead8ba106403af)\n### Update install.sh\n\n> Commit: [3525c446bb0056b05fe6745085ead8ba106403af](https://github.com/dOpensource/dsiprouter/commit/3525c446bb0056b05fe6745085ead8ba106403af)  \n> Date: Fri, 16 Oct 2020 11:48:20 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3525c446bb0056b05fe6745085ead8ba106403af)\n[//]: # (START_SECTION 02e128e74c941b83c3f8c1aea14c1387070c1393)\n### Added the ability to handle INTERNAL MSTeams Call Transfers\n\n> Commit: [02e128e74c941b83c3f8c1aea14c1387070c1393](https://github.com/dOpensource/dsiprouter/commit/02e128e74c941b83c3f8c1aea14c1387070c1393)  \n> Date: Thu, 15 Oct 2020 19:54:24 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 02e128e74c941b83c3f8c1aea14c1387070c1393)\n[//]: # (START_SECTION 0b27f18583f624ac932f8d5cbabbd3b5407efd9f)\n### Update install.sh\n\n> Commit: [0b27f18583f624ac932f8d5cbabbd3b5407efd9f](https://github.com/dOpensource/dsiprouter/commit/0b27f18583f624ac932f8d5cbabbd3b5407efd9f)  \n> Date: Wed, 14 Oct 2020 21:57:13 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0b27f18583f624ac932f8d5cbabbd3b5407efd9f)\n[//]: # (START_SECTION 595544e72ea35d3e85034389459821b27716520a)\n### Update settings.py\n\n> Commit: [595544e72ea35d3e85034389459821b27716520a](https://github.com/dOpensource/dsiprouter/commit/595544e72ea35d3e85034389459821b27716520a)  \n> Date: Wed, 14 Oct 2020 11:13:04 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 595544e72ea35d3e85034389459821b27716520a)\n[//]: # (START_SECTION 8697661b71b53e5dbf4e7e3755f3d6daea35248a)\n### Update cert_combined.crt\n\n> Commit: [8697661b71b53e5dbf4e7e3755f3d6daea35248a](https://github.com/dOpensource/dsiprouter/commit/8697661b71b53e5dbf4e7e3755f3d6daea35248a)  \n> Date: Wed, 14 Oct 2020 11:02:08 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8697661b71b53e5dbf4e7e3755f3d6daea35248a)\n[//]: # (START_SECTION 2d69d1b3639238a9082d9dd1befe7b3b97fa8ad5)\n### Update cert.key\n\n> Commit: [2d69d1b3639238a9082d9dd1befe7b3b97fa8ad5](https://github.com/dOpensource/dsiprouter/commit/2d69d1b3639238a9082d9dd1befe7b3b97fa8ad5)  \n> Date: Wed, 14 Oct 2020 11:01:17 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2d69d1b3639238a9082d9dd1befe7b3b97fa8ad5)\n[//]: # (START_SECTION fc41bcee84c9a43fc507116dd63d8b62dae7fdb2)\n### Centos 8 update\n\n> Commit: [fc41bcee84c9a43fc507116dd63d8b62dae7fdb2](https://github.com/dOpensource/dsiprouter/commit/fc41bcee84c9a43fc507116dd63d8b62dae7fdb2)  \n> Date: Wed, 14 Oct 2020 14:53:33 +0000  \n> Author: richard (richstorm781@gmail.com)  \n> Committer: richard (richstorm781@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fc41bcee84c9a43fc507116dd63d8b62dae7fdb2)\n[//]: # (START_SECTION ab07be7a8d96b82155c8447d5e73bb7df8c9e6a3)\n### Update 8.sh\n\n> Commit: [ab07be7a8d96b82155c8447d5e73bb7df8c9e6a3](https://github.com/dOpensource/dsiprouter/commit/ab07be7a8d96b82155c8447d5e73bb7df8c9e6a3)  \n> Date: Mon, 12 Oct 2020 14:53:50 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ab07be7a8d96b82155c8447d5e73bb7df8c9e6a3)\n[//]: # (START_SECTION 333d72ebbc833e2b2424cc63a0d19b23be9cc3c7)\n### Update 8.sh\n\n> Commit: [333d72ebbc833e2b2424cc63a0d19b23be9cc3c7](https://github.com/dOpensource/dsiprouter/commit/333d72ebbc833e2b2424cc63a0d19b23be9cc3c7)  \n> Date: Mon, 12 Oct 2020 13:36:18 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 333d72ebbc833e2b2424cc63a0d19b23be9cc3c7)\n[//]: # (START_SECTION 0d97d8d35d0cb44c8ff2444964967acfbab8aaf3)\n### Update 8.sh\n\n> Commit: [0d97d8d35d0cb44c8ff2444964967acfbab8aaf3](https://github.com/dOpensource/dsiprouter/commit/0d97d8d35d0cb44c8ff2444964967acfbab8aaf3)  \n> Date: Mon, 12 Oct 2020 12:01:16 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0d97d8d35d0cb44c8ff2444964967acfbab8aaf3)\n[//]: # (START_SECTION b6c1ba5ef380ab555007b98c54c5af03309c1042)\n### Update 8.sh\n\n> Commit: [b6c1ba5ef380ab555007b98c54c5af03309c1042](https://github.com/dOpensource/dsiprouter/commit/b6c1ba5ef380ab555007b98c54c5af03309c1042)  \n> Date: Mon, 12 Oct 2020 11:09:27 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b6c1ba5ef380ab555007b98c54c5af03309c1042)\n[//]: # (START_SECTION b89473a34ba4911150e72d636e7660763c6eb62d)\n### Update 8.sh\n\n> Commit: [b89473a34ba4911150e72d636e7660763c6eb62d](https://github.com/dOpensource/dsiprouter/commit/b89473a34ba4911150e72d636e7660763c6eb62d)  \n> Date: Mon, 12 Oct 2020 09:43:38 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b89473a34ba4911150e72d636e7660763c6eb62d)\n[//]: # (START_SECTION d9039e6c980daefce4e5e3e1acd81fc74864283c)\n### Cent0S 8 update\n\n> Commit: [d9039e6c980daefce4e5e3e1acd81fc74864283c](https://github.com/dOpensource/dsiprouter/commit/d9039e6c980daefce4e5e3e1acd81fc74864283c)  \n> Date: Fri, 9 Oct 2020 03:46:08 +0000  \n> Author: richard (richstorm781@gmail.com)  \n> Committer: richard (richstorm781@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d9039e6c980daefce4e5e3e1acd81fc74864283c)\n[//]: # (START_SECTION 963a62c8dd0f0dea807bd2ecd041cab385e54daa)\n### Update 8.sh\n\n> Commit: [963a62c8dd0f0dea807bd2ecd041cab385e54daa](https://github.com/dOpensource/dsiprouter/commit/963a62c8dd0f0dea807bd2ecd041cab385e54daa)  \n> Date: Thu, 8 Oct 2020 11:15:42 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 963a62c8dd0f0dea807bd2ecd041cab385e54daa)\n[//]: # (START_SECTION fd2a050cb03bc8a496075f26ae7a8659739b4647)\n### Update 8.sh\n\n> Commit: [fd2a050cb03bc8a496075f26ae7a8659739b4647](https://github.com/dOpensource/dsiprouter/commit/fd2a050cb03bc8a496075f26ae7a8659739b4647)  \n> Date: Thu, 8 Oct 2020 10:47:29 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fd2a050cb03bc8a496075f26ae7a8659739b4647)\n[//]: # (START_SECTION 69e29d707000c0414c64ee0b96530628364f0cc8)\n### Update 8.sh\n\n> Commit: [69e29d707000c0414c64ee0b96530628364f0cc8](https://github.com/dOpensource/dsiprouter/commit/69e29d707000c0414c64ee0b96530628364f0cc8)  \n> Date: Thu, 8 Oct 2020 09:39:27 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 69e29d707000c0414c64ee0b96530628364f0cc8)\n[//]: # (START_SECTION dc31e424924c5e02936096bb158b6ac210979f68)\n### Update 8.sh\n\n> Commit: [dc31e424924c5e02936096bb158b6ac210979f68](https://github.com/dOpensource/dsiprouter/commit/dc31e424924c5e02936096bb158b6ac210979f68)  \n> Date: Thu, 8 Oct 2020 09:37:13 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dc31e424924c5e02936096bb158b6ac210979f68)\n[//]: # (START_SECTION f73c6e8e4228f1009aa4820521ca638945e9bd20)\n### Update dsiprouter.sh\n\n> Commit: [f73c6e8e4228f1009aa4820521ca638945e9bd20](https://github.com/dOpensource/dsiprouter/commit/f73c6e8e4228f1009aa4820521ca638945e9bd20)  \n> Date: Wed, 7 Oct 2020 09:51:17 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f73c6e8e4228f1009aa4820521ca638945e9bd20)\n[//]: # (START_SECTION 62485dd6238c815d2714729fac136dc37a7770b9)\n### Update settings.py\n\n> Commit: [62485dd6238c815d2714729fac136dc37a7770b9](https://github.com/dOpensource/dsiprouter/commit/62485dd6238c815d2714729fac136dc37a7770b9)  \n> Date: Mon, 5 Oct 2020 16:46:09 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 62485dd6238c815d2714729fac136dc37a7770b9)\n[//]: # (START_SECTION b5d6c624ae39a2fe49d4107dd05af163936b4d35)\n### Update cert_combined.crt\n\n> Commit: [b5d6c624ae39a2fe49d4107dd05af163936b4d35](https://github.com/dOpensource/dsiprouter/commit/b5d6c624ae39a2fe49d4107dd05af163936b4d35)  \n> Date: Mon, 5 Oct 2020 16:45:04 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b5d6c624ae39a2fe49d4107dd05af163936b4d35)\n[//]: # (START_SECTION 61ed44cd0573769b198807e232ebc005561d62a9)\n### Update cert.key\n\n> Commit: [61ed44cd0573769b198807e232ebc005561d62a9](https://github.com/dOpensource/dsiprouter/commit/61ed44cd0573769b198807e232ebc005561d62a9)  \n> Date: Mon, 5 Oct 2020 16:43:11 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 61ed44cd0573769b198807e232ebc005561d62a9)\n[//]: # (START_SECTION 2227edd47210f18a730d6a9abefa8256e09b996c)\n### CentOS 8 support\n\n> Commit: [2227edd47210f18a730d6a9abefa8256e09b996c](https://github.com/dOpensource/dsiprouter/commit/2227edd47210f18a730d6a9abefa8256e09b996c)  \n> Date: Mon, 5 Oct 2020 20:34:23 +0000  \n> Author: Richard (richstorm781@gmail.com)  \n> Committer: Richard (richstorm781@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2227edd47210f18a730d6a9abefa8256e09b996c)\n[//]: # (START_SECTION 79c5b318678348c4e3b3fa7b19f0f784b334f0e1)\n### Update settings.py\n\n> Commit: [79c5b318678348c4e3b3fa7b19f0f784b334f0e1](https://github.com/dOpensource/dsiprouter/commit/79c5b318678348c4e3b3fa7b19f0f784b334f0e1)  \n> Date: Mon, 5 Oct 2020 15:37:15 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 79c5b318678348c4e3b3fa7b19f0f784b334f0e1)\n[//]: # (START_SECTION 3f31a4e61a2aa5e1cca17471bff1073f5652a649)\n### Update cert_combined.crt\n\n> Commit: [3f31a4e61a2aa5e1cca17471bff1073f5652a649](https://github.com/dOpensource/dsiprouter/commit/3f31a4e61a2aa5e1cca17471bff1073f5652a649)  \n> Date: Mon, 5 Oct 2020 15:30:06 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3f31a4e61a2aa5e1cca17471bff1073f5652a649)\n[//]: # (START_SECTION d4e7eaa96c7270a12b833c6d7430db5f1cd69405)\n### Update cert.key\n\n> Commit: [d4e7eaa96c7270a12b833c6d7430db5f1cd69405](https://github.com/dOpensource/dsiprouter/commit/d4e7eaa96c7270a12b833c6d7430db5f1cd69405)  \n> Date: Mon, 5 Oct 2020 11:42:19 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d4e7eaa96c7270a12b833c6d7430db5f1cd69405)\n[//]: # (START_SECTION 0aba1d2e1c1885f67df744297f296f8fa1d41202)\n### Fixed CentOS installation error\n\n> Commit: [0aba1d2e1c1885f67df744297f296f8fa1d41202](https://github.com/dOpensource/dsiprouter/commit/0aba1d2e1c1885f67df744297f296f8fa1d41202)  \n> Date: Mon, 5 Oct 2020 15:37:27 +0000  \n> Author: richard (richstorm781@gmail.com)  \n> Committer: richard (richstorm781@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0aba1d2e1c1885f67df744297f296f8fa1d41202)\n[//]: # (START_SECTION ebea34b830e20160c547ee399b0a89a42528354d)\n### Update settings.py\n\n> Commit: [ebea34b830e20160c547ee399b0a89a42528354d](https://github.com/dOpensource/dsiprouter/commit/ebea34b830e20160c547ee399b0a89a42528354d)  \n> Date: Tue, 29 Sep 2020 10:10:30 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ebea34b830e20160c547ee399b0a89a42528354d)\n[//]: # (START_SECTION 97e1f9bdee0f277a10348208ced3c5ecc52fe030)\n### Update cert_combined.crt\n\n> Commit: [97e1f9bdee0f277a10348208ced3c5ecc52fe030](https://github.com/dOpensource/dsiprouter/commit/97e1f9bdee0f277a10348208ced3c5ecc52fe030)  \n> Date: Tue, 29 Sep 2020 10:01:47 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 97e1f9bdee0f277a10348208ced3c5ecc52fe030)\n[//]: # (START_SECTION 5292d8fdc00d79026d1ce6e3a08245308f8d60d5)\n### Update cert.key\n\n> Commit: [5292d8fdc00d79026d1ce6e3a08245308f8d60d5](https://github.com/dOpensource/dsiprouter/commit/5292d8fdc00d79026d1ce6e3a08245308f8d60d5)  \n> Date: Tue, 29 Sep 2020 10:00:14 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5292d8fdc00d79026d1ce6e3a08245308f8d60d5)\n[//]: # (START_SECTION d26de7235ab9db2370396b262cdb604814aafde9)\n### Fixed #217\n\n> Commit: [d26de7235ab9db2370396b262cdb604814aafde9](https://github.com/dOpensource/dsiprouter/commit/d26de7235ab9db2370396b262cdb604814aafde9)  \n> Date: Mon, 28 Sep 2020 14:31:36 +0000  \n> Author: RichSosa28 (richstorm781@gmail.com)  \n> Committer: RichSosa28 (richstorm781@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d26de7235ab9db2370396b262cdb604814aafde9)\n[//]: # (START_SECTION a2296b5a2bde683ee4d11558e583777fb3e24f23)\n### Update install.sh\n\n> Commit: [a2296b5a2bde683ee4d11558e583777fb3e24f23](https://github.com/dOpensource/dsiprouter/commit/a2296b5a2bde683ee4d11558e583777fb3e24f23)  \n> Date: Fri, 25 Sep 2020 10:33:17 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a2296b5a2bde683ee4d11558e583777fb3e24f23)\n[//]: # (START_SECTION 514a3ed75f3c8497c6dc15550d1f25ccf79c1734)\n### Fixed Compile Issue\n\n> Commit: [514a3ed75f3c8497c6dc15550d1f25ccf79c1734](https://github.com/dOpensource/dsiprouter/commit/514a3ed75f3c8497c6dc15550d1f25ccf79c1734)  \n> Date: Sat, 12 Sep 2020 12:51:58 +0000  \n> Author: root (root@demo.dsiprouter.org)  \n> Committer: root (root@demo.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 514a3ed75f3c8497c6dc15550d1f25ccf79c1734)\n[//]: # (START_SECTION 1a9f29175a7be590bf6e9af55961cfc1450b3f62)\n### Recomplied dSIPRouter Module for Kamailio 5.36\n\n> Commit: [1a9f29175a7be590bf6e9af55961cfc1450b3f62](https://github.com/dOpensource/dsiprouter/commit/1a9f29175a7be590bf6e9af55961cfc1450b3f62)  \n> Date: Sat, 12 Sep 2020 12:26:25 +0000  \n> Author: root (root@demo.dsiprouter.org)  \n> Committer: root (root@demo.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1a9f29175a7be590bf6e9af55961cfc1450b3f62)\n[//]: # (START_SECTION 8f9a792030bebbd18185c86bacafb1e134b34841)\n### Update 10.sh\n\n> Commit: [8f9a792030bebbd18185c86bacafb1e134b34841](https://github.com/dOpensource/dsiprouter/commit/8f9a792030bebbd18185c86bacafb1e134b34841)  \n> Date: Thu, 24 Sep 2020 21:12:54 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8f9a792030bebbd18185c86bacafb1e134b34841)\n[//]: # (START_SECTION 02fc3dc6e85b78e87f8262532ce8dde6c10cdcba)\n### Create 10.sh\n\n> Commit: [02fc3dc6e85b78e87f8262532ce8dde6c10cdcba](https://github.com/dOpensource/dsiprouter/commit/02fc3dc6e85b78e87f8262532ce8dde6c10cdcba)  \n> Date: Thu, 24 Sep 2020 14:17:32 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 02fc3dc6e85b78e87f8262532ce8dde6c10cdcba)\n[//]: # (START_SECTION 035535dd27f4661db5b130576828a4dae9246175)\n### Delete 10.sh\n\n> Commit: [035535dd27f4661db5b130576828a4dae9246175](https://github.com/dOpensource/dsiprouter/commit/035535dd27f4661db5b130576828a4dae9246175)  \n> Date: Thu, 24 Sep 2020 14:17:14 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 035535dd27f4661db5b130576828a4dae9246175)\n[//]: # (START_SECTION 863138e4529b0176a20a2927a33ee1bc9ddde4f0)\n### Create 10.sh\n\n> Commit: [863138e4529b0176a20a2927a33ee1bc9ddde4f0](https://github.com/dOpensource/dsiprouter/commit/863138e4529b0176a20a2927a33ee1bc9ddde4f0)  \n> Date: Thu, 24 Sep 2020 11:59:20 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 863138e4529b0176a20a2927a33ee1bc9ddde4f0)\n[//]: # (START_SECTION 3f30c92abead39a75bb0ffcc3b4adf424ceaa792)\n### Update 10.sh\n\n> Commit: [3f30c92abead39a75bb0ffcc3b4adf424ceaa792](https://github.com/dOpensource/dsiprouter/commit/3f30c92abead39a75bb0ffcc3b4adf424ceaa792)  \n> Date: Thu, 24 Sep 2020 11:55:47 -0400  \n> Author: Richard Bolaji (56362787+RichSosa28@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3f30c92abead39a75bb0ffcc3b4adf424ceaa792)\n[//]: # (START_SECTION 5a18865fbce5cfd9dab6db8e86aa0fd75782af5d)\n### Fixed MSTeams Test Connectivity Button so that the TLS Certificate would be tested properly\n\n> Commit: [5a18865fbce5cfd9dab6db8e86aa0fd75782af5d](https://github.com/dOpensource/dsiprouter/commit/5a18865fbce5cfd9dab6db8e86aa0fd75782af5d)  \n> Date: Sun, 20 Sep 2020 22:07:26 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5a18865fbce5cfd9dab6db8e86aa0fd75782af5d)\n[//]: # (START_SECTION 50d4d13b68096b45d1b76b3ced4f18701da68a56)\n### Added a link for dSIPRouter Core Subscription\n\n> Commit: [50d4d13b68096b45d1b76b3ced4f18701da68a56](https://github.com/dOpensource/dsiprouter/commit/50d4d13b68096b45d1b76b3ced4f18701da68a56)  \n> Date: Sun, 20 Sep 2020 08:57:19 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 50d4d13b68096b45d1b76b3ced4f18701da68a56)\n[//]: # (START_SECTION 8618f4f292ecf3547c230744dbd0b5c73192a2c7)\n### Made Skytel the default carrier again\n\n> Commit: [8618f4f292ecf3547c230744dbd0b5c73192a2c7](https://github.com/dOpensource/dsiprouter/commit/8618f4f292ecf3547c230744dbd0b5c73192a2c7)  \n> Date: Sun, 20 Sep 2020 05:25:04 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8618f4f292ecf3547c230744dbd0b5c73192a2c7)\n[//]: # (START_SECTION a2579b15708e02c1acdf28bc929754b58d9bd41c)\n### Added Twilio as a default Carrier\n\n> Commit: [a2579b15708e02c1acdf28bc929754b58d9bd41c](https://github.com/dOpensource/dsiprouter/commit/a2579b15708e02c1acdf28bc929754b58d9bd41c)  \n> Date: Sun, 20 Sep 2020 04:25:53 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a2579b15708e02c1acdf28bc929754b58d9bd41c)\n[//]: # (START_SECTION 041c87f228eca59819fc9af856d9982f38c4e973)\n### Fixes Issue #259\n\n> Commit: [041c87f228eca59819fc9af856d9982f38c4e973](https://github.com/dOpensource/dsiprouter/commit/041c87f228eca59819fc9af856d9982f38c4e973)  \n> Date: Sat, 19 Sep 2020 11:40:54 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 041c87f228eca59819fc9af856d9982f38c4e973)\n[//]: # (START_SECTION 78d23b58a76940d8f58e6af1550344598110572b)\n### Merge of 0.63-fixes to 0.63\n\n> Commit: [78d23b58a76940d8f58e6af1550344598110572b](https://github.com/dOpensource/dsiprouter/commit/78d23b58a76940d8f58e6af1550344598110572b)  \n> Date: Sat, 19 Sep 2020 11:23:22 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 78d23b58a76940d8f58e6af1550344598110572b)\n[//]: # (START_SECTION f04ecbdf9d38ad1d9b796bb56a3cbf981f7da4fe)\n### Merge or 0.63-fixes to 0.63\n\n> Commit: [f04ecbdf9d38ad1d9b796bb56a3cbf981f7da4fe](https://github.com/dOpensource/dsiprouter/commit/f04ecbdf9d38ad1d9b796bb56a3cbf981f7da4fe)  \n> Date: Sat, 19 Sep 2020 11:23:22 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f04ecbdf9d38ad1d9b796bb56a3cbf981f7da4fe)\n[//]: # (START_SECTION 0c7c05ff628c54f8b8fecb0c9b807f92905ded70)\n### dSIPRouter Installed Fixes - Fixed an issue with conflicts between MySQL and MariaDB Dev libaries in the dSIPRouter UI installer\n\n> Commit: [0c7c05ff628c54f8b8fecb0c9b807f92905ded70](https://github.com/dOpensource/dsiprouter/commit/0c7c05ff628c54f8b8fecb0c9b807f92905ded70)  \n> Date: Wed, 16 Sep 2020 10:57:05 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0c7c05ff628c54f8b8fecb0c9b807f92905ded70)\n[//]: # (START_SECTION cde7d17a518207871133990e4427e2ebe3afb2de)\n### Fixed Compile Issue\n\n> Commit: [cde7d17a518207871133990e4427e2ebe3afb2de](https://github.com/dOpensource/dsiprouter/commit/cde7d17a518207871133990e4427e2ebe3afb2de)  \n> Date: Sat, 12 Sep 2020 12:51:58 +0000  \n> Author: root (root@demo.dsiprouter.org)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cde7d17a518207871133990e4427e2ebe3afb2de)\n[//]: # (START_SECTION c16d04c234423f448de65f8a1825658e9d1544de)\n### Recomplied dSIPRouter Module for Kamailio 5.36\n\n> Commit: [c16d04c234423f448de65f8a1825658e9d1544de](https://github.com/dOpensource/dsiprouter/commit/c16d04c234423f448de65f8a1825658e9d1544de)  \n> Date: Sat, 12 Sep 2020 12:26:25 +0000  \n> Author: root (root@demo.dsiprouter.org)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c16d04c234423f448de65f8a1825658e9d1544de)\n[//]: # (START_SECTION 19090e57612572aa6007c4ea1e1c752e38ac0fa8)\n### Merge in updated MSTeams Logic\n\n> Commit: [19090e57612572aa6007c4ea1e1c752e38ac0fa8](https://github.com/dOpensource/dsiprouter/commit/19090e57612572aa6007c4ea1e1c752e38ac0fa8)  \n> Date: Sun, 13 Sep 2020 12:56:16 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 19090e57612572aa6007c4ea1e1c752e38ac0fa8)\n[//]: # (START_SECTION a8055bd9aef058d810836fe24e73e8fe6d8527e2)\n### Fixed issue #261\n\n> Commit: [a8055bd9aef058d810836fe24e73e8fe6d8527e2](https://github.com/dOpensource/dsiprouter/commit/a8055bd9aef058d810836fe24e73e8fe6d8527e2)  \n> Date: Sat, 12 Sep 2020 21:55:36 +0000  \n> Author: root (root@demo.dsiprouter.org)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a8055bd9aef058d810836fe24e73e8fe6d8527e2)\n[//]: # (START_SECTION d4ed38deb180211c1f6744691a7147a77e63adfd)\n### Fixed issue #261\n\n> Commit: [d4ed38deb180211c1f6744691a7147a77e63adfd](https://github.com/dOpensource/dsiprouter/commit/d4ed38deb180211c1f6744691a7147a77e63adfd)  \n> Date: Sat, 12 Sep 2020 21:55:36 +0000  \n> Author: root (root@demo.dsiprouter.org)  \n> Committer: root (root@demo.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d4ed38deb180211c1f6744691a7147a77e63adfd)\n[//]: # (START_SECTION df155e9611f1b685c31e895a17117f098f8b64d8)\n### Update dsiprouter.sh\n\n> Commit: [df155e9611f1b685c31e895a17117f098f8b64d8](https://github.com/dOpensource/dsiprouter/commit/df155e9611f1b685c31e895a17117f098f8b64d8)  \n> Date: Sat, 12 Sep 2020 16:32:51 -0400  \n> Author: Dean Forester (58430470+DForester-FTC@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Fixed typo - Datebase to Database\n\n\n---\n\n[//]: # (END_SECTION df155e9611f1b685c31e895a17117f098f8b64d8)\n[//]: # (START_SECTION 963356453cdded1169f9c4adbf5effa8a16caff4)\n### Fixed Compile Issue\n\n> Commit: [963356453cdded1169f9c4adbf5effa8a16caff4](https://github.com/dOpensource/dsiprouter/commit/963356453cdded1169f9c4adbf5effa8a16caff4)  \n> Date: Sat, 12 Sep 2020 12:51:58 +0000  \n> Author: root (root@demo.dsiprouter.org)  \n> Committer: root (root@demo.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 963356453cdded1169f9c4adbf5effa8a16caff4)\n[//]: # (START_SECTION 9f5c7c72eaab5cf9ddb0cead7dca609e60423f3d)\n### Recomplied dSIPRouter Module for Kamailio 5.36\n\n> Commit: [9f5c7c72eaab5cf9ddb0cead7dca609e60423f3d](https://github.com/dOpensource/dsiprouter/commit/9f5c7c72eaab5cf9ddb0cead7dca609e60423f3d)  \n> Date: Sat, 12 Sep 2020 12:26:25 +0000  \n> Author: root (root@demo.dsiprouter.org)  \n> Committer: root (root@demo.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9f5c7c72eaab5cf9ddb0cead7dca609e60423f3d)\n[//]: # (START_SECTION 46777afc96ba104bcfffddd7a1406a3c21a80f39)\n### Fix SQL Statement For User/Pass Registration\n\n> Commit: [46777afc96ba104bcfffddd7a1406a3c21a80f39](https://github.com/dOpensource/dsiprouter/commit/46777afc96ba104bcfffddd7a1406a3c21a80f39)  \n> Date: Fri, 11 Sep 2020 13:56:02 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves dOpensource/dsiprouter#258\n\n\n---\n\n[//]: # (END_SECTION 46777afc96ba104bcfffddd7a1406a3c21a80f39)\n[//]: # (START_SECTION 1d29383b9c8f4c77ed93584b183ba8c1a314c155)\n### Fix SQL Statement For User/Pass Registration\n\n> Commit: [1d29383b9c8f4c77ed93584b183ba8c1a314c155](https://github.com/dOpensource/dsiprouter/commit/1d29383b9c8f4c77ed93584b183ba8c1a314c155)  \n> Date: Fri, 11 Sep 2020 13:56:02 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves dOpensource/dsiprouter#258\n\n\n---\n\n[//]: # (END_SECTION 1d29383b9c8f4c77ed93584b183ba8c1a314c155)\n[//]: # (START_SECTION bda4091c8e359bfc3d2ca5f613eac84e37385f05)\n### Update settings.py\n\n> Commit: [bda4091c8e359bfc3d2ca5f613eac84e37385f05](https://github.com/dOpensource/dsiprouter/commit/bda4091c8e359bfc3d2ca5f613eac84e37385f05)  \n> Date: Fri, 11 Sep 2020 07:46:46 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bda4091c8e359bfc3d2ca5f613eac84e37385f05)\n[//]: # (START_SECTION 16824e42f60e3c2d21e0bc3f81a7244b42a75e06)\n### Updated upgrade documentation\n\n> Commit: [16824e42f60e3c2d21e0bc3f81a7244b42a75e06](https://github.com/dOpensource/dsiprouter/commit/16824e42f60e3c2d21e0bc3f81a7244b42a75e06)  \n> Date: Tue, 25 Aug 2020 02:25:52 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 16824e42f60e3c2d21e0bc3f81a7244b42a75e06)\n[//]: # (START_SECTION af8797fd57a4b341a53644df2f04785175a8096e)\n### Added additional logic to the upgrade feature\n\n> Commit: [af8797fd57a4b341a53644df2f04785175a8096e](https://github.com/dOpensource/dsiprouter/commit/af8797fd57a4b341a53644df2f04785175a8096e)  \n> Date: Mon, 24 Aug 2020 22:01:24 +0000  \n> Author: root (root@demo.dsiprouter.org)  \n> Committer: root (root@demo.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION af8797fd57a4b341a53644df2f04785175a8096e)\n[//]: # (START_SECTION 70ab6bea7b72e633ebb43fb979da2ffef6640830)\n### Added the ability to upgrade the Kamailio Configuration from a new release\n\n> Commit: [70ab6bea7b72e633ebb43fb979da2ffef6640830](https://github.com/dOpensource/dsiprouter/commit/70ab6bea7b72e633ebb43fb979da2ffef6640830)  \n> Date: Sun, 23 Aug 2020 22:13:03 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 70ab6bea7b72e633ebb43fb979da2ffef6640830)\n[//]: # (START_SECTION 461d7f7677ad4aaa411cbc66d88b7729a05f8b5b)\n### Typo Correction CVS to CSV\n\n> Commit: [461d7f7677ad4aaa411cbc66d88b7729a05f8b5b](https://github.com/dOpensource/dsiprouter/commit/461d7f7677ad4aaa411cbc66d88b7729a05f8b5b)  \n> Date: Fri, 21 Aug 2020 15:09:26 -0500  \n> Author: demonspork (demonspork@gmail.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Typo Corrections!\r\n- Feel free to just fix this yourself instead of accepting this PR, it might honestly be faster :)\n\n\n---\n\n[//]: # (END_SECTION 461d7f7677ad4aaa411cbc66d88b7729a05f8b5b)\n[//]: # (START_SECTION 52282aff92f1992955beef6fa70417b9b09410c3)\n### Fixed - Implemented logic to configure the dispatcher socket based on if dSIP is in servernat mode Fixes: https://git.flyball.co/dsiprouter/enterprise/issues/80\n\n> Commit: [52282aff92f1992955beef6fa70417b9b09410c3](https://github.com/dOpensource/dsiprouter/commit/52282aff92f1992955beef6fa70417b9b09410c3)  \n> Date: Thu, 20 Aug 2020 11:14:34 +0000  \n> Author: root (root@ip-172-31-29-213.ec2.internal)  \n> Committer: root (root@ip-172-31-29-213.ec2.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 52282aff92f1992955beef6fa70417b9b09410c3)\n[//]: # (START_SECTION 0f337cd8c71673748cfc3272cf5f457053458b47)\n### Fixed issue with Inbound to MSTeams Session Timers\n\n> Commit: [0f337cd8c71673748cfc3272cf5f457053458b47](https://github.com/dOpensource/dsiprouter/commit/0f337cd8c71673748cfc3272cf5f457053458b47)  \n> Date: Wed, 19 Aug 2020 12:51:55 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0f337cd8c71673748cfc3272cf5f457053458b47)\n[//]: # (START_SECTION 0c7714a4f95ac3f8981d44e526e35200e8512425)\n### Updated the documentation to contain a list of the supported configurations\n\n> Commit: [0c7714a4f95ac3f8981d44e526e35200e8512425](https://github.com/dOpensource/dsiprouter/commit/0c7714a4f95ac3f8981d44e526e35200e8512425)  \n> Date: Tue, 18 Aug 2020 21:31:46 +0000  \n> Author: root (root@demo.dsiprouter.org)  \n> Committer: root (root@demo.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0c7714a4f95ac3f8981d44e526e35200e8512425)\n[//]: # (START_SECTION f76634964bd6d89dabac201afc71c392db27d5ae)\n### Fixed issues:  - Session Timers from MSTeams to Carriers  - Add Support for MSTeams Carrier Use Cases, where a Carrier has multiple customers\n\n> Commit: [f76634964bd6d89dabac201afc71c392db27d5ae](https://github.com/dOpensource/dsiprouter/commit/f76634964bd6d89dabac201afc71c392db27d5ae)  \n> Date: Mon, 17 Aug 2020 12:19:44 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f76634964bd6d89dabac201afc71c392db27d5ae)\n[//]: # (START_SECTION f68f44037b0ced445391958b21e1d862112aae80)\n### Update Precommit Hook\n\n> Commit: [f68f44037b0ced445391958b21e1d862112aae80](https://github.com/dOpensource/dsiprouter/commit/f68f44037b0ced445391958b21e1d862112aae80)  \n> Date: Wed, 5 Aug 2020 18:41:12 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix empty `requirements.txt` when no deps found\n\n\n---\n\n[//]: # (END_SECTION f68f44037b0ced445391958b21e1d862112aae80)\n[//]: # (START_SECTION 6c89858eeaafb8a03f709613572ee28a48ff0032)\n### Git CE Improvements\n\n> Commit: [6c89858eeaafb8a03f709613572ee28a48ff0032](https://github.com/dOpensource/dsiprouter/commit/6c89858eeaafb8a03f709613572ee28a48ff0032)  \n> Date: Wed, 5 Aug 2020 17:05:39 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update syntax checker for vcs hooks\n\n\n---\n\n[//]: # (END_SECTION 6c89858eeaafb8a03f709613572ee28a48ff0032)\n[//]: # (START_SECTION 73e05ee0f38d2f206cd60de6c84a8593371d3e0b)\n### Fix Kamailio Versioning\n\n> Commit: [73e05ee0f38d2f206cd60de6c84a8593371d3e0b](https://github.com/dOpensource/dsiprouter/commit/73e05ee0f38d2f206cd60de6c84a8593371d3e0b)  \n> Date: Wed, 5 Aug 2020 16:56:04 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add apt preferences for kamailio repo\n\n\n---\n\n[//]: # (END_SECTION 73e05ee0f38d2f206cd60de6c84a8593371d3e0b)\n[//]: # (START_SECTION 2612c8303932ff5a8fafad624f39ad220e65e72e)\n### Added support for Continuous Integration with Jenkins and Gitlab\n\n> Commit: [2612c8303932ff5a8fafad624f39ad220e65e72e](https://github.com/dOpensource/dsiprouter/commit/2612c8303932ff5a8fafad624f39ad220e65e72e)  \n> Date: Tue, 4 Aug 2020 05:36:22 +0000  \n> Author: root (root@sbc3.dsiprouter.net)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2612c8303932ff5a8fafad624f39ad220e65e72e)\n[//]: # (START_SECTION d0f03270c6429c25f275af3baf0680f4dc69aa10)\n### Went back to RTPEngine mr6.1.1.1\n\n> Commit: [d0f03270c6429c25f275af3baf0680f4dc69aa10](https://github.com/dOpensource/dsiprouter/commit/d0f03270c6429c25f275af3baf0680f4dc69aa10)  \n> Date: Tue, 4 Aug 2020 03:32:20 +0000  \n> Author: root (root@sbc3.dsiprouter.net)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d0f03270c6429c25f275af3baf0680f4dc69aa10)\n[//]: # (START_SECTION 8b643f37be453043787bc55b33f1c648a9274fcc)\n### Update dsiprouter.sh\n\n> Commit: [8b643f37be453043787bc55b33f1c648a9274fcc](https://github.com/dOpensource/dsiprouter/commit/8b643f37be453043787bc55b33f1c648a9274fcc)  \n> Date: Tue, 4 Aug 2020 02:37:54 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8b643f37be453043787bc55b33f1c648a9274fcc)\n[//]: # (START_SECTION 586cc5d9e7aebfef68491efcdd2d2621ae8b6b4f)\n### RTPEngine Upgrade Patches for Debian\n\n> Commit: [586cc5d9e7aebfef68491efcdd2d2621ae8b6b4f](https://github.com/dOpensource/dsiprouter/commit/586cc5d9e7aebfef68491efcdd2d2621ae8b6b4f)  \n> Date: Mon, 3 Aug 2020 17:49:25 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add apt repo configuration to install process\n- update rtpengine install to fix debian dependecy issues\n\n\n---\n\n[//]: # (END_SECTION 586cc5d9e7aebfef68491efcdd2d2621ae8b6b4f)\n[//]: # (START_SECTION f4a320d63763ef5598ad98eb5f9a4da8648736a5)\n### Updated Kamailio config to fix error\n\n> Commit: [f4a320d63763ef5598ad98eb5f9a4da8648736a5](https://github.com/dOpensource/dsiprouter/commit/f4a320d63763ef5598ad98eb5f9a4da8648736a5)  \n> Date: Mon, 3 Aug 2020 13:04:26 +0000  \n> Author: root (root@sbc3.dsiprouter.net)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f4a320d63763ef5598ad98eb5f9a4da8648736a5)\n[//]: # (START_SECTION e2a8fa552f782c6dfe955ffd66bc3576c55d72d8)\n### Configure SSL from Command Line - Added a feature that allows a user to configure the system wide/default cert from the command line - The user can force the certificate to be re-generated by using a -f or --force\n\n> Commit: [e2a8fa552f782c6dfe955ffd66bc3576c55d72d8](https://github.com/dOpensource/dsiprouter/commit/e2a8fa552f782c6dfe955ffd66bc3576c55d72d8)  \n> Date: Sun, 2 Aug 2020 16:45:31 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e2a8fa552f782c6dfe955ffd66bc3576c55d72d8)\n[//]: # (START_SECTION c95f0c44e4c765d890aec8214205e51820224161)\n### Added the Let's Encrypt Root Certificate\n\n> Commit: [c95f0c44e4c765d890aec8214205e51820224161](https://github.com/dOpensource/dsiprouter/commit/c95f0c44e4c765d890aec8214205e51820224161)  \n> Date: Sun, 2 Aug 2020 16:26:05 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c95f0c44e4c765d890aec8214205e51820224161)\n[//]: # (START_SECTION acd96690acb1d5298b429816a5ddf2aa5ad7b8ea)\n### WIP: Current MSTeams Changes\n\n> Commit: [acd96690acb1d5298b429816a5ddf2aa5ad7b8ea](https://github.com/dOpensource/dsiprouter/commit/acd96690acb1d5298b429816a5ddf2aa5ad7b8ea)  \n> Date: Thu, 30 Jul 2020 14:19:36 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add current work in progress for msteams bug fixes\n\n\n---\n\n[//]: # (END_SECTION acd96690acb1d5298b429816a5ddf2aa5ad7b8ea)\n[//]: # (START_SECTION 96f140127d8092590d12f6a0f0474779b780ae44)\n### Fixed the FusionPBX Sync function -  Resolves #https://github.com/dOpensource/dsiprouter/issues/247\n\n> Commit: [96f140127d8092590d12f6a0f0474779b780ae44](https://github.com/dOpensource/dsiprouter/commit/96f140127d8092590d12f6a0f0474779b780ae44)  \n> Date: Sun, 19 Jul 2020 22:51:57 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 96f140127d8092590d12f6a0f0474779b780ae44)\n[//]: # (START_SECTION deef4bcbe3f9b1ffdb112370672b49a29a34f6af)\n### Enhancements to work with FusionPBX Domain Support  - Resolves #https://github.com/dOpensource/dsiprouter/issues/245\n\n> Commit: [deef4bcbe3f9b1ffdb112370672b49a29a34f6af](https://github.com/dOpensource/dsiprouter/commit/deef4bcbe3f9b1ffdb112370672b49a29a34f6af)  \n> Date: Sat, 18 Jul 2020 02:41:16 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION deef4bcbe3f9b1ffdb112370672b49a29a34f6af)\n[//]: # (START_SECTION 35ddd3d327dff18a151fcdb52767fddd27e9736f)\n### Made is easier to execute the commands that are needed to enable FusionPBX pass-thru\n\n> Commit: [35ddd3d327dff18a151fcdb52767fddd27e9736f](https://github.com/dOpensource/dsiprouter/commit/35ddd3d327dff18a151fcdb52767fddd27e9736f)  \n> Date: Sat, 18 Jul 2020 02:25:35 +0000  \n> Author: root (root@sbc4.dsiprouter.net)  \n> Committer: root (root@sbc4.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 35ddd3d327dff18a151fcdb52767fddd27e9736f)\n[//]: # (START_SECTION 0121f1cb99c979c68027b16660ccca25a9acc43b)\n### Fixes to handle SIP Updates - Resolves #https://github.com/dOpensource/dsiprouter/issues/245\n\n> Commit: [0121f1cb99c979c68027b16660ccca25a9acc43b](https://github.com/dOpensource/dsiprouter/commit/0121f1cb99c979c68027b16660ccca25a9acc43b)  \n> Date: Wed, 15 Jul 2020 16:59:36 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0121f1cb99c979c68027b16660ccca25a9acc43b)\n[//]: # (START_SECTION 37aaa51a4282e6843a16f560ee3cae2333564562)\n### Kamailio fix for tls issue Resolves #https://git.flyball.co/dsiprouter/enterprise/issues/71\n\n> Commit: [37aaa51a4282e6843a16f560ee3cae2333564562](https://github.com/dOpensource/dsiprouter/commit/37aaa51a4282e6843a16f560ee3cae2333564562)  \n> Date: Tue, 14 Jul 2020 12:14:02 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 37aaa51a4282e6843a16f560ee3cae2333564562)\n[//]: # (START_SECTION 7ee0942d174d95cb9acf7aca092a883c43fc6cdd)\n### Fixed the tls_config parameters\n\n> Commit: [7ee0942d174d95cb9acf7aca092a883c43fc6cdd](https://github.com/dOpensource/dsiprouter/commit/7ee0942d174d95cb9acf7aca092a883c43fc6cdd)  \n> Date: Thu, 2 Jul 2020 18:58:14 +0000  \n> Author: root (root@sbc2.dsiprouter.net)  \n> Committer: root (root@sbc2.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7ee0942d174d95cb9acf7aca092a883c43fc6cdd)\n[//]: # (START_SECTION c5bb21e63eecd953a2585a0874f447a43d760aec)\n### Fixes for CSRF and Unregister\n\n> Commit: [c5bb21e63eecd953a2585a0874f447a43d760aec](https://github.com/dOpensource/dsiprouter/commit/c5bb21e63eecd953a2585a0874f447a43d760aec)  \n> Date: Thu, 2 Jul 2020 13:09:39 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix external ua -> api csrf error\n- fix autoregister entries not being removed on unregister\n- cleanup kam config\n\n\n---\n\n[//]: # (END_SECTION c5bb21e63eecd953a2585a0874f447a43d760aec)\n[//]: # (START_SECTION 06f03f513ada2fad54be0a113646e44146bb982d)\n### Inbound DID Import - Fixed the endpoint override.  Resolves https://git.flyball.co/dsiprouter/enterprise/issues/72 - Placed the sample CSV template into the proper diretory so that it could be downloaded\n\n> Commit: [06f03f513ada2fad54be0a113646e44146bb982d](https://github.com/dOpensource/dsiprouter/commit/06f03f513ada2fad54be0a113646e44146bb982d)  \n> Date: Thu, 2 Jul 2020 10:06:56 +0000  \n> Author: root (root@demo.dsiprouter.org)  \n> Committer: root (root@demo.dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 06f03f513ada2fad54be0a113646e44146bb982d)\n[//]: # (START_SECTION 5cc24e46160f96401ed15304bb893a10fef3ceae)\n### Added a sample csv file to gui/static/template Resolves https://git.flyball.co/dsiprouter/enterprise/issues/72\n\n> Commit: [5cc24e46160f96401ed15304bb893a10fef3ceae](https://github.com/dOpensource/dsiprouter/commit/5cc24e46160f96401ed15304bb893a10fef3ceae)  \n> Date: Sun, 28 Jun 2020 09:43:23 -0700  \n> Author: Mack Hendricks (mack@dopensource.net)  \n> Committer: Mack Hendricks (mack@dopensource.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5cc24e46160f96401ed15304bb893a10fef3ceae)\n[//]: # (START_SECTION ee98cfaa069f12ca4469caeaea2301c10537df5b)\n### Adding db.commit() Resolves https://github.com/dOpensource/dsiprouter/issues/238\n\n> Commit: [ee98cfaa069f12ca4469caeaea2301c10537df5b](https://github.com/dOpensource/dsiprouter/commit/ee98cfaa069f12ca4469caeaea2301c10537df5b)  \n> Date: Sun, 28 Jun 2020 13:39:01 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ee98cfaa069f12ca4469caeaea2301c10537df5b)\n[//]: # (START_SECTION cb0805ff8bb419be0862283bf395e10eadb32792)\n### Fix for transport=tls parameter missing Resolves https://git.flyball.co/dsiprouter/enterprise/issues/71\n\n> Commit: [cb0805ff8bb419be0862283bf395e10eadb32792](https://github.com/dOpensource/dsiprouter/commit/cb0805ff8bb419be0862283bf395e10eadb32792)  \n> Date: Fri, 26 Jun 2020 11:59:16 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cb0805ff8bb419be0862283bf395e10eadb32792)\n[//]: # (START_SECTION accaa33ba8bde121822be1af560944207b1c1c39)\n### Updated the version number\n\n> Commit: [accaa33ba8bde121822be1af560944207b1c1c39](https://github.com/dOpensource/dsiprouter/commit/accaa33ba8bde121822be1af560944207b1c1c39)  \n> Date: Tue, 23 Jun 2020 10:37:03 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION accaa33ba8bde121822be1af560944207b1c1c39)\n[//]: # (START_SECTION 947ce453dd787b1d398e5935f768abb3034ba42b)\n### Updating dSIPRouter 5.3.5\n\n> Commit: [947ce453dd787b1d398e5935f768abb3034ba42b](https://github.com/dOpensource/dsiprouter/commit/947ce453dd787b1d398e5935f768abb3034ba42b)  \n> Date: Mon, 22 Jun 2020 15:03:43 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 947ce453dd787b1d398e5935f768abb3034ba42b)\n[//]: # (START_SECTION fa64ad6c012d104c56280cfb9799297a7884fca4)\n### Updated dSIPRouter license module for Kamailio 5.35\n\n> Commit: [fa64ad6c012d104c56280cfb9799297a7884fca4](https://github.com/dOpensource/dsiprouter/commit/fa64ad6c012d104c56280cfb9799297a7884fca4)  \n> Date: Mon, 22 Jun 2020 13:45:36 +0000  \n> Author: root (root@sbc2.dsiprouter.net)  \n> Committer: root (root@sbc2.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fa64ad6c012d104c56280cfb9799297a7884fca4)\n[//]: # (START_SECTION 7c7385b77478d340b548fe63613efb6e0dbafdc2)\n### Fixed issue with certificate validation\n\n> Commit: [7c7385b77478d340b548fe63613efb6e0dbafdc2](https://github.com/dOpensource/dsiprouter/commit/7c7385b77478d340b548fe63613efb6e0dbafdc2)  \n> Date: Mon, 22 Jun 2020 12:49:23 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7c7385b77478d340b548fe63613efb6e0dbafdc2)\n[//]: # (START_SECTION 3259ebdc73fe4256876a2d0652bdcd0ae186629c)\n### Updated README\n\n> Commit: [3259ebdc73fe4256876a2d0652bdcd0ae186629c](https://github.com/dOpensource/dsiprouter/commit/3259ebdc73fe4256876a2d0652bdcd0ae186629c)  \n> Date: Mon, 22 Jun 2020 04:48:39 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3259ebdc73fe4256876a2d0652bdcd0ae186629c)\n[//]: # (START_SECTION 5c76eeb103ca9ee1627db466461f7d428c5763d0)\n### Updated README\n\n> Commit: [5c76eeb103ca9ee1627db466461f7d428c5763d0](https://github.com/dOpensource/dsiprouter/commit/5c76eeb103ca9ee1627db466461f7d428c5763d0)  \n> Date: Mon, 22 Jun 2020 04:45:51 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5c76eeb103ca9ee1627db466461f7d428c5763d0)\n[//]: # (START_SECTION 1b1add09d1e189297a0198fd0189b0a505616106)\n### Fixed MSTeam Test Connectivity Feature\n\n> Commit: [1b1add09d1e189297a0198fd0189b0a505616106](https://github.com/dOpensource/dsiprouter/commit/1b1add09d1e189297a0198fd0189b0a505616106)  \n> Date: Mon, 22 Jun 2020 04:19:41 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- - Changed the logic so that the TLS check just validates the connection to the Kamailio Server using the SNI\n\n\n---\n\n[//]: # (END_SECTION 1b1add09d1e189297a0198fd0189b0a505616106)\n[//]: # (START_SECTION 03976c746507769e95416b888af101e01d8a3361)\n### Added the Carrier Outboind and Inbound Enrichments back in\n\n> Commit: [03976c746507769e95416b888af101e01d8a3361](https://github.com/dOpensource/dsiprouter/commit/03976c746507769e95416b888af101e01d8a3361)  \n> Date: Mon, 22 Jun 2020 03:35:48 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 03976c746507769e95416b888af101e01d8a3361)\n[//]: # (START_SECTION c949e68309974eec224f3709667ab2e73d2edf80)\n### v0.621 Bug Fixes\n\n> Commit: [c949e68309974eec224f3709667ab2e73d2edf80](https://github.com/dOpensource/dsiprouter/commit/c949e68309974eec224f3709667ab2e73d2edf80)  \n> Date: Fri, 19 Jun 2020 14:28:14 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix missing parenthesis in kam cfg\n- add failure check to requirements creation in hook\n- rename async as identifier was shadowing builtin\n\n\n---\n\n[//]: # (END_SECTION c949e68309974eec224f3709667ab2e73d2edf80)\n[//]: # (START_SECTION a129fb2eb5e4099d82d15e92ba1f83db88606ee7)\n### Add merge conflict check to pre-commit\n\n> Commit: [a129fb2eb5e4099d82d15e92ba1f83db88606ee7](https://github.com/dOpensource/dsiprouter/commit/a129fb2eb5e4099d82d15e92ba1f83db88606ee7)  \n> Date: Fri, 19 Jun 2020 17:07:41 +0000  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a129fb2eb5e4099d82d15e92ba1f83db88606ee7)\n[//]: # (START_SECTION 07d2fd16487913f7174e0e7d2e626f50121c344c)\n### Update requirements.txt\n\n> Commit: [07d2fd16487913f7174e0e7d2e626f50121c344c](https://github.com/dOpensource/dsiprouter/commit/07d2fd16487913f7174e0e7d2e626f50121c344c)  \n> Date: Fri, 19 Jun 2020 17:06:24 +0000  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 07d2fd16487913f7174e0e7d2e626f50121c344c)\n[//]: # (START_SECTION f7f14a6b4c8f1bdba9975f6da08e88385185c229)\n### v0.621 Bug Fixes\n\n> Commit: [f7f14a6b4c8f1bdba9975f6da08e88385185c229](https://github.com/dOpensource/dsiprouter/commit/f7f14a6b4c8f1bdba9975f6da08e88385185c229)  \n> Date: Thu, 18 Jun 2020 23:25:38 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/61\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/63\n- Workaround for TLS reload bug [kam upstream issue]\n- Move prefix char definition to non-db backed settings\n- Improve reformat URI kam route\n- Add route priority handling to kam gwgroup lookup\n- Fix missing endif in kam nat route\n- Fix issues with precommit hook\n\n\n---\n\n[//]: # (END_SECTION f7f14a6b4c8f1bdba9975f6da08e88385185c229)\n[//]: # (START_SECTION 885cb512a28f28cf7a9bfbaa294398bfab2044ed)\n### Document FusionPBX UI Proxy - Resolves #62\n\n> Commit: [885cb512a28f28cf7a9bfbaa294398bfab2044ed](https://github.com/dOpensource/dsiprouter/commit/885cb512a28f28cf7a9bfbaa294398bfab2044ed)  \n> Date: Fri, 19 Jun 2020 16:27:31 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 885cb512a28f28cf7a9bfbaa294398bfab2044ed)\n[//]: # (START_SECTION c67c821d22016544fd0cb52111314ce64e9020a0)\n### Certificates\n\n> Commit: [c67c821d22016544fd0cb52111314ce64e9020a0](https://github.com/dOpensource/dsiprouter/commit/c67c821d22016544fd0cb52111314ce64e9020a0)  \n> Date: Fri, 19 Jun 2020 14:50:12 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- - Added support for updating the default Certificate #Resolves #65\n- - Changed the renewsslcert command so that it doesn't renew the default cert if it was uploaded\n- - Added a cacert.pem that just contains the MSTeams Root Certs\n\n\n---\n\n[//]: # (END_SECTION c67c821d22016544fd0cb52111314ce64e9020a0)\n[//]: # (START_SECTION d720a5ed18a74aa50c1e53eb486b8b62ac36c713)\n### 0.62 Fixes - Pull tls.reload out of the Reload API for now - Fixed issues with uploading a certificate.  Users must restart Kamailio for a cert to become active - Change the message bar slideup time from 3 secs to 10 seconds - Fixed an issue with the CDR page giving a Failed alert when clicking on the CDR page\n\n> Commit: [d720a5ed18a74aa50c1e53eb486b8b62ac36c713](https://github.com/dOpensource/dsiprouter/commit/d720a5ed18a74aa50c1e53eb486b8b62ac36c713)  \n> Date: Fri, 12 Jun 2020 06:28:11 +0000  \n> Author: root (root@sbc3.dsiprouter.net)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d720a5ed18a74aa50c1e53eb486b8b62ac36c713)\n[//]: # (START_SECTION 9069634da8b9b8f0aa037702d4ec0394458ded10)\n### Fixes - Fixed Javascript errors in Domains and MSTeams Validation Pages - Removed logic to convert OS certs to PEM.  They are already in PEM format\n\n> Commit: [9069634da8b9b8f0aa037702d4ec0394458ded10](https://github.com/dOpensource/dsiprouter/commit/9069634da8b9b8f0aa037702d4ec0394458ded10)  \n> Date: Thu, 11 Jun 2020 23:16:36 +0000  \n> Author: root (root@sbc3.dsiprouter.net)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9069634da8b9b8f0aa037702d4ec0394458ded10)\n[//]: # (START_SECTION 86645a3177b6281d66e1b4e35e1811b52338c11c)\n### Fixed an issue that prevented the Domain authtype to disable the other fields\n\n> Commit: [86645a3177b6281d66e1b4e35e1811b52338c11c](https://github.com/dOpensource/dsiprouter/commit/86645a3177b6281d66e1b4e35e1811b52338c11c)  \n> Date: Thu, 11 Jun 2020 18:42:01 +0000  \n> Author: root (root@sbc3.dsiprouter.net)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 86645a3177b6281d66e1b4e35e1811b52338c11c)\n[//]: # (START_SECTION 678dd84fab164bc2c7b31437ba1a98a982d50aee)\n### Added support for manually creating wildcard certs\n\n> Commit: [678dd84fab164bc2c7b31437ba1a98a982d50aee](https://github.com/dOpensource/dsiprouter/commit/678dd84fab164bc2c7b31437ba1a98a982d50aee)  \n> Date: Thu, 11 Jun 2020 02:57:06 +0000  \n> Author: root (root@mack.dsiprouter.net)  \n> Committer: root (root@mack.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 678dd84fab164bc2c7b31437ba1a98a982d50aee)\n[//]: # (START_SECTION e05fdf5204a0b9955e4d8f1ed38fab19a2c8b1d9)\n### Certificates - Fixed issues with Adding, Updating and Delete Certifcates\n\n> Commit: [e05fdf5204a0b9955e4d8f1ed38fab19a2c8b1d9](https://github.com/dOpensource/dsiprouter/commit/e05fdf5204a0b9955e4d8f1ed38fab19a2c8b1d9)  \n> Date: Thu, 11 Jun 2020 02:07:07 +0000  \n> Author: root (root@mack.dsiprouter.net)  \n> Committer: root (root@mack.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e05fdf5204a0b9955e4d8f1ed38fab19a2c8b1d9)\n[//]: # (START_SECTION 5622efa5487a31dce38ca45e71ff7208bba67778)\n### Continued V0.62 Bug Fixes\n\n> Commit: [5622efa5487a31dce38ca45e71ff7208bba67778](https://github.com/dOpensource/dsiprouter/commit/5622efa5487a31dce38ca45e71ff7208bba67778)  \n> Date: Wed, 10 Jun 2020 17:29:47 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/59\n- fixed restart CLI cmd missing start function\n- fixed kamcmd empty string processing when updating kamailio\n- fixed `pre-snapshot.sh` missing function definition\n- fixed CDR endpoint URL's malformed\n\n\n---\n\n[//]: # (END_SECTION 5622efa5487a31dce38ca45e71ff7208bba67778)\n[//]: # (START_SECTION c250876b72f02c2288d02ae211d5c0b7920bf981)\n### Update PreCommit Hook with Requirements Changes\n\n> Commit: [c250876b72f02c2288d02ae211d5c0b7920bf981](https://github.com/dOpensource/dsiprouter/commit/c250876b72f02c2288d02ae211d5c0b7920bf981)  \n> Date: Wed, 10 Jun 2020 10:21:42 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- add certbot to forced includes\n- add acme and josepy to forced excludes\n\n\n---\n\n[//]: # (END_SECTION c250876b72f02c2288d02ae211d5c0b7920bf981)\n[//]: # (START_SECTION dd215cc504ee085a031aac6577c81ad8ae753a7f)\n### Removed the acme and josepy from requirements.txt and just added in certbot\n\n> Commit: [dd215cc504ee085a031aac6577c81ad8ae753a7f](https://github.com/dOpensource/dsiprouter/commit/dd215cc504ee085a031aac6577c81ad8ae753a7f)  \n> Date: Wed, 10 Jun 2020 11:38:45 +0000  \n> Author: root (root@mack.dsiprouter.net)  \n> Committer: root (root@mack.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dd215cc504ee085a031aac6577c81ad8ae753a7f)\n[//]: # (START_SECTION e391260463b04eeaed6769f316c95ff9088b3a43)\n### Fixes for v0.62 Release\n\n> Commit: [e391260463b04eeaed6769f316c95ff9088b3a43](https://github.com/dOpensource/dsiprouter/commit/e391260463b04eeaed6769f316c95ff9088b3a43)  \n> Date: Wed, 10 Jun 2020 06:34:06 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/53\n- fix carrier group and endpoint group listener issues\n- fix improper record route handling\n- add example improvements for fusionpbx sync\n- fix carrier group delete exceptions on UAC DNE\n\n\n---\n\n[//]: # (END_SECTION e391260463b04eeaed6769f316c95ff9088b3a43)\n[//]: # (START_SECTION a37c39557a4ba52f53f4e83454a363bd778f1a3f)\n### Certificates - Completed Add, Update and Delete of Certificates\n\n> Commit: [a37c39557a4ba52f53f4e83454a363bd778f1a3f](https://github.com/dOpensource/dsiprouter/commit/a37c39557a4ba52f53f4e83454a363bd778f1a3f)  \n> Date: Tue, 9 Jun 2020 08:49:40 +0000  \n> Author: root (root@mack.dsiprouter.net)  \n> Committer: root (root@mack.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a37c39557a4ba52f53f4e83454a363bd778f1a3f)\n[//]: # (START_SECTION 34de2c54dcb4b73865783cc24ca0b96969b1e87e)\n### Add uuidgen Dependency to Install\n\n> Commit: [34de2c54dcb4b73865783cc24ca0b96969b1e87e](https://github.com/dOpensource/dsiprouter/commit/34de2c54dcb4b73865783cc24ca0b96969b1e87e)  \n> Date: Mon, 8 Jun 2020 16:48:33 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fix missing uuidgen binary on install\n\n\n---\n\n[//]: # (END_SECTION 34de2c54dcb4b73865783cc24ca0b96969b1e87e)\n[//]: # (START_SECTION 25b99e1b211e78ecb9ed20cb803594b1959b3000)\n### Fix Misordered Logic in Endpoint Groups URI Parsing\n\n> Commit: [25b99e1b211e78ecb9ed20cb803594b1959b3000](https://github.com/dOpensource/dsiprouter/commit/25b99e1b211e78ecb9ed20cb803594b1959b3000)  \n> Date: Mon, 8 Jun 2020 16:30:01 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fix URI parsing for endpoint groups\n\n\n---\n\n[//]: # (END_SECTION 25b99e1b211e78ecb9ed20cb803594b1959b3000)\n[//]: # (START_SECTION 5c7816d4ae274af72fadf354bffe70319e4bacdc)\n### Fix API Regressions\n\n> Commit: [5c7816d4ae274af72fadf354bffe70319e4bacdc](https://github.com/dOpensource/dsiprouter/commit/5c7816d4ae274af72fadf354bffe70319e4bacdc)  \n> Date: Mon, 8 Jun 2020 16:20:29 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fix regressions from bad merge\n\n\n---\n\n[//]: # (END_SECTION 5c7816d4ae274af72fadf354bffe70319e4bacdc)\n[//]: # (START_SECTION f43cae7dfbb13d56ea9a8c3d1d816b2e4fe1cfc9)\n### Fixed issue with certificates\n\n> Commit: [f43cae7dfbb13d56ea9a8c3d1d816b2e4fe1cfc9](https://github.com/dOpensource/dsiprouter/commit/f43cae7dfbb13d56ea9a8c3d1d816b2e4fe1cfc9)  \n> Date: Mon, 8 Jun 2020 19:24:04 +0000  \n> Author: root (root@mack.dsiprouter.net)  \n> Committer: root (root@mack.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f43cae7dfbb13d56ea9a8c3d1d816b2e4fe1cfc9)\n[//]: # (START_SECTION 330d69481ece2323d04de2ac8f5b496e503f24cc)\n### Fix Kamailio TLS Config Handling\n\n> Commit: [330d69481ece2323d04de2ac8f5b496e503f24cc](https://github.com/dOpensource/dsiprouter/commit/330d69481ece2323d04de2ac8f5b496e503f24cc)  \n> Date: Mon, 8 Jun 2020 15:09:32 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- improve regex and config management in `kamtls.py`\n- fix small bug in CLI restart command defaults\n\n\n---\n\n[//]: # (END_SECTION 330d69481ece2323d04de2ac8f5b496e503f24cc)\n[//]: # (START_SECTION b6e233d04a02cfbee6e25e62d8f033510b08e527)\n### Removed acme for testing\n\n> Commit: [b6e233d04a02cfbee6e25e62d8f033510b08e527](https://github.com/dOpensource/dsiprouter/commit/b6e233d04a02cfbee6e25e62d8f033510b08e527)  \n> Date: Mon, 8 Jun 2020 18:38:34 +0000  \n> Author: root (root@mack.dsiprouter.net)  \n> Committer: root (root@mack.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b6e233d04a02cfbee6e25e62d8f033510b08e527)\n[//]: # (START_SECTION cd5df131c230e5f47b03dd5a93bcfc9d0b17b019)\n### Cron Services - Changed the permissions of the file to 744 so that it's executable\n\n> Commit: [cd5df131c230e5f47b03dd5a93bcfc9d0b17b019](https://github.com/dOpensource/dsiprouter/commit/cd5df131c230e5f47b03dd5a93bcfc9d0b17b019)  \n> Date: Mon, 8 Jun 2020 18:22:50 +0000  \n> Author: root (root@mack.dsiprouter.net)  \n> Committer: root (root@mack.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cd5df131c230e5f47b03dd5a93bcfc9d0b17b019)\n[//]: # (START_SECTION cb2428d137a134aff9dbb3bc8a61b9bde38d8f36)\n### Removed josepy\n\n> Commit: [cb2428d137a134aff9dbb3bc8a61b9bde38d8f36](https://github.com/dOpensource/dsiprouter/commit/cb2428d137a134aff9dbb3bc8a61b9bde38d8f36)  \n> Date: Mon, 8 Jun 2020 18:08:34 +0000  \n> Author: root (root@mack.dsiprouter.net)  \n> Committer: root (root@mack.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cb2428d137a134aff9dbb3bc8a61b9bde38d8f36)\n[//]: # (START_SECTION 3b91672c6c9c1357190caf3369545b5150fad649)\n### Certificates - Changed out version of josepy module - Changed the error handling of adding Kam certificates to the TLS config\n\n> Commit: [3b91672c6c9c1357190caf3369545b5150fad649](https://github.com/dOpensource/dsiprouter/commit/3b91672c6c9c1357190caf3369545b5150fad649)  \n> Date: Mon, 8 Jun 2020 17:22:23 +0000  \n> Author: root (root@mack.dsiprouter.net)  \n> Committer: root (root@mack.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3b91672c6c9c1357190caf3369545b5150fad649)\n[//]: # (START_SECTION 3f5074c9b8147552ec8e1592d74df60c9e46a50e)\n### Fix Command Parsing for Commands with Defaults\n\n> Commit: [3f5074c9b8147552ec8e1592d74df60c9e46a50e](https://github.com/dOpensource/dsiprouter/commit/3f5074c9b8147552ec8e1592d74df60c9e46a50e)  \n> Date: Mon, 8 Jun 2020 09:44:26 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- parse debug option before checking for num of args\n\n\n---\n\n[//]: # (END_SECTION 3f5074c9b8147552ec8e1592d74df60c9e46a50e)\n[//]: # (START_SECTION 2aed9209cbce02686c61aca1c580907f2b174df4)\n### Changed the order of PyOpenSSL\n\n> Commit: [2aed9209cbce02686c61aca1c580907f2b174df4](https://github.com/dOpensource/dsiprouter/commit/2aed9209cbce02686c61aca1c580907f2b174df4)  \n> Date: Mon, 8 Jun 2020 13:26:22 +0000  \n> Author: root (root@mack.dsiprouter.net)  \n> Committer: root (root@mack.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2aed9209cbce02686c61aca1c580907f2b174df4)\n[//]: # (START_SECTION b5de18522aa45387382973031ce9c4861cbc8422)\n### Fix Carrier Group Issue\n\n> Commit: [b5de18522aa45387382973031ce9c4861cbc8422](https://github.com/dOpensource/dsiprouter/commit/b5de18522aa45387382973031ce9c4861cbc8422)  \n> Date: Mon, 8 Jun 2020 08:29:09 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fix issue causing server error on parsing auth type\n\n\n---\n\n[//]: # (END_SECTION b5de18522aa45387382973031ce9c4861cbc8422)\n[//]: # (START_SECTION 06bbc86f9ac496d95efd98bca3d26a7089d56c94)\n### Change Deafults for Start/Stop/Restart commands\n\n> Commit: [06bbc86f9ac496d95efd98bca3d26a7089d56c94](https://github.com/dOpensource/dsiprouter/commit/06bbc86f9ac496d95efd98bca3d26a7089d56c94)  \n> Date: Mon, 8 Jun 2020 07:56:28 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- make start/stop/restart CLI commands default to only dsiprouter\n- add command line completion for new command options\n\n\n---\n\n[//]: # (END_SECTION 06bbc86f9ac496d95efd98bca3d26a7089d56c94)\n[//]: # (START_SECTION c49afc5cca89af0112ce7bcc046a993e37363552)\n### Certificates - Can now upload certificates - TODO: Fix table update after upload, finish update and fix kamtls output of certs\n\n> Commit: [c49afc5cca89af0112ce7bcc046a993e37363552](https://github.com/dOpensource/dsiprouter/commit/c49afc5cca89af0112ce7bcc046a993e37363552)  \n> Date: Mon, 8 Jun 2020 03:48:57 +0000  \n> Author: root (root@mack.dsiprouter.net)  \n> Committer: root (root@mack.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c49afc5cca89af0112ce7bcc046a993e37363552)\n[//]: # (START_SECTION e73013afd487afb3ea4b564fc29f101d9412d629)\n### Certificate Management - Completed the ability to Add a Certficate - Completed the ability to Delete a Certificate - TODO: Add Upload Cert Capability\n\n> Commit: [e73013afd487afb3ea4b564fc29f101d9412d629](https://github.com/dOpensource/dsiprouter/commit/e73013afd487afb3ea4b564fc29f101d9412d629)  \n> Date: Sun, 7 Jun 2020 00:25:55 +0000  \n> Author: root (root@mack.dsiprouter.net)  \n> Committer: root (root@mack.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e73013afd487afb3ea4b564fc29f101d9412d629)\n[//]: # (START_SECTION 601657baf95fe3ce7884389bb00695bf7bb4330b)\n### v0.62 Fixes\n\n> Commit: [601657baf95fe3ce7884389bb00695bf7bb4330b](https://github.com/dOpensource/dsiprouter/commit/601657baf95fe3ce7884389bb00695bf7bb4330b)  \n> Date: Sat, 6 Jun 2020 03:04:17 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fix endpoint group address table mapping\n- update default gateways to match address table mapping\n- add support for ipv6 in a majority of functions\n- add support for uri parsing\n- fix uacreg entry handling in GUI routes\n- fix endpoint group error on add\n- fix error handling when modal visible\n- update python docstring documentation\n\n\n---\n\n[//]: # (END_SECTION 601657baf95fe3ce7884389bb00695bf7bb4330b)\n[//]: # (START_SECTION 3657bfd59850a0b74e2a94c53c72348513abcf50)\n### Certificates - Added support for adding certificates\n\n> Commit: [3657bfd59850a0b74e2a94c53c72348513abcf50](https://github.com/dOpensource/dsiprouter/commit/3657bfd59850a0b74e2a94c53c72348513abcf50)  \n> Date: Fri, 5 Jun 2020 23:24:55 +0000  \n> Author: root (root@mack.dsiprouter.net)  \n> Committer: root (root@mack.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3657bfd59850a0b74e2a94c53c72348513abcf50)\n[//]: # (START_SECTION 31359590de49b8f5098a8568432929555645689e)\n### Initial commit of LetsEncrypt functions\n\n> Commit: [31359590de49b8f5098a8568432929555645689e](https://github.com/dOpensource/dsiprouter/commit/31359590de49b8f5098a8568432929555645689e)  \n> Date: Fri, 5 Jun 2020 20:52:58 +0000  \n> Author: root (root@mack.dsiprouter.net)  \n> Committer: root (root@mack.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 31359590de49b8f5098a8568432929555645689e)\n[//]: # (START_SECTION 9fab8dbf3eee065837301bec528982ffc32c6089)\n### Fix Endpoint Group Table Update\n\n> Commit: [9fab8dbf3eee065837301bec528982ffc32c6089](https://github.com/dOpensource/dsiprouter/commit/9fab8dbf3eee065837301bec528982ffc32c6089)  \n> Date: Wed, 3 Jun 2020 17:20:46 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fix typo in `endpointgroups.js` causing updating in GUI to fail\n\n\n---\n\n[//]: # (END_SECTION 9fab8dbf3eee065837301bec528982ffc32c6089)\n[//]: # (START_SECTION b3873d2aad6a8a8d58f65a369499771db8162013)\n### Improved Error Handling\n\n> Commit: [b3873d2aad6a8a8d58f65a369499771db8162013](https://github.com/dOpensource/dsiprouter/commit/b3873d2aad6a8a8d58f65a369499771db8162013)  \n> Date: Wed, 3 Jun 2020 16:49:10 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- allow error messages to propogate to error handler\n- handle error in same page on kamailio reload error\n- set kamailio uac reg reload limit to 30 sec\n\n\n---\n\n[//]: # (END_SECTION b3873d2aad6a8a8d58f65a369499771db8162013)\n[//]: # (START_SECTION e4defd9cd3b98b19fd9b2a50c95559a37d26765f)\n###   Resolves https://git.flyball.co/dsiprouter/enterprise/issues/55   - implements some standardization as mentioned in https://git.flyball.co/dsiprouter/enterprise/issues/49   - fixes endpoint group update/add   - fixes endpoint add/update/delete   - fixes call limit default   - fixes kam reload for API   - extends `jquery.tableEdit` functionality   - added comments in API for future updates   - abstract request data handling into `shared.py`   - use `util.security.py` funcs for password generation   - fix API login timeout redirection url   - fix dashboard stats update to match API kam reload   - fix CA certificate format issue for debian OS   - reset settings.py defaults\n\n> Commit: [e4defd9cd3b98b19fd9b2a50c95559a37d26765f](https://github.com/dOpensource/dsiprouter/commit/e4defd9cd3b98b19fd9b2a50c95559a37d26765f)  \n> Date: Wed, 3 Jun 2020 14:40:44 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e4defd9cd3b98b19fd9b2a50c95559a37d26765f)\n[//]: # (START_SECTION 65b76a75e8c8a28560f096d3ae61df52aae053ca)\n### mend\n\n> Commit: [65b76a75e8c8a28560f096d3ae61df52aae053ca](https://github.com/dOpensource/dsiprouter/commit/65b76a75e8c8a28560f096d3ae61df52aae053ca)  \n> Date: Tue, 2 Jun 2020 21:41:36 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 65b76a75e8c8a28560f096d3ae61df52aae053ca)\n[//]: # (START_SECTION 87b771ec8ea8ec5fcbf08c24fcbf2c34ae76db9f)\n### Certificates Module - Added SQL to store the certs in the DB - Added the standard install script\n\n> Commit: [87b771ec8ea8ec5fcbf08c24fcbf2c34ae76db9f](https://github.com/dOpensource/dsiprouter/commit/87b771ec8ea8ec5fcbf08c24fcbf2c34ae76db9f)  \n> Date: Tue, 2 Jun 2020 21:40:30 +0000  \n> Author: root (root@sbc.dsiprouter.net)  \n> Committer: root (root@sbc.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 87b771ec8ea8ec5fcbf08c24fcbf2c34ae76db9f)\n[//]: # (START_SECTION 698ab1c7481fad6fedeaa698dcc4d7b00deea0bc)\n### Certificates - Added UI components to allow a user to select Generate or Upload a certificate\n\n> Commit: [698ab1c7481fad6fedeaa698dcc4d7b00deea0bc](https://github.com/dOpensource/dsiprouter/commit/698ab1c7481fad6fedeaa698dcc4d7b00deea0bc)  \n> Date: Mon, 1 Jun 2020 13:22:09 +0000  \n> Author: root (root@sbc.dsiprouter.net)  \n> Committer: root (root@sbc.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 698ab1c7481fad6fedeaa698dcc4d7b00deea0bc)\n[//]: # (START_SECTION d57d36e0580f4dc5dc69536825fd7f26dfe9b77d)\n### Fixup GUI and API Issues\n\n> Commit: [d57d36e0580f4dc5dc69536825fd7f26dfe9b77d](https://github.com/dOpensource/dsiprouter/commit/d57d36e0580f4dc5dc69536825fd7f26dfe9b77d)  \n> Date: Sun, 31 May 2020 18:26:18 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/55\n- fixed various endpoint group bugs\n- fixed reload button issue on specific pages\n- fixed edge case for request error handler\n- started refactoring endpoint groups for new architecture\n- made url globals read only in `fullwidth_layout.html`\n\n\n---\n\n[//]: # (END_SECTION d57d36e0580f4dc5dc69536825fd7f26dfe9b77d)\n[//]: # (START_SECTION 8cf4a51e31112475d0e67fe0ff6faa555f59e71a)\n### Added a default .gitignore file and added settings.py to it\n\n> Commit: [8cf4a51e31112475d0e67fe0ff6faa555f59e71a](https://github.com/dOpensource/dsiprouter/commit/8cf4a51e31112475d0e67fe0ff6faa555f59e71a)  \n> Date: Sat, 30 May 2020 01:26:03 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8cf4a51e31112475d0e67fe0ff6faa555f59e71a)\n[//]: # (START_SECTION f33d6146ddd5dbb8fd4a5f5555aa430788c85c72)\n### Put the default settings.py file back\n\n> Commit: [f33d6146ddd5dbb8fd4a5f5555aa430788c85c72](https://github.com/dOpensource/dsiprouter/commit/f33d6146ddd5dbb8fd4a5f5555aa430788c85c72)  \n> Date: Sat, 30 May 2020 01:18:08 +0000  \n> Author: root (root@sbc.dsiprouter.net)  \n> Committer: root (root@sbc.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f33d6146ddd5dbb8fd4a5f5555aa430788c85c72)\n[//]: # (START_SECTION 806831ba03e3a5265bed7022d3d723751c2c6eac)\n### mend\n\n> Commit: [806831ba03e3a5265bed7022d3d723751c2c6eac](https://github.com/dOpensource/dsiprouter/commit/806831ba03e3a5265bed7022d3d723751c2c6eac)  \n> Date: Fri, 29 May 2020 14:23:26 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 806831ba03e3a5265bed7022d3d723751c2c6eac)\n[//]: # (START_SECTION 1ee4502f9b109ca2c06d030c1ec12c1b733348a4)\n### Added API's for managing certs\n\n> Commit: [1ee4502f9b109ca2c06d030c1ec12c1b733348a4](https://github.com/dOpensource/dsiprouter/commit/1ee4502f9b109ca2c06d030c1ec12c1b733348a4)  \n> Date: Fri, 29 May 2020 14:20:09 +0000  \n> Author: root (root@sbc.dsiprouter.net)  \n> Committer: root (root@sbc.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1ee4502f9b109ca2c06d030c1ec12c1b733348a4)\n[//]: # (START_SECTION e1523407df5d05d6019b36faac922ca53fc603e5)\n### Frontend Updates\n\n> Commit: [e1523407df5d05d6019b36faac922ca53fc603e5](https://github.com/dOpensource/dsiprouter/commit/e1523407df5d05d6019b36faac922ca53fc603e5)  \n> Date: Fri, 29 May 2020 09:54:33 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/2\n- Updates https://git.flyball.co/dsiprouter/enterprise/issues/54\n- refactor frontend to be more modular\n- add CSRF protection\n- add error handling to frontend for API calls\n- possible fixes for https://git.flyball.co/dsiprouter/enterprise/issues/53\n\n\n---\n\n[//]: # (END_SECTION e1523407df5d05d6019b36faac922ca53fc603e5)\n[//]: # (START_SECTION 682677ce50baefea11903b0c868a4f5efedbefb2)\n### Bug Fixes for v0.62\n\n> Commit: [682677ce50baefea11903b0c868a4f5efedbefb2](https://github.com/dOpensource/dsiprouter/commit/682677ce50baefea11903b0c868a4f5efedbefb2)  \n> Date: Fri, 22 May 2020 16:56:50 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/52\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/40\n- removed `sslenable` CLI command as this is the default now\n- removed `mpath` CLI command as this is not needed by the end user\n- update CLI command options documentation\n- update CLI tab completion for changes\n- SECURITY FIX: change hash checks to store salt with hash\n- SECURITY FIX: fix size limits on credentials and salts\n- fix logrotate not working with some services\n- fix logging hangs on systemd journal rotate\n- cleanup `dsip_lib.sh` comments\n- fix layout for `dashboard.html` and `endpointgroups.html`\n- fix edge cases for https://git.flyball.co/dsiprouter/enterprise/issues/40\n- fix API redirection for endpointgroups endpoint, ref: https://git.flyball.co/dsiprouter/enterprise/issues/2\n- update `dsip_settings` table to accomdate security fixes\n- add `error` endpoint to GUI so API's can redirect there\n- add several ports to python update functions in `dsiprouter.sh`\n- add hot reloading to kam update functions in `dsiprouter.sh`\n- fix bug in `dr_gateways` and `subscribers` default args\n- consolidate/update credential setting/resetting functions in `dsiprouter.sh`\n- update diplayed credentials upon install finish\n- update `help` commands documentation to be more concise\n- expand `resetpassword` command functionality\n- update several install scripts; ubuntu, debian, centos, amazon\n- add utility functions in `main.js`\n- inject settings into template context processor in `dsiprouter.py`\n- update some tests that were not working; common, 7.sh\n\n\n---\n\n[//]: # (END_SECTION 682677ce50baefea11903b0c868a4f5efedbefb2)\n[//]: # (START_SECTION 1d22d38baeffde8996987d5fe2a6fbf91ad3a4b9)\n### Initial commit of the certificate module\n\n> Commit: [1d22d38baeffde8996987d5fe2a6fbf91ad3a4b9](https://github.com/dOpensource/dsiprouter/commit/1d22d38baeffde8996987d5fe2a6fbf91ad3a4b9)  \n> Date: Fri, 22 May 2020 01:49:24 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1d22d38baeffde8996987d5fe2a6fbf91ad3a4b9)\n[//]: # (START_SECTION 33dc11da36ed72996df70997833e5510e7b45c87)\n### Update README and CONTRIBUTING docs\n\n> Commit: [33dc11da36ed72996df70997833e5510e7b45c87](https://github.com/dOpensource/dsiprouter/commit/33dc11da36ed72996df70997833e5510e7b45c87)  \n> Date: Tue, 19 May 2020 16:04:58 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/42\n- Spruce up `README.md` and update links\n- Add Getting Started section to `CONTRIBUTING.md`\n\n\n---\n\n[//]: # (END_SECTION 33dc11da36ed72996df70997833e5510e7b45c87)\n[//]: # (START_SECTION 2f1aca8ae2ee142d3bb185c204ee5c64b52033ea)\n###   Resolves https://git.flyball.co/dsiprouter/enterprise/issues/40   - move ami build script to more generic `build_image.sh`   - add more system hardening in `pre-snapshot.sh`   - update pw resetting to only apply to VM/VPS image on 1st boot\n\n> Commit: [2f1aca8ae2ee142d3bb185c204ee5c64b52033ea](https://github.com/dOpensource/dsiprouter/commit/2f1aca8ae2ee142d3bb185c204ee5c64b52033ea)  \n> Date: Tue, 19 May 2020 13:47:55 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 2f1aca8ae2ee142d3bb185c204ee5c64b52033ea)\n[//]: # (START_SECTION a958ecfae0c0277bd5b9755e6dacdc480a98d652)\n### Map Gateway Groups to CDRs\n\n> Commit: [a958ecfae0c0277bd5b9755e6dacdc480a98d652](https://github.com/dOpensource/dsiprouter/commit/a958ecfae0c0277bd5b9755e6dacdc480a98d652)  \n> Date: Mon, 18 May 2020 13:45:22 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/48\n- add mapping between CDRs and gwgroups\n- update DB tables to handle new data\n- update api routes to handle new data\n- update CDR's GUI page\n- Add more sophisticated JSON serializer\n- Cleanup `kamailio.cfg`\n- Add support in dr_gateways trigger for adding new attrs\n- Cleanup `dsip_lcr.sql` and `dsip_mainmode.sql`\n- Update GUI to make tables slightly larger / fit better\n- Update dr_gateways defaults to be more reasonable\n\n\n---\n\n[//]: # (END_SECTION a958ecfae0c0277bd5b9755e6dacdc480a98d652)\n[//]: # (START_SECTION 7e3ae78ed13627dc420cac7016921ec85d4aa947)\n### Update README.md\n\n> Commit: [7e3ae78ed13627dc420cac7016921ec85d4aa947](https://github.com/dOpensource/dsiprouter/commit/7e3ae78ed13627dc420cac7016921ec85d4aa947)  \n> Date: Sun, 17 May 2020 12:21:30 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7e3ae78ed13627dc420cac7016921ec85d4aa947)\n[//]: # (START_SECTION 3d9118d7f619649dd6cbfedf510fc94cc8365ac3)\n### Update README.md\n\n> Commit: [3d9118d7f619649dd6cbfedf510fc94cc8365ac3](https://github.com/dOpensource/dsiprouter/commit/3d9118d7f619649dd6cbfedf510fc94cc8365ac3)  \n> Date: Sun, 17 May 2020 12:20:43 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3d9118d7f619649dd6cbfedf510fc94cc8365ac3)\n[//]: # (START_SECTION 51d6bafff4fc1e0cbbb8e2f55efcc959da4722a3)\n### Removed dnsmasq from the -all install parameter\n\n> Commit: [51d6bafff4fc1e0cbbb8e2f55efcc959da4722a3](https://github.com/dOpensource/dsiprouter/commit/51d6bafff4fc1e0cbbb8e2f55efcc959da4722a3)  \n> Date: Sun, 17 May 2020 14:32:24 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 51d6bafff4fc1e0cbbb8e2f55efcc959da4722a3)\n[//]: # (START_SECTION 53dbbfe3e7d9e264e3706f3a76bd668b6995f805)\n### Enrichment Framework\n\n> Commit: [53dbbfe3e7d9e264e3706f3a76bd668b6995f805](https://github.com/dOpensource/dsiprouter/commit/53dbbfe3e7d9e264e3706f3a76bd668b6995f805)  \n> Date: Sun, 17 May 2020 00:00:48 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- - Added support for Inbound Enrichment\n- - Added Enrichment procedure for SignalWire\n\n\n---\n\n[//]: # (END_SECTION 53dbbfe3e7d9e264e3706f3a76bd668b6995f805)\n[//]: # (START_SECTION 606d7460776dbc264fa3822396f8ea47dd628e36)\n### Add New Settings to Database\n\n> Commit: [606d7460776dbc264fa3822396f8ea47dd628e36](https://github.com/dOpensource/dsiprouter/commit/606d7460776dbc264fa3822396f8ea47dd628e36)  \n> Date: Fri, 15 May 2020 18:11:15 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add new settings from https://git.flyball.co/dsiprouter/enterprise/commit/52ecf3ad8b0875477bca362c855c22776df30a8b to dsip_settings table\n- add new settings to database update functions\n\n\n---\n\n[//]: # (END_SECTION 606d7460776dbc264fa3822396f8ea47dd628e36)\n[//]: # (START_SECTION 52ecf3ad8b0875477bca362c855c22776df30a8b)\n### Add Backend Supoport for Managing Domain TLS Certs\n\n> Commit: [52ecf3ad8b0875477bca362c855c22776df30a8b](https://github.com/dOpensource/dsiprouter/commit/52ecf3ad8b0875477bca362c855c22776df30a8b)  \n> Date: Fri, 15 May 2020 17:40:15 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Add backend functions to support https://git.flyball.co/dsiprouter/enterprise/issues/41\n- Make tls module reload on kamreload\n\n\n---\n\n[//]: # (END_SECTION 52ecf3ad8b0875477bca362c855c22776df30a8b)\n[//]: # (START_SECTION 47c2b4bf8e54fa56e329a878ad13e001eb83d6dc)\n### Fix Domain Setting on User/Pass Auth\n\n> Commit: [47c2b4bf8e54fa56e329a878ad13e001eb83d6dc](https://github.com/dOpensource/dsiprouter/commit/47c2b4bf8e54fa56e329a878ad13e001eb83d6dc)  \n> Date: Wed, 13 May 2020 14:29:07 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/33\n- change `EXTERNAL_FQDN` to resolve from external DNS resolver\n- set `EXTERNAL_FQDN` to `EXTERNAL_IP` on failed resolution\n- set default domain when not specified per endpoint group\n\n\n---\n\n[//]: # (END_SECTION 47c2b4bf8e54fa56e329a878ad13e001eb83d6dc)\n[//]: # (START_SECTION 0e4d9929cfcf243a29b0d0da63cffaaf250fe891)\n### Add Support for Gitlab in Hooks\n\n> Commit: [0e4d9929cfcf243a29b0d0da63cffaaf250fe891](https://github.com/dOpensource/dsiprouter/commit/0e4d9929cfcf243a29b0d0da63cffaaf250fe891)  \n> Date: Wed, 13 May 2020 13:51:14 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/43\n\n\n---\n\n[//]: # (END_SECTION 0e4d9929cfcf243a29b0d0da63cffaaf250fe891)\n[//]: # (START_SECTION 159f5320f9466298e66caccb5b483ef6e5055b77)\n### Outbound Routes Carrier Group Selection\n\n> Commit: [159f5320f9466298e66caccb5b483ef6e5055b77](https://github.com/dOpensource/dsiprouter/commit/159f5320f9466298e66caccb5b483ef6e5055b77)  \n> Date: Tue, 12 May 2020 17:00:54 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/27\n- add a selection dropdown to outbound routes for carrier group selection\n\n\n---\n\n[//]: # (END_SECTION 159f5320f9466298e66caccb5b483ef6e5055b77)\n[//]: # (START_SECTION 958e418f8a950b1ed20d690b8309262d05827a01)\n### Fix Crosslinking Issues\n\n> Commit: [958e418f8a950b1ed20d690b8309262d05827a01](https://github.com/dOpensource/dsiprouter/commit/958e418f8a950b1ed20d690b8309262d05827a01)  \n> Date: Tue, 12 May 2020 13:28:34 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/9\n\n\n---\n\n[//]: # (END_SECTION 958e418f8a950b1ed20d690b8309262d05827a01)\n[//]: # (START_SECTION c7deab623214aa39432e4ef483be4e1b5b578dbf)\n### Update Domain on Install\n\n> Commit: [c7deab623214aa39432e4ef483be4e1b5b578dbf](https://github.com/dOpensource/dsiprouter/commit/c7deab623214aa39432e4ef483be4e1b5b578dbf)  \n> Date: Tue, 12 May 2020 13:13:29 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/33\n- update DOMAIN in `settings.py` when python settings updated\n\n\n---\n\n[//]: # (END_SECTION c7deab623214aa39432e4ef483be4e1b5b578dbf)\n[//]: # (START_SECTION 40e8d85965c90166eafb4425f2159949a2e764ed)\n### Update Prefix and Strip Descriptions\n\n> Commit: [40e8d85965c90166eafb4425f2159949a2e764ed](https://github.com/dOpensource/dsiprouter/commit/40e8d85965c90166eafb4425f2159949a2e764ed)  \n> Date: Tue, 12 May 2020 12:58:57 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/20\n- update descriptions to infer call direction supported\n\n\n---\n\n[//]: # (END_SECTION 40e8d85965c90166eafb4425f2159949a2e764ed)\n[//]: # (START_SECTION 29d156221015801c663e86e15c3923c768d62caf)\n### Update Documentation\n\n> Commit: [29d156221015801c663e86e15c3923c768d62caf](https://github.com/dOpensource/dsiprouter/commit/29d156221015801c663e86e15c3923c768d62caf)  \n> Date: Tue, 12 May 2020 12:34:33 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves https://git.flyball.co/dsiprouter/enterprise/issues/21\n- update install options\n- move `CONTRIBUTING.md` to project root (standard location)\n\n\n---\n\n[//]: # (END_SECTION 29d156221015801c663e86e15c3923c768d62caf)\n[//]: # (START_SECTION cbae76acf8e8549ce8b510ee9e5bf10b9665dce8)\n### Fixed regression with gui/settings.py\n\n> Commit: [cbae76acf8e8549ce8b510ee9e5bf10b9665dce8](https://github.com/dOpensource/dsiprouter/commit/cbae76acf8e8549ce8b510ee9e5bf10b9665dce8)  \n> Date: Tue, 12 May 2020 06:03:52 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cbae76acf8e8549ce8b510ee9e5bf10b9665dce8)\n[//]: # (START_SECTION 3916bd328bf72d08f372edb7f79a0d6958cc4f6a)\n### mend\n\n> Commit: [3916bd328bf72d08f372edb7f79a0d6958cc4f6a](https://github.com/dOpensource/dsiprouter/commit/3916bd328bf72d08f372edb7f79a0d6958cc4f6a)  \n> Date: Tue, 12 May 2020 05:25:31 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3916bd328bf72d08f372edb7f79a0d6958cc4f6a)\n[//]: # (START_SECTION 211cb4b9731b40e36b51c4f1e29b0b5491e280ad)\n### Azure Fixes - Fixed bug with INTERNAL_FQDN\n\n> Commit: [211cb4b9731b40e36b51c4f1e29b0b5491e280ad](https://github.com/dOpensource/dsiprouter/commit/211cb4b9731b40e36b51c4f1e29b0b5491e280ad)  \n> Date: Tue, 12 May 2020 05:22:22 +0000  \n> Author: root (root@sbc.dsiprouter.net)  \n> Committer: root (root@sbc.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 211cb4b9731b40e36b51c4f1e29b0b5491e280ad)\n[//]: # (START_SECTION adc817e32355e6f5339c35c0c83dd521cb0ad9c8)\n### Azure Installation Fixes - Added logic to use the hostname command for the hostname if dig can't perform a reverse lookup based on the IP address - Changed the URL used to obtain the vmID from the Azure Metadata service\n\n> Commit: [adc817e32355e6f5339c35c0c83dd521cb0ad9c8](https://github.com/dOpensource/dsiprouter/commit/adc817e32355e6f5339c35c0c83dd521cb0ad9c8)  \n> Date: Tue, 12 May 2020 04:44:19 +0000  \n> Author: root (root@sbc.dsiprouter.net)  \n> Committer: root (root@sbc.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION adc817e32355e6f5339c35c0c83dd521cb0ad9c8)\n[//]: # (START_SECTION 4db9e48d53dc3b098158a30c418a65ce1d095c24)\n### Azure Installation Fixes - Added logic to use the hostname command for the hostname if dig can't perform a reverse lookup based on the IP address - Changed the URL used to obtain the vmID from the Azure Metadata service\n\n> Commit: [4db9e48d53dc3b098158a30c418a65ce1d095c24](https://github.com/dOpensource/dsiprouter/commit/4db9e48d53dc3b098158a30c418a65ce1d095c24)  \n> Date: Tue, 12 May 2020 04:42:16 +0000  \n> Author: root (root@sbc.dsiprouter.net)  \n> Committer: root (root@sbc.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4db9e48d53dc3b098158a30c418a65ce1d095c24)\n[//]: # (START_SECTION 0fcecf8474d694bbd425388b62c67925786c472d)\n### Carrier Groups\n\n> Commit: [0fcecf8474d694bbd425388b62c67925786c472d](https://github.com/dOpensource/dsiprouter/commit/0fcecf8474d694bbd425388b62c67925786c472d)  \n> Date: Mon, 11 May 2020 22:00:24 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n- resolves dsiprouter@enterprise#39\n\n\n---\n\n[//]: # (END_SECTION 0fcecf8474d694bbd425388b62c67925786c472d)\n[//]: # (START_SECTION cc096b110266d6a71ced8a3b27ba3ad9589147d8)\n### Update CONTRIBUTING.md\n\n> Commit: [cc096b110266d6a71ced8a3b27ba3ad9589147d8](https://github.com/dOpensource/dsiprouter/commit/cc096b110266d6a71ced8a3b27ba3ad9589147d8)  \n> Date: Mon, 11 May 2020 15:45:14 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cc096b110266d6a71ced8a3b27ba3ad9589147d8)\n[//]: # (START_SECTION 0e8a5f3c97049b8e101820d33c521215e46c9d28)\n### MSTeams Inbound Integration - Fixed the Kamailio logic so that the contact is not being re-written. Hence, allowing the ACk to be accepted by MSTeams\n\n> Commit: [0e8a5f3c97049b8e101820d33c521215e46c9d28](https://github.com/dOpensource/dsiprouter/commit/0e8a5f3c97049b8e101820d33c521215e46c9d28)  \n> Date: Mon, 11 May 2020 15:42:33 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0e8a5f3c97049b8e101820d33c521215e46c9d28)\n[//]: # (START_SECTION e71a9384f9d3b9cb5dcb280655a1fe1c86badb8e)\n### Update CONTRIBUTING.md\n\n> Commit: [e71a9384f9d3b9cb5dcb280655a1fe1c86badb8e](https://github.com/dOpensource/dsiprouter/commit/e71a9384f9d3b9cb5dcb280655a1fe1c86badb8e)  \n> Date: Mon, 11 May 2020 15:17:43 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e71a9384f9d3b9cb5dcb280655a1fe1c86badb8e)\n[//]: # (START_SECTION 14dd6be9cd21681f23d8ef63bb77805b24df71e0)\n### Update CONTRIBUTING.md\n\n> Commit: [14dd6be9cd21681f23d8ef63bb77805b24df71e0](https://github.com/dOpensource/dsiprouter/commit/14dd6be9cd21681f23d8ef63bb77805b24df71e0)  \n> Date: Mon, 11 May 2020 15:15:35 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 14dd6be9cd21681f23d8ef63bb77805b24df71e0)\n[//]: # (START_SECTION e8668ea90a39405a3518b3a02071ad1f94ae6476)\n### Update CONTRIBUTING.md\n\n> Commit: [e8668ea90a39405a3518b3a02071ad1f94ae6476](https://github.com/dOpensource/dsiprouter/commit/e8668ea90a39405a3518b3a02071ad1f94ae6476)  \n> Date: Mon, 11 May 2020 14:46:10 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e8668ea90a39405a3518b3a02071ad1f94ae6476)\n[//]: # (START_SECTION b617dfc5df3a6d6a93ecafb79fbd1291295392b7)\n### Crontab Edge Case Handling\n\n> Commit: [b617dfc5df3a6d6a93ecafb79fbd1291295392b7](https://github.com/dOpensource/dsiprouter/commit/b617dfc5df3a6d6a93ecafb79fbd1291295392b7)  \n> Date: Fri, 8 May 2020 17:20:34 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- resolves dsiprouter@enterprise#38\n\n\n---\n\n[//]: # (END_SECTION b617dfc5df3a6d6a93ecafb79fbd1291295392b7)\n[//]: # (START_SECTION 2f52ca511ff7f9de4ae3aa2142fe55997c1c3b16)\n### MSTeams Inbound Routing - Added logic that will create an endpoint group when a MSTEAMS domain is created - Updated the Kamailio.cfg will with logic to support inbound MSTEAM calls\n\n> Commit: [2f52ca511ff7f9de4ae3aa2142fe55997c1c3b16](https://github.com/dOpensource/dsiprouter/commit/2f52ca511ff7f9de4ae3aa2142fe55997c1c3b16)  \n> Date: Fri, 8 May 2020 21:00:27 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2f52ca511ff7f9de4ae3aa2142fe55997c1c3b16)\n[//]: # (START_SECTION 9c184a81ef4d7110ed1f013765fc58306b646fcf)\n### Bug Fixes\n\n> Commit: [9c184a81ef4d7110ed1f013765fc58306b646fcf](https://github.com/dOpensource/dsiprouter/commit/9c184a81ef4d7110ed1f013765fc58306b646fcf)  \n> Date: Fri, 8 May 2020 16:34:00 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix endpoints not shown in endpoint groups\n- fix endpoint groups wrong tabs active on re-edit\n- fix endpoint groups update button color on re-edit\n- fix regression with FLT_DIALOG in `kamailio.cfg`\n- fix http/https variable for api\n\n\n---\n\n[//]: # (END_SECTION 9c184a81ef4d7110ed1f013765fc58306b646fcf)\n[//]: # (START_SECTION 466caaad9e55c4d3887389203f37935fa9e2e74f)\n### Fix Proxmox Install\n\n> Commit: [466caaad9e55c4d3887389203f37935fa9e2e74f](https://github.com/dOpensource/dsiprouter/commit/466caaad9e55c4d3887389203f37935fa9e2e74f)  \n> Date: Thu, 7 May 2020 18:36:59 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- resolves dsiprouter@enterprise#36\n\n\n---\n\n[//]: # (END_SECTION 466caaad9e55c4d3887389203f37935fa9e2e74f)\n[//]: # (START_SECTION 2c57eaa22af0449cd931b6c2aaa28226f6eeb5dc)\n###   - resolves dsiprouter@enterprise#24   - resolves dsiprouter@enterprise#29   - fix custom routes issue in shared.py   - add CDR feature enhancements   - redo crontab dsiprouter integration   - create crontab integratino script   - add loading spinning to table layout   - add field validation to endpoints endpoint   - update / optimize dsip_settings table   - update settings.py adding some fields to DB   - fix typos in inboundmapping.js   - update dsip_cdrinfo table   - add cron usability functions   - increase logout timeout while in debug mode   - various changes to install scripts for cron updates   - update CDR Report fields   - add urandomChars to util.security (future use)   - seperate cdrs static code from template code\n\n> Commit: [2c57eaa22af0449cd931b6c2aaa28226f6eeb5dc](https://github.com/dOpensource/dsiprouter/commit/2c57eaa22af0449cd931b6c2aaa28226f6eeb5dc)  \n> Date: Thu, 7 May 2020 17:49:42 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 2c57eaa22af0449cd931b6c2aaa28226f6eeb5dc)\n[//]: # (START_SECTION 63704b7f57b5225a072d58f2dc84b00cec2f9f6e)\n### Fixed issue\n\n> Commit: [63704b7f57b5225a072d58f2dc84b00cec2f9f6e](https://github.com/dOpensource/dsiprouter/commit/63704b7f57b5225a072d58f2dc84b00cec2f9f6e)  \n> Date: Wed, 6 May 2020 21:07:18 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 63704b7f57b5225a072d58f2dc84b00cec2f9f6e)\n[//]: # (START_SECTION af0745134f8e9d7efa14ff0cbff2bf05e76bb7c5)\n### Added dSIPRouter Support for Kamailio 5.3.4 - Added logic to install the dSIPRouter module based on the verison of Kamailio installed - Added source code for dSIPRouter Module\n\n> Commit: [af0745134f8e9d7efa14ff0cbff2bf05e76bb7c5](https://github.com/dOpensource/dsiprouter/commit/af0745134f8e9d7efa14ff0cbff2bf05e76bb7c5)  \n> Date: Wed, 6 May 2020 20:47:06 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION af0745134f8e9d7efa14ff0cbff2bf05e76bb7c5)\n[//]: # (START_SECTION 371bbf536cbd2982f2f25647e885b061a71b662a)\n### CDR Monthly Email Process - Used a function that replace non-alphanumeric characters with underscores. Fixed: https://git.flyball.co/dsiprouter/enterprise/issues/34\n\n> Commit: [371bbf536cbd2982f2f25647e885b061a71b662a](https://github.com/dOpensource/dsiprouter/commit/371bbf536cbd2982f2f25647e885b061a71b662a)  \n> Date: Tue, 5 May 2020 19:31:21 +0000  \n> Author: root (root@sbc3.dsiprouter.net)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 371bbf536cbd2982f2f25647e885b061a71b662a)\n[//]: # (START_SECTION 877e46d801a3a6f47a1cb04231ec22a251c1d05d)\n### Bug Fixes and CLI QOL Updates\n\n> Commit: [877e46d801a3a6f47a1cb04231ec22a251c1d05d](https://github.com/dOpensource/dsiprouter/commit/877e46d801a3a6f47a1cb04231ec22a251c1d05d)  \n> Date: Fri, 1 May 2020 19:53:24 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add dsip uuid generation on install\n- fix syntax bug in `gui/modules/domain/domain_routes.py`\n- add generatekamconfig command to `dsiprouter.sh`\n- add version command to `dsiprouter.sh`\n- add command completion to `dsiprouter` cmd\n- reset default configs\n\n\n---\n\n[//]: # (END_SECTION 877e46d801a3a6f47a1cb04231ec22a251c1d05d)\n[//]: # (START_SECTION 0208e6faccfd390c8948c344869d283bd0612369)\n### Update README.md\n\n> Commit: [0208e6faccfd390c8948c344869d283bd0612369](https://github.com/dOpensource/dsiprouter/commit/0208e6faccfd390c8948c344869d283bd0612369)  \n> Date: Fri, 1 May 2020 13:44:06 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0208e6faccfd390c8948c344869d283bd0612369)\n[//]: # (START_SECTION 43a3d3db002f3040cb7d4807d65cb4b157ed563e)\n### Update README.md\n\n> Commit: [43a3d3db002f3040cb7d4807d65cb4b157ed563e](https://github.com/dOpensource/dsiprouter/commit/43a3d3db002f3040cb7d4807d65cb4b157ed563e)  \n> Date: Fri, 1 May 2020 13:42:32 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 43a3d3db002f3040cb7d4807d65cb4b157ed563e)\n[//]: # (START_SECTION 6e2961f62120e0784b352802f032a7e023a41705)\n### MSTeams Fixes - Fixed Kamailio option messages for MSTeams so that it works with any domain - Fixed the Option Message Status Check - Added styling to the MSTeams Test Connectivity tooltips\n\n> Commit: [6e2961f62120e0784b352802f032a7e023a41705](https://github.com/dOpensource/dsiprouter/commit/6e2961f62120e0784b352802f032a7e023a41705)  \n> Date: Fri, 1 May 2020 12:46:18 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6e2961f62120e0784b352802f032a7e023a41705)\n[//]: # (START_SECTION ab88f69fa9fd45e21854fae16956f47b474595c2)\n### Added Tooltips to MSTeams test connectivity\n\n> Commit: [ab88f69fa9fd45e21854fae16956f47b474595c2](https://github.com/dOpensource/dsiprouter/commit/ab88f69fa9fd45e21854fae16956f47b474595c2)  \n> Date: Fri, 1 May 2020 11:49:11 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ab88f69fa9fd45e21854fae16956f47b474595c2)\n[//]: # (START_SECTION 6117350ddcd962e464318e8757399155507cb48f)\n### Fix MSTeams hostname check\n\n> Commit: [6117350ddcd962e464318e8757399155507cb48f](https://github.com/dOpensource/dsiprouter/commit/6117350ddcd962e464318e8757399155507cb48f)  \n> Date: Fri, 1 May 2020 10:50:12 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6117350ddcd962e464318e8757399155507cb48f)\n[//]: # (START_SECTION 291097a63daf3743365e8a53cdf56e2077802cd8)\n### Fixes for MSTeams Connectivity Checks\n\n> Commit: [291097a63daf3743365e8a53cdf56e2077802cd8](https://github.com/dOpensource/dsiprouter/commit/291097a63daf3743365e8a53cdf56e2077802cd8)  \n> Date: Fri, 1 May 2020 04:37:30 +0000  \n> Author: root (root@sbc3.dsiprouter.net)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 291097a63daf3743365e8a53cdf56e2077802cd8)\n[//]: # (START_SECTION 883ff5e6ba4d94da068e855ce1fc0f309ea7f382)\n### v0.60 Release Bug Fixes\n\n> Commit: [883ff5e6ba4d94da068e855ce1fc0f309ea7f382](https://github.com/dOpensource/dsiprouter/commit/883ff5e6ba4d94da068e855ce1fc0f309ea7f382)  \n> Date: Thu, 30 Apr 2020 18:58:26 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix bug in `dsiprouter/dsip_lib.sh` that caused bad kamcfg settings\n- fix uac auth bug causing improper uacreg lookup\n- add validated carrier IP's for centurylink carrier enrichment\n- add some TODO's to `dsiprouter.sh`\n- reset defaults in `gui/settings.py`\n- fix bug in `gui/shared.py` causing exceptions when debugging\n\n\n---\n\n[//]: # (END_SECTION 883ff5e6ba4d94da068e855ce1fc0f309ea7f382)\n[//]: # (START_SECTION d1c0d7f6556cff41c5ef4dc99ce5502a5e48fe7e)\n### Fixed RTP selection\n\n> Commit: [d1c0d7f6556cff41c5ef4dc99ce5502a5e48fe7e](https://github.com/dOpensource/dsiprouter/commit/d1c0d7f6556cff41c5ef4dc99ce5502a5e48fe7e)  \n> Date: Thu, 30 Apr 2020 16:03:06 +0000  \n> Author: root (root@sbc3.dsiprouter.net)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d1c0d7f6556cff41c5ef4dc99ce5502a5e48fe7e)\n[//]: # (START_SECTION 8114297a1d628d8f277dbc21f05e5861c1be9e72)\n### MOTD Updates and set the protocol of the GUI to http\n\n> Commit: [8114297a1d628d8f277dbc21f05e5861c1be9e72](https://github.com/dOpensource/dsiprouter/commit/8114297a1d628d8f277dbc21f05e5861c1be9e72)  \n> Date: Thu, 30 Apr 2020 02:14:06 +0000  \n> Author: root (root@sbc3.dsiprouter.net)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8114297a1d628d8f277dbc21f05e5861c1be9e72)\n[//]: # (START_SECTION 03fb8aece5a640dc3db4b69e79b0d7413a0aa4db)\n### Installer / GUI Fixes\n\n> Commit: [03fb8aece5a640dc3db4b69e79b0d7413a0aa4db](https://github.com/dOpensource/dsiprouter/commit/03fb8aece5a640dc3db4b69e79b0d7413a0aa4db)  \n> Date: Wed, 29 Apr 2020 19:16:08 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- various GUI fixes\n- installer fixes\n\n\n---\n\n[//]: # (END_SECTION 03fb8aece5a640dc3db4b69e79b0d7413a0aa4db)\n[//]: # (START_SECTION 246a128be0e5e83a193dc73b8be1400867ceeca4)\n### Fixed issue with endpointgroup regressions\n\n> Commit: [246a128be0e5e83a193dc73b8be1400867ceeca4](https://github.com/dOpensource/dsiprouter/commit/246a128be0e5e83a193dc73b8be1400867ceeca4)  \n> Date: Wed, 29 Apr 2020 22:26:33 +0000  \n> Author: root (root@sbc3.dsiprouter.net)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 246a128be0e5e83a193dc73b8be1400867ceeca4)\n[//]: # (START_SECTION 8e5192baf3c5772c63bb4ddabebd4a1231950409)\n### Fixed regression with endpointgroup\n\n> Commit: [8e5192baf3c5772c63bb4ddabebd4a1231950409](https://github.com/dOpensource/dsiprouter/commit/8e5192baf3c5772c63bb4ddabebd4a1231950409)  \n> Date: Wed, 29 Apr 2020 21:09:50 +0000  \n> Author: root (root@sbc3.dsiprouter.net)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8e5192baf3c5772c63bb4ddabebd4a1231950409)\n[//]: # (START_SECTION e1d46db6f7bed641bdcbd127e18e5c4c796afb65)\n### Fixed regression with endpointgroup\n\n> Commit: [e1d46db6f7bed641bdcbd127e18e5c4c796afb65](https://github.com/dOpensource/dsiprouter/commit/e1d46db6f7bed641bdcbd127e18e5c4c796afb65)  \n> Date: Wed, 29 Apr 2020 20:45:23 +0000  \n> Author: root (root@sbc3.dsiprouter.net)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e1d46db6f7bed641bdcbd127e18e5c4c796afb65)\n[//]: # (START_SECTION 20fcdf84403bab2f83ccca2b7b059b4c5f838764)\n### Packaging fixes - Changed login splash pic - Updated version from 0.60+ent to just 0.60\n\n> Commit: [20fcdf84403bab2f83ccca2b7b059b4c5f838764](https://github.com/dOpensource/dsiprouter/commit/20fcdf84403bab2f83ccca2b7b059b4c5f838764)  \n> Date: Wed, 29 Apr 2020 15:05:37 +0000  \n> Author: root (root@sbc3.dsiprouter.net)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 20fcdf84403bab2f83ccca2b7b059b4c5f838764)\n[//]: # (START_SECTION 73be03970380f6b4ee6bed206672290f2f700f13)\n### We applied commmit 4d67b83 becasue it was overriden\n\n> Commit: [73be03970380f6b4ee6bed206672290f2f700f13](https://github.com/dOpensource/dsiprouter/commit/73be03970380f6b4ee6bed206672290f2f700f13)  \n> Date: Mon, 27 Apr 2020 19:08:38 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 73be03970380f6b4ee6bed206672290f2f700f13)\n[//]: # (START_SECTION 49f7abb47ecc6f5799be16aa057c7382bbbdd1cb)\n### Fixed error in install script for configuring SSL\n\n> Commit: [49f7abb47ecc6f5799be16aa057c7382bbbdd1cb](https://github.com/dOpensource/dsiprouter/commit/49f7abb47ecc6f5799be16aa057c7382bbbdd1cb)  \n> Date: Wed, 29 Apr 2020 11:10:57 +0000  \n> Author: root (root@sbc2.dsiprouter.net)  \n> Committer: root (root@sbc2.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 49f7abb47ecc6f5799be16aa057c7382bbbdd1cb)\n[//]: # (START_SECTION 9f09554d741c879c37b50561ff4610a635328d17)\n### SSL Fixes - Added a renewsslcert option to dsiprouter.sh - Added a cronjob that will call renewsslcert.  It will revnew the Let's Encrypt Certificate if it needs to be renewed\n\n> Commit: [9f09554d741c879c37b50561ff4610a635328d17](https://github.com/dOpensource/dsiprouter/commit/9f09554d741c879c37b50561ff4610a635328d17)  \n> Date: Wed, 29 Apr 2020 05:24:38 +0000  \n> Author: Mack Hendricks (mack@dopensource.net)  \n> Committer: Mack Hendricks (mack@dopensource.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9f09554d741c879c37b50561ff4610a635328d17)\n[//]: # (START_SECTION 4d1ca93260487e72f2c9767892e100fcc780127f)\n### Pre-release Fixes\n\n> Commit: [4d1ca93260487e72f2c9767892e100fcc780127f](https://github.com/dOpensource/dsiprouter/commit/4d1ca93260487e72f2c9767892e100fcc780127f)  \n> Date: Tue, 28 Apr 2020 20:25:14 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update dispatcher settings in kam config\n- cleanup non-debug output on install\n- fix regressions in api routes\n- fix external fqdn resolution bug\n- various code reformatting\n- remove copy of file_handling.py\n\n\n---\n\n[//]: # (END_SECTION 4d1ca93260487e72f2c9767892e100fcc780127f)\n[//]: # (START_SECTION 62b3f81321d59bc796f2203a349415e54395ec2a)\n### Fixed an issue with importing addresses that contain MSTeams SBC's\n\n> Commit: [62b3f81321d59bc796f2203a349415e54395ec2a](https://github.com/dOpensource/dsiprouter/commit/62b3f81321d59bc796f2203a349415e54395ec2a)  \n> Date: Tue, 28 Apr 2020 22:44:39 +0000  \n> Author: Mack Hendricks (mack@dopensource.net)  \n> Committer: Mack Hendricks (mack@dopensource.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 62b3f81321d59bc796f2203a349415e54395ec2a)\n[//]: # (START_SECTION 1165c6fcf796434b5924a747527a9d31e3e2513f)\n### Updating the Support links\n\n> Commit: [1165c6fcf796434b5924a747527a9d31e3e2513f](https://github.com/dOpensource/dsiprouter/commit/1165c6fcf796434b5924a747527a9d31e3e2513f)  \n> Date: Tue, 28 Apr 2020 18:06:55 +0000  \n> Author: Mack Hendricks (mack@dopensource.net)  \n> Committer: Mack Hendricks (mack@dopensource.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1165c6fcf796434b5924a747527a9d31e3e2513f)\n[//]: # (START_SECTION 4f31943f01eec31d76c18352c3c8da5059d50184)\n### Fixed a regression in dsiprouter.sh - An echo command was in mistakenly packed in the sources.list file\n\n> Commit: [4f31943f01eec31d76c18352c3c8da5059d50184](https://github.com/dOpensource/dsiprouter/commit/4f31943f01eec31d76c18352c3c8da5059d50184)  \n> Date: Tue, 28 Apr 2020 14:35:10 +0000  \n> Author: root (root@sbc2.dsiprouter.net)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4f31943f01eec31d76c18352c3c8da5059d50184)\n[//]: # (START_SECTION 465acc71768d23ed3f544ede236cd8fa8d107cc7)\n### MS Teams Support - Added dsiprouter module binary to the repo and logic to install it - Added logic to the Domains page that checks if the user has a valid dSIPRouter Subscription - Added a button to check MS Teams Connectivity\n\n> Commit: [465acc71768d23ed3f544ede236cd8fa8d107cc7](https://github.com/dOpensource/dsiprouter/commit/465acc71768d23ed3f544ede236cd8fa8d107cc7)  \n> Date: Tue, 28 Apr 2020 13:31:47 +0000  \n> Author: Mack Hendricks (mack@dopensource.net)  \n> Committer: Mack Hendricks (mack@dopensource.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 465acc71768d23ed3f544ede236cd8fa8d107cc7)\n[//]: # (START_SECTION 4d67b831ad017435924be58ae4b91d34bc302ab1)\n### v0.60+ent pre-release fixes\n\n> Commit: [4d67b831ad017435924be58ae4b91d34bc302ab1](https://github.com/dOpensource/dsiprouter/commit/4d67b831ad017435924be58ae4b91d34bc302ab1)  \n> Date: Mon, 27 Apr 2020 19:08:38 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix subscriber table reversion\n- add animation to hidden css class\n- fix endpoint groups ip auth selection issue\n\n\n---\n\n[//]: # (END_SECTION 4d67b831ad017435924be58ae4b91d34bc302ab1)\n[//]: # (START_SECTION 9c69fc7899a019d48f63185b1aa1a1aa3cfe36e6)\n### Small Fixes\n\n> Commit: [9c69fc7899a019d48f63185b1aa1a1aa3cfe36e6](https://github.com/dOpensource/dsiprouter/commit/9c69fc7899a019d48f63185b1aa1a1aa3cfe36e6)  \n> Date: Mon, 27 Apr 2020 16:34:04 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- move ENRICH_CARRIER_CENTURYLINK route outside of custom routes\n- fix custom routes display in outbound routes\n- fix endpoint group icon misaligned\n- commit current work on endpoint group ip auth selection bug fix\n- fix carrier group auth proxy bug by using r_username\n- small formatting updates\n- update debian9 install to use /etc/apt/sources.list.d for kam repos\n\n\n---\n\n[//]: # (END_SECTION 9c69fc7899a019d48f63185b1aa1a1aa3cfe36e6)\n[//]: # (START_SECTION ad576c81c8872ae8f3e2c913bf1a7aa89f2be46f)\n### Adding Fixes\n\n> Commit: [ad576c81c8872ae8f3e2c913bf1a7aa89f2be46f](https://github.com/dOpensource/dsiprouter/commit/ad576c81c8872ae8f3e2c913bf1a7aa89f2be46f)  \n> Date: Mon, 27 Apr 2020 13:34:21 +0000  \n> Author: Mack Hendricks (mack@dopensource.net)  \n> Committer: Mack Hendricks (mack@dopensource.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ad576c81c8872ae8f3e2c913bf1a7aa89f2be46f)\n[//]: # (START_SECTION 792ee6d70184c078a4d0e7eb7a5759daffde9575)\n### Added a firewall rule to allow Letsencrypt to validate the hostname\n\n> Commit: [792ee6d70184c078a4d0e7eb7a5759daffde9575](https://github.com/dOpensource/dsiprouter/commit/792ee6d70184c078a4d0e7eb7a5759daffde9575)  \n> Date: Fri, 24 Apr 2020 11:26:27 +0000  \n> Author: root (root@sbc2.dsiprouter.net)  \n> Committer: root (root@sbc2.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 792ee6d70184c078a4d0e7eb7a5759daffde9575)\n[//]: # (START_SECTION a726868f2a0352dd69509f29b228753f066bf0b4)\n### Added certbot to the Kamailio installer\n\n> Commit: [a726868f2a0352dd69509f29b228753f066bf0b4](https://github.com/dOpensource/dsiprouter/commit/a726868f2a0352dd69509f29b228753f066bf0b4)  \n> Date: Fri, 24 Apr 2020 11:00:46 +0000  \n> Author: root (root@sbc2.dsiprouter.net)  \n> Committer: root (root@sbc2.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a726868f2a0352dd69509f29b228753f066bf0b4)\n[//]: # (START_SECTION d9f9802ae6c4fe2f0e81b3bb033d53c948dc44e1)\n### Fixed issues with installed\n\n> Commit: [d9f9802ae6c4fe2f0e81b3bb033d53c948dc44e1](https://github.com/dOpensource/dsiprouter/commit/d9f9802ae6c4fe2f0e81b3bb033d53c948dc44e1)  \n> Date: Fri, 24 Apr 2020 10:49:35 +0000  \n> Author: root (root@sbc2.dsiprouter.net)  \n> Committer: root (root@sbc2.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d9f9802ae6c4fe2f0e81b3bb033d53c948dc44e1)\n[//]: # (START_SECTION bad988b5fc9f0656ef94d7f60547b15349f925d7)\n### Kamailio password has to be in plain text during the initial install\n\n> Commit: [bad988b5fc9f0656ef94d7f60547b15349f925d7](https://github.com/dOpensource/dsiprouter/commit/bad988b5fc9f0656ef94d7f60547b15349f925d7)  \n> Date: Fri, 24 Apr 2020 10:34:51 +0000  \n> Author: root (root@sbc2.dsiprouter.net)  \n> Committer: root (root@sbc2.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bad988b5fc9f0656ef94d7f60547b15349f925d7)\n[//]: # (START_SECTION 82c2cc261fc2899d674cae1faf4c3b3f9ee4482f)\n### Installed Fixes - Added Letencrypt support for MSTeams - Fixed issue with MSTeams gateways being configured on install - Remove PySpark from the Python Requirements.txt file\n\n> Commit: [82c2cc261fc2899d674cae1faf4c3b3f9ee4482f](https://github.com/dOpensource/dsiprouter/commit/82c2cc261fc2899d674cae1faf4c3b3f9ee4482f)  \n> Date: Thu, 23 Apr 2020 23:37:43 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 82c2cc261fc2899d674cae1faf4c3b3f9ee4482f)\n[//]: # (START_SECTION a6e0d1325b70c263ef61bac6480a5724cda69d63)\n### Updated the version of Kamailio to 5.3\n\n> Commit: [a6e0d1325b70c263ef61bac6480a5724cda69d63](https://github.com/dOpensource/dsiprouter/commit/a6e0d1325b70c263ef61bac6480a5724cda69d63)  \n> Date: Thu, 23 Apr 2020 04:24:04 +0000  \n> Author: Mack Hendricks (mack@dopensource.net)  \n> Committer: Mack Hendricks (mack@dopensource.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a6e0d1325b70c263ef61bac6480a5724cda69d63)\n[//]: # (START_SECTION e122a48ff42eb16f07d1e4c60fa5e798c137c133)\n### Added MSTeams Support - Support from calling from MSTeams Client to a Carrier using SIP with SRTP - Added the MSTeams SBC's to the address table on install - Updated the Kamailio TLS file so that it installs with the proper configuration - Changed the default amount of Shared and Private Memory from 64MB and 8MB to 128MB and 16MB respectfully\n\n> Commit: [e122a48ff42eb16f07d1e4c60fa5e798c137c133](https://github.com/dOpensource/dsiprouter/commit/e122a48ff42eb16f07d1e4c60fa5e798c137c133)  \n> Date: Thu, 23 Apr 2020 04:17:53 +0000  \n> Author: Mack Hendricks (mack@dopensource.net)  \n> Committer: Mack Hendricks (mack@dopensource.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e122a48ff42eb16f07d1e4c60fa5e798c137c133)\n[//]: # (START_SECTION a57a7d284d6b29d26d661fdcb2fb5a568a3d6399)\n### Updated Carrier Registration - Can support auth username and auth password authentication, where the SIP username can be different then auth username - Support manual input of auth_proxy URI from the GUI\n\n> Commit: [a57a7d284d6b29d26d661fdcb2fb5a568a3d6399](https://github.com/dOpensource/dsiprouter/commit/a57a7d284d6b29d26d661fdcb2fb5a568a3d6399)  \n> Date: Tue, 21 Apr 2020 17:10:26 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a57a7d284d6b29d26d661fdcb2fb5a568a3d6399)\n[//]: # (START_SECTION bc080a6a5218483c333d8a8d62ad3c9f42effc8a)\n### Added Auth Proxy authentication\n\n> Commit: [bc080a6a5218483c333d8a8d62ad3c9f42effc8a](https://github.com/dOpensource/dsiprouter/commit/bc080a6a5218483c333d8a8d62ad3c9f42effc8a)  \n> Date: Mon, 20 Apr 2020 21:46:18 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bc080a6a5218483c333d8a8d62ad3c9f42effc8a)\n[//]: # (START_SECTION 8ffa9ff5428a7e90f5946ab178517578cd5dbe7f)\n### Added Support for CenturyLink Carrier - Added an Enrichment Carrier Framework where custom SIP manipulation can be done per the carriers requirements - Updated the logic for doing User/Password auth to a carrier\n\n> Commit: [8ffa9ff5428a7e90f5946ab178517578cd5dbe7f](https://github.com/dOpensource/dsiprouter/commit/8ffa9ff5428a7e90f5946ab178517578cd5dbe7f)  \n> Date: Mon, 20 Apr 2020 12:50:10 +0000  \n> Author: root (root@dSIP060entNightly-0.localdomain)  \n> Committer: root (root@dSIP060entNightly-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8ffa9ff5428a7e90f5946ab178517578cd5dbe7f)\n[//]: # (START_SECTION 013ef8b186632b8e625a7f70a688291e4fd93204)\n### FusionPBX Domain Routing Fixes - Modified the logic to work with Endpoint Groups - Fixed the Domains page so that it displays the Endpoint Group that contains the FusionPBX Server - Fixed the Fusion Sync script so that it works with Endpoint Groups - Fixed an issue with the Kamailio Script that was caused by the DMQ module, which prevent endponts from registering\n\n> Commit: [013ef8b186632b8e625a7f70a688291e4fd93204](https://github.com/dOpensource/dsiprouter/commit/013ef8b186632b8e625a7f70a688291e4fd93204)  \n> Date: Sun, 19 Apr 2020 14:27:45 +0000  \n> Author: root (root@dSIP060entNightly-0.localdomain)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 013ef8b186632b8e625a7f70a688291e4fd93204)\n[//]: # (START_SECTION a297703456ffe8ec046d25411f087d303ee8527b)\n### Turned DMQ off by default\n\n> Commit: [a297703456ffe8ec046d25411f087d303ee8527b](https://github.com/dOpensource/dsiprouter/commit/a297703456ffe8ec046d25411f087d303ee8527b)  \n> Date: Sat, 18 Apr 2020 18:41:27 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a297703456ffe8ec046d25411f087d303ee8527b)\n[//]: # (START_SECTION ef54273ce5a79fc9fa4f268c67f647be21854a6a)\n### SIP to WebRTC and WebRTC to SIP has been tested with 2-way audio\n\n> Commit: [ef54273ce5a79fc9fa4f268c67f647be21854a6a](https://github.com/dOpensource/dsiprouter/commit/ef54273ce5a79fc9fa4f268c67f647be21854a6a)  \n> Date: Sat, 18 Apr 2020 16:14:09 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ef54273ce5a79fc9fa4f268c67f647be21854a6a)\n[//]: # (START_SECTION 3c19fec781e2176b5135ee5f082c43008f47bdef)\n### Merged in changes from gogcit branch\n\n> Commit: [3c19fec781e2176b5135ee5f082c43008f47bdef](https://github.com/dOpensource/dsiprouter/commit/3c19fec781e2176b5135ee5f082c43008f47bdef)  \n> Date: Fri, 17 Apr 2020 10:39:57 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3c19fec781e2176b5135ee5f082c43008f47bdef)\n[//]: # (START_SECTION efc63d151901fdcb6dcccca38c723091ed3670e5)\n### SIP to Web Working with Contact being re-written properly for FusionPBX\n\n> Commit: [efc63d151901fdcb6dcccca38c723091ed3670e5](https://github.com/dOpensource/dsiprouter/commit/efc63d151901fdcb6dcccca38c723091ed3670e5)  \n> Date: Tue, 14 Apr 2020 03:44:49 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION efc63d151901fdcb6dcccca38c723091ed3670e5)\n[//]: # (START_SECTION c03761dd6db163fdac549c68dc765f2b1b2de134)\n### Testing from Web to SIP worked\n\n> Commit: [c03761dd6db163fdac549c68dc765f2b1b2de134](https://github.com/dOpensource/dsiprouter/commit/c03761dd6db163fdac549c68dc765f2b1b2de134)  \n> Date: Sun, 12 Apr 2020 12:51:26 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c03761dd6db163fdac549c68dc765f2b1b2de134)\n[//]: # (START_SECTION 11c48611038c211b9272a39eb1a558beb257ae2a)\n### Initial testing of Web to SIP\n\n> Commit: [11c48611038c211b9272a39eb1a558beb257ae2a](https://github.com/dOpensource/dsiprouter/commit/11c48611038c211b9272a39eb1a558beb257ae2a)  \n> Date: Sat, 11 Apr 2020 23:07:52 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 11c48611038c211b9272a39eb1a558beb257ae2a)\n[//]: # (START_SECTION 15a7af2fdbe19138fc1d279301322941d72df051)\n### Updated configuration file with SIP-to-SIP calls working with Audio\n\n> Commit: [15a7af2fdbe19138fc1d279301322941d72df051](https://github.com/dOpensource/dsiprouter/commit/15a7af2fdbe19138fc1d279301322941d72df051)  \n> Date: Sat, 11 Apr 2020 18:04:02 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 15a7af2fdbe19138fc1d279301322941d72df051)\n[//]: # (START_SECTION 50e4a4832f60cb11953440d6ab5cc318ba6c292f)\n### Added logic to decode the encrypted database password\n\n> Commit: [50e4a4832f60cb11953440d6ab5cc318ba6c292f](https://github.com/dOpensource/dsiprouter/commit/50e4a4832f60cb11953440d6ab5cc318ba6c292f)  \n> Date: Sat, 11 Apr 2020 16:07:58 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 50e4a4832f60cb11953440d6ab5cc318ba6c292f)\n[//]: # (START_SECTION 71c67854aaf9634bb00d3f0cabf04faa45b1703d)\n### Integrated the refactored functions into the script\n\n> Commit: [71c67854aaf9634bb00d3f0cabf04faa45b1703d](https://github.com/dOpensource/dsiprouter/commit/71c67854aaf9634bb00d3f0cabf04faa45b1703d)  \n> Date: Sat, 11 Apr 2020 13:18:11 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 71c67854aaf9634bb00d3f0cabf04faa45b1703d)\n[//]: # (START_SECTION 726439a19dd7ea4ae87b185e68bcfd348cf8fdc5)\n### Fixed dSIPCDRInfo Table - The definition was missing in the database mapping file\n\n> Commit: [726439a19dd7ea4ae87b185e68bcfd348cf8fdc5](https://github.com/dOpensource/dsiprouter/commit/726439a19dd7ea4ae87b185e68bcfd348cf8fdc5)  \n> Date: Fri, 10 Apr 2020 11:32:54 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 726439a19dd7ea4ae87b185e68bcfd348cf8fdc5)\n[//]: # (START_SECTION a7db2e9b74cc69f9862d3216d2c857ebaf57dc14)\n### Initial Refactoring of Kamailio.cfg\n\n> Commit: [a7db2e9b74cc69f9862d3216d2c857ebaf57dc14](https://github.com/dOpensource/dsiprouter/commit/a7db2e9b74cc69f9862d3216d2c857ebaf57dc14)  \n> Date: Sat, 11 Apr 2020 12:18:33 +0000  \n> Author: root (root@dSIP060entNightly-0.localdomain)  \n> Committer: root (root@dSIP060entNightly-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a7db2e9b74cc69f9862d3216d2c857ebaf57dc14)\n[//]: # (START_SECTION f126871d033c243282abc4e572d68f52a6987d8a)\n### Fixed dSIPCDRInfo Table - The definition was missing in the database mapping file\n\n> Commit: [f126871d033c243282abc4e572d68f52a6987d8a](https://github.com/dOpensource/dsiprouter/commit/f126871d033c243282abc4e572d68f52a6987d8a)  \n> Date: Fri, 10 Apr 2020 11:32:54 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f126871d033c243282abc4e572d68f52a6987d8a)\n[//]: # (START_SECTION f2b2a9202d15e047129f223bfd1046c240f172d0)\n### Added WebSocket Support - Added the kamailio websocket module to the install on Debian 9 - Added Support in Kamailio script\n\n> Commit: [f2b2a9202d15e047129f223bfd1046c240f172d0](https://github.com/dOpensource/dsiprouter/commit/f2b2a9202d15e047129f223bfd1046c240f172d0)  \n> Date: Fri, 10 Apr 2020 01:03:02 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f2b2a9202d15e047129f223bfd1046c240f172d0)\n[//]: # (START_SECTION eeac604c28012dd95c83af6d9164fed333f5c587)\n### Fixed Issue #22 - Bug in the Inbound DID Mapping Import\n\n> Commit: [eeac604c28012dd95c83af6d9164fed333f5c587](https://github.com/dOpensource/dsiprouter/commit/eeac604c28012dd95c83af6d9164fed333f5c587)  \n> Date: Tue, 7 Apr 2020 05:17:56 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eeac604c28012dd95c83af6d9164fed333f5c587)\n[//]: # (START_SECTION 6deb85b4571275288851976ec031caac1101dc3d)\n### Fixed typo\n\n> Commit: [6deb85b4571275288851976ec031caac1101dc3d](https://github.com/dOpensource/dsiprouter/commit/6deb85b4571275288851976ec031caac1101dc3d)  \n> Date: Fri, 3 Apr 2020 11:41:24 +0000  \n> Author: Mack Hendricks (mack@dopensource.net)  \n> Committer: Mack Hendricks (mack@dopensource.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6deb85b4571275288851976ec031caac1101dc3d)\n[//]: # (START_SECTION c1ea635a6606e7800f03813ead94aa660f9d374f)\n### MS Teams Support - Ability to handle one MS Teams Domain - Configure CA List for Kamaiio certs - Added MS Teams Support to Domains - Added a MS Teams Conifguration page\n\n> Commit: [c1ea635a6606e7800f03813ead94aa660f9d374f](https://github.com/dOpensource/dsiprouter/commit/c1ea635a6606e7800f03813ead94aa660f9d374f)  \n> Date: Fri, 3 Apr 2020 10:20:26 +0000  \n> Author: Mack Hendricks (mack@dopensource.net)  \n> Committer: Mack Hendricks (mack@dopensource.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c1ea635a6606e7800f03813ead94aa660f9d374f)\n[//]: # (START_SECTION e244a66db7c5f9bdad8697c4af31e9f33126bcac)\n### Microsoft Teams Support - Added configuration to the Kamailio configuration to have a listen address with a hostname - Added External Hostname parameters - Configured TLS configuration to validate server certs - Add support for sending OPTION messages with a Contact Header\n\n> Commit: [e244a66db7c5f9bdad8697c4af31e9f33126bcac](https://github.com/dOpensource/dsiprouter/commit/e244a66db7c5f9bdad8697c4af31e9f33126bcac)  \n> Date: Tue, 31 Mar 2020 22:48:01 +0000  \n> Author: Mack Hendricks (mack@dopensource.net)  \n> Committer: Mack Hendricks (mack@dopensource.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e244a66db7c5f9bdad8697c4af31e9f33126bcac)\n[//]: # (START_SECTION 470fbe91ac813f08980a9603b4b3a6d77a903825)\n### Microsoft Teams Support - Added configuration to the Kamailio configuration to have a listen address with a hostname - Added External Hostname parameters - Configured TLS configuration to validate server certs - Add support for sending OPTION messages with a Contact Header\n\n> Commit: [470fbe91ac813f08980a9603b4b3a6d77a903825](https://github.com/dOpensource/dsiprouter/commit/470fbe91ac813f08980a9603b4b3a6d77a903825)  \n> Date: Tue, 31 Mar 2020 22:46:43 +0000  \n> Author: root (root@sbc3.dsiprouter.net)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 470fbe91ac813f08980a9603b4b3a6d77a903825)\n[//]: # (START_SECTION 901af97eb7a61588afcb32a7747c167c6736295f)\n### System CA Certs - Configured Kamailio to use the CA's shipped with the OS\n\n> Commit: [901af97eb7a61588afcb32a7747c167c6736295f](https://github.com/dOpensource/dsiprouter/commit/901af97eb7a61588afcb32a7747c167c6736295f)  \n> Date: Mon, 30 Mar 2020 00:39:50 +0000  \n> Author: root (root@sbc3.dsiprouter.net)  \n> Committer: root (root@sbc3.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 901af97eb7a61588afcb32a7747c167c6736295f)\n[//]: # (START_SECTION b734fc096d4e3d6534a21f67b053b4491d67513b)\n### SSL Enable - Fixed permission issues\n\n> Commit: [b734fc096d4e3d6534a21f67b053b4491d67513b](https://github.com/dOpensource/dsiprouter/commit/b734fc096d4e3d6534a21f67b053b4491d67513b)  \n> Date: Mon, 30 Mar 2020 00:04:45 +0000  \n> Author: root (root@sbc2.dsiprouter.net)  \n> Committer: root (root@sbc2.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b734fc096d4e3d6534a21f67b053b4491d67513b)\n[//]: # (START_SECTION 67d5925d3dedfb92935a3983010dfba6286ea636)\n### Added LetsEncrypt Support - Will try to create LetsEncrypt certs if TEAMS support is enabled - Will create Self-Signed Certs if LetsEncrypt fails - Started a CONTRIBUTING page to capture our coding standards\n\n> Commit: [67d5925d3dedfb92935a3983010dfba6286ea636](https://github.com/dOpensource/dsiprouter/commit/67d5925d3dedfb92935a3983010dfba6286ea636)  \n> Date: Sun, 29 Mar 2020 17:40:47 +0000  \n> Author: root (root@sbc2.dsiprouter.net)  \n> Committer: root (root@sbc2.dsiprouter.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 67d5925d3dedfb92935a3983010dfba6286ea636)\n[//]: # (START_SECTION 7143eb17c6c1cc9c78c7b6600f30452f921f7f8c)\n### Fix Carrier Update Changing Reload Button\n\n> Commit: [7143eb17c6c1cc9c78c7b6600f30452f921f7f8c](https://github.com/dOpensource/dsiprouter/commit/7143eb17c6c1cc9c78c7b6600f30452f921f7f8c)  \n> Date: Thu, 26 Mar 2020 16:25:53 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update carriers.html to propagate reload state\n- Resolves #15\n\n\n---\n\n[//]: # (END_SECTION 7143eb17c6c1cc9c78c7b6600f30452f921f7f8c)\n[//]: # (START_SECTION eefe02ba547f850e9da5c56892a9ac45cfbb29c4)\n### Fix Carrier Address Mismatch Issue\n\n> Commit: [eefe02ba547f850e9da5c56892a9ac45cfbb29c4](https://github.com/dOpensource/dsiprouter/commit/eefe02ba547f850e9da5c56892a9ac45cfbb29c4)  \n> Date: Thu, 26 Mar 2020 15:31:27 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update dr_gateways to track address entry id\n- Resolves #18\n\n\n---\n\n[//]: # (END_SECTION eefe02ba547f850e9da5c56892a9ac45cfbb29c4)\n[//]: # (START_SECTION a6d7a717a154ae35ae26baaa396978d3d7ba4a68)\n### Set KAM_DB_PASS to the default value of kamailiorw\n\n> Commit: [a6d7a717a154ae35ae26baaa396978d3d7ba4a68](https://github.com/dOpensource/dsiprouter/commit/a6d7a717a154ae35ae26baaa396978d3d7ba4a68)  \n> Date: Wed, 25 Mar 2020 11:11:14 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a6d7a717a154ae35ae26baaa396978d3d7ba4a68)\n[//]: # (START_SECTION b59ada6025e875ea781d2b0f07cd2cab114543d1)\n### Fixed a regression\n\n> Commit: [b59ada6025e875ea781d2b0f07cd2cab114543d1](https://github.com/dOpensource/dsiprouter/commit/b59ada6025e875ea781d2b0f07cd2cab114543d1)  \n> Date: Wed, 25 Mar 2020 10:25:07 +0000  \n> Author: root (root@dSIP060entNightly-0.localdomain)  \n> Committer: root (root@dSIP060entNightly-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b59ada6025e875ea781d2b0f07cd2cab114543d1)\n[//]: # (START_SECTION 9948823743a42effcd85c3ae147daa1d9d15d31d)\n### TLS Support - Added support that will enable TLS on initial install - Added support to install the proper Kamailio modules for Debian - Added logic to generate a self-signed certitifcate during install\n\n> Commit: [9948823743a42effcd85c3ae147daa1d9d15d31d](https://github.com/dOpensource/dsiprouter/commit/9948823743a42effcd85c3ae147daa1d9d15d31d)  \n> Date: Wed, 25 Mar 2020 00:18:39 +0000  \n> Author: root (root@dSIP060entNightly-0.localdomain)  \n> Committer: root (root@dSIP060entNightly-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9948823743a42effcd85c3ae147daa1d9d15d31d)\n[//]: # (START_SECTION 4bcdc842c6a4645265230b921ced574dbe15588f)\n### Update mysqldump Commands to Handle More Use Cases\n\n> Commit: [4bcdc842c6a4645265230b921ced574dbe15588f](https://github.com/dOpensource/dsiprouter/commit/4bcdc842c6a4645265230b921ced574dbe15588f)  \n> Date: Mon, 23 Mar 2020 14:04:50 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update mysql dump and commands to remove definer and MyISAM\n- update mysql dump and restore commands in api to be more secure\n- resolves #11\n\n\n---\n\n[//]: # (END_SECTION 4bcdc842c6a4645265230b921ced574dbe15588f)\n[//]: # (START_SECTION cfc1cf6ebf8f8bf9f534b2d640af4d4564213467)\n### Increase Kamailio Max Loop Count\n\n> Commit: [cfc1cf6ebf8f8bf9f534b2d640af4d4564213467](https://github.com/dOpensource/dsiprouter/commit/cfc1cf6ebf8f8bf9f534b2d640af4d4564213467)  \n> Date: Mon, 23 Mar 2020 10:25:46 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #8\n\n\n---\n\n[//]: # (END_SECTION cfc1cf6ebf8f8bf9f534b2d640af4d4564213467)\n[//]: # (START_SECTION 5a493fc61cb370b5c2c0c2487aa18f8ca81ef067)\n### Fix inbound mapping import issues\n\n> Commit: [5a493fc61cb370b5c2c0c2487aa18f8ca81ef067](https://github.com/dOpensource/dsiprouter/commit/5a493fc61cb370b5c2c0c2487aa18f8ca81ef067)  \n> Date: Fri, 20 Mar 2020 14:10:00 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 5a493fc61cb370b5c2c0c2487aa18f8ca81ef067)\n[//]: # (START_SECTION 14fa0a028a6568cc9c660893502917fb960f93d7)\n### Stability Fixes and Cluster Sync/Install Updates\n\n> Commit: [14fa0a028a6568cc9c660893502917fb960f93d7](https://github.com/dOpensource/dsiprouter/commit/14fa0a028a6568cc9c660893502917fb960f93d7)  \n> Date: Fri, 20 Mar 2020 11:53:44 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update pbx invite timeout on 180,181,183 response\n- update cdr email to send only last month of cdr's\n- disable mysql service on remote db configuration\n- fix redirection for isinstance checks\n- fix mysql service linking\n- update kamctlrc to include DB settings\n- fix issues installing with remote db\n- add setkamdbconfig command to allow changing db configs\n- add current work on clusterinstall command\n\n\n---\n\n[//]: # (END_SECTION 14fa0a028a6568cc9c660893502917fb960f93d7)\n[//]: # (START_SECTION cd8dbc26b3cd8def1aa00e8391bbaf18c01530b1)\n### Fix errors in dsiprouter script\n\n> Commit: [cd8dbc26b3cd8def1aa00e8391bbaf18c01530b1](https://github.com/dOpensource/dsiprouter/commit/cd8dbc26b3cd8def1aa00e8391bbaf18c01530b1)  \n> Date: Fri, 13 Mar 2020 13:19:55 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix missing semi-colon\n- re-order OS info function\n\n\n---\n\n[//]: # (END_SECTION cd8dbc26b3cd8def1aa00e8391bbaf18c01530b1)\n[//]: # (START_SECTION 3aa8587aae55ce50f5389923521388a467d2a7ea)\n### CDR / Backup Feature Fixes\n\n> Commit: [3aa8587aae55ce50f5389923521388a467d2a7ea](https://github.com/dOpensource/dsiprouter/commit/3aa8587aae55ce50f5389923521388a467d2a7ea)  \n> Date: Fri, 13 Mar 2020 11:51:22 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- change sorting to be done on backend for CDR's\n- fix shell quoting issue w/ backup/restore cmds\n- handle bad file upload on restore cmd\n- auto format api_routes code\n\n\n---\n\n[//]: # (END_SECTION 3aa8587aae55ce50f5389923521388a467d2a7ea)\n[//]: # (START_SECTION 91d038e332e1b38799eb7b5e594d4fb7e93675f7)\n### Merge v0.55+ent changes into v0.60+ent\n\n> Commit: [91d038e332e1b38799eb7b5e594d4fb7e93675f7](https://github.com/dOpensource/dsiprouter/commit/91d038e332e1b38799eb7b5e594d4fb7e93675f7)  \n> Date: Thu, 12 Mar 2020 16:24:26 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- merge in cdr and backup/restore changes\n- various styling / syntax fixes\n- make bootstrap-datepicker.css local\n- update docker compose file for v0.60\n- fix some remaining db session bugs missed b4\n- fix dsiprouter syslog not logging pid issue\n\n\n---\n\n[//]: # (END_SECTION 91d038e332e1b38799eb7b5e594d4fb7e93675f7)\n[//]: # (START_SECTION 62dc6be4fc038b2526ccfdd2e9a31e424652571e)\n### Merge dmq-feature branch into v0.60+ent\n\n> Commit: [62dc6be4fc038b2526ccfdd2e9a31e424652571e](https://github.com/dOpensource/dsiprouter/commit/62dc6be4fc038b2526ccfdd2e9a31e424652571e)  \n> Date: Thu, 12 Mar 2020 10:37:41 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 62dc6be4fc038b2526ccfdd2e9a31e424652571e)\n[//]: # (START_SECTION 817a8062a283827aa22d90a8d5aaa201310effac)\n### Fix Testing Makefile\n\n> Commit: [817a8062a283827aa22d90a8d5aaa201310effac](https://github.com/dOpensource/dsiprouter/commit/817a8062a283827aa22d90a8d5aaa201310effac)  \n> Date: Tue, 25 Feb 2020 15:33:22 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Makefile requires tabs, re-tabbed spaces\n\n\n---\n\n[//]: # (END_SECTION 817a8062a283827aa22d90a8d5aaa201310effac)\n[//]: # (START_SECTION c2027be6c0363d6a0011bb6df9948ba734021921)\n### Fix RTPEngine Kernel Header Install Issue\n\n> Commit: [c2027be6c0363d6a0011bb6df9948ba734021921](https://github.com/dOpensource/dsiprouter/commit/c2027be6c0363d6a0011bb6df9948ba734021921)  \n> Date: Tue, 25 Feb 2020 14:54:46 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- find current kernels headers instead of installing new kernel\n\n\n---\n\n[//]: # (END_SECTION c2027be6c0363d6a0011bb6df9948ba734021921)\n[//]: # (START_SECTION fcd9ef8c7d66d8b2af645be760aa0f99a3c3cbac)\n### V0.60 Bug Fixes\n\n> Commit: [fcd9ef8c7d66d8b2af645be760aa0f99a3c3cbac](https://github.com/dOpensource/dsiprouter/commit/fcd9ef8c7d66d8b2af645be760aa0f99a3c3cbac)  \n> Date: Mon, 24 Feb 2020 10:55:29 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- dynamic kam db url updating\n- AES implementation update / fix\n- allow dsip dummy session to handle scoped session calls\n- update api auth to use Accept header instead of User-Agent\n- fix cdr start time field\n- fix inbound mapping failover fwd description\n- allow same endpoint on failover for inbound mappings\n- fix mail user / pass ENV variables\n- fix git pre/post commit hook\n- fix parsing of special chars in kam config db url update\n\n\n---\n\n[//]: # (END_SECTION fcd9ef8c7d66d8b2af645be760aa0f99a3c3cbac)\n[//]: # (START_SECTION fe4e90763318d06f134b4093e7fec2f9dfd74dcf)\n### Update settings.py\n\n> Commit: [fe4e90763318d06f134b4093e7fec2f9dfd74dcf](https://github.com/dOpensource/dsiprouter/commit/fe4e90763318d06f134b4093e7fec2f9dfd74dcf)  \n> Date: Mon, 4 Nov 2019 03:48:25 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fe4e90763318d06f134b4093e7fec2f9dfd74dcf)\n[//]: # (START_SECTION efa8d3fb5a9c3c28325df28cfe4244711020be28)\n### Update domains.rst\n\n> Commit: [efa8d3fb5a9c3c28325df28cfe4244711020be28](https://github.com/dOpensource/dsiprouter/commit/efa8d3fb5a9c3c28325df28cfe4244711020be28)  \n> Date: Mon, 4 Nov 2019 03:45:57 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION efa8d3fb5a9c3c28325df28cfe4244711020be28)\n[//]: # (START_SECTION a4875920e015c40c97023e103cdfa28b21282e37)\n### Update supported_configurations.rst\n\n> Commit: [a4875920e015c40c97023e103cdfa28b21282e37](https://github.com/dOpensource/dsiprouter/commit/a4875920e015c40c97023e103cdfa28b21282e37)  \n> Date: Mon, 4 Nov 2019 03:21:36 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a4875920e015c40c97023e103cdfa28b21282e37)\n[//]: # (START_SECTION 0a1b04837ef6f9793989fc65d48bad9c7df94bea)\n### Update supported_configurations.rst\n\n> Commit: [0a1b04837ef6f9793989fc65d48bad9c7df94bea](https://github.com/dOpensource/dsiprouter/commit/0a1b04837ef6f9793989fc65d48bad9c7df94bea)  \n> Date: Mon, 4 Nov 2019 03:19:17 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0a1b04837ef6f9793989fc65d48bad9c7df94bea)\n[//]: # (START_SECTION f55a391d94e4da8c5eaf3ffeeab3a2e27ad6adc9)\n### Update domains.rst\n\n> Commit: [f55a391d94e4da8c5eaf3ffeeab3a2e27ad6adc9](https://github.com/dOpensource/dsiprouter/commit/f55a391d94e4da8c5eaf3ffeeab3a2e27ad6adc9)  \n> Date: Mon, 4 Nov 2019 01:54:57 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f55a391d94e4da8c5eaf3ffeeab3a2e27ad6adc9)\n[//]: # (START_SECTION fe9218330241deaf90871bbdef5a671634bce81e)\n### Update supported_configurations.rst\n\n> Commit: [fe9218330241deaf90871bbdef5a671634bce81e](https://github.com/dOpensource/dsiprouter/commit/fe9218330241deaf90871bbdef5a671634bce81e)  \n> Date: Mon, 4 Nov 2019 01:37:43 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fe9218330241deaf90871bbdef5a671634bce81e)\n[//]: # (START_SECTION 2c2a83c592a934a35346e5dd9467fa8c4820dc60)\n### Update supported_configurations.rst\n\n> Commit: [2c2a83c592a934a35346e5dd9467fa8c4820dc60](https://github.com/dOpensource/dsiprouter/commit/2c2a83c592a934a35346e5dd9467fa8c4820dc60)  \n> Date: Mon, 4 Nov 2019 01:20:21 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2c2a83c592a934a35346e5dd9467fa8c4820dc60)\n[//]: # (START_SECTION df92443f24256ca322a777b2e39623a4373fb681)\n### Update supported_configurations.rst\n\n> Commit: [df92443f24256ca322a777b2e39623a4373fb681](https://github.com/dOpensource/dsiprouter/commit/df92443f24256ca322a777b2e39623a4373fb681)  \n> Date: Sun, 3 Nov 2019 17:55:27 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION df92443f24256ca322a777b2e39623a4373fb681)\n[//]: # (START_SECTION c4cd5ad10f765a8afc4f33e31fbf919c765507ec)\n### Update supported_configurations.rst\n\n> Commit: [c4cd5ad10f765a8afc4f33e31fbf919c765507ec](https://github.com/dOpensource/dsiprouter/commit/c4cd5ad10f765a8afc4f33e31fbf919c765507ec)  \n> Date: Sun, 3 Nov 2019 17:45:00 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c4cd5ad10f765a8afc4f33e31fbf919c765507ec)\n[//]: # (START_SECTION 5c507275ea83d7164daedc690b3f8c8879b21e83)\n### Update supported_configurations.rst\n\n> Commit: [5c507275ea83d7164daedc690b3f8c8879b21e83](https://github.com/dOpensource/dsiprouter/commit/5c507275ea83d7164daedc690b3f8c8879b21e83)  \n> Date: Sun, 3 Nov 2019 17:42:28 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5c507275ea83d7164daedc690b3f8c8879b21e83)\n[//]: # (START_SECTION 0f44d04cf565cc434ad0d76e7a48e41e882acd5a)\n### Update supported_configurations.rst\n\n> Commit: [0f44d04cf565cc434ad0d76e7a48e41e882acd5a](https://github.com/dOpensource/dsiprouter/commit/0f44d04cf565cc434ad0d76e7a48e41e882acd5a)  \n> Date: Sun, 3 Nov 2019 17:40:34 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0f44d04cf565cc434ad0d76e7a48e41e882acd5a)\n[//]: # (START_SECTION 50ffb4ad4a36804c57a6897a101a9d4a937104d4)\n### Update supported_configurations.rst\n\n> Commit: [50ffb4ad4a36804c57a6897a101a9d4a937104d4](https://github.com/dOpensource/dsiprouter/commit/50ffb4ad4a36804c57a6897a101a9d4a937104d4)  \n> Date: Sun, 3 Nov 2019 17:30:19 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 50ffb4ad4a36804c57a6897a101a9d4a937104d4)\n[//]: # (START_SECTION 91ed94f3e7ba639bcbbd97ee5319416bd64890d4)\n### Update supported_configurations.rst\n\n> Commit: [91ed94f3e7ba639bcbbd97ee5319416bd64890d4](https://github.com/dOpensource/dsiprouter/commit/91ed94f3e7ba639bcbbd97ee5319416bd64890d4)  \n> Date: Sun, 3 Nov 2019 17:28:58 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 91ed94f3e7ba639bcbbd97ee5319416bd64890d4)\n[//]: # (START_SECTION 750a611301607ec8613ffa1e07ce863362a27570)\n### Update supported_configurations.rst\n\n> Commit: [750a611301607ec8613ffa1e07ce863362a27570](https://github.com/dOpensource/dsiprouter/commit/750a611301607ec8613ffa1e07ce863362a27570)  \n> Date: Sun, 3 Nov 2019 17:27:36 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 750a611301607ec8613ffa1e07ce863362a27570)\n[//]: # (START_SECTION b183b1e717e37c7243b99ec18b8d697aa8b0a1fc)\n### Made Pass Thru authentication the default method\n\n> Commit: [b183b1e717e37c7243b99ec18b8d697aa8b0a1fc](https://github.com/dOpensource/dsiprouter/commit/b183b1e717e37c7243b99ec18b8d697aa8b0a1fc)  \n> Date: Sun, 3 Nov 2019 22:23:06 +0000  \n> Author: root (root@dSIPRouterMackv0523Hotfix-0.localdomain)  \n> Committer: root (root@dSIPRouterMackv0523Hotfix-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b183b1e717e37c7243b99ec18b8d697aa8b0a1fc)\n[//]: # (START_SECTION c13ff8da9d0e6d04c5f530a992501fd1ff3c3992)\n### Fixed #102 for both FreePBX with chan_sip and FusionPBX with Sofia.\n\n> Commit: [c13ff8da9d0e6d04c5f530a992501fd1ff3c3992](https://github.com/dOpensource/dsiprouter/commit/c13ff8da9d0e6d04c5f530a992501fd1ff3c3992)  \n> Date: Sun, 3 Nov 2019 21:26:59 +0000  \n> Author: root (root@dSIPMack0523hotfix-0.localdomain)  \n> Committer: root (root@dSIPMack0523hotfix-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c13ff8da9d0e6d04c5f530a992501fd1ff3c3992)\n[//]: # (START_SECTION a5eac062f964e5485f309248e295d8f0110b2dd7)\n### Fixed logic that checks if dSIPRouter is running on a cloud provider.  The Google Cloud Check was not evaluating correctly and resulted in setting an empty string password for the admin user.\n\n> Commit: [a5eac062f964e5485f309248e295d8f0110b2dd7](https://github.com/dOpensource/dsiprouter/commit/a5eac062f964e5485f309248e295d8f0110b2dd7)  \n> Date: Sat, 2 Nov 2019 21:52:56 +1100  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a5eac062f964e5485f309248e295d8f0110b2dd7)\n[//]: # (START_SECTION 224322a9885f4b5b452d96bfc68a226ca0a02bd3)\n### FusionPBX Extension to Extension Dialing - This fix should resovle issues with users being able to do Extension to Extension calls on FusionPBX\n\n> Commit: [224322a9885f4b5b452d96bfc68a226ca0a02bd3](https://github.com/dOpensource/dsiprouter/commit/224322a9885f4b5b452d96bfc68a226ca0a02bd3)  \n> Date: Mon, 14 Oct 2019 04:04:18 +0000  \n> Author: root (root@dSIP0523SSLEnable-0.localdomain)  \n> Committer: root (root@dSIP0523SSLEnable-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 224322a9885f4b5b452d96bfc68a226ca0a02bd3)\n[//]: # (START_SECTION a0824bac4caeee87b7f21e22d362f29c9dc8843a)\n### Fixed issue with ServerNAT being enabled by default\n\n> Commit: [a0824bac4caeee87b7f21e22d362f29c9dc8843a](https://github.com/dOpensource/dsiprouter/commit/a0824bac4caeee87b7f21e22d362f29c9dc8843a)  \n> Date: Sun, 13 Oct 2019 07:15:48 +0000  \n> Author: root (root@dSIP0523SSLEnable-0.localdomain)  \n> Committer: root (root@dSIP0523SSLEnable-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a0824bac4caeee87b7f21e22d362f29c9dc8843a)\n[//]: # (START_SECTION 6fcc6255f4eb63135e4c714d65638c524f8278bc)\n### Location information is now stored under extension@<ip address of dsip>\n\n> Commit: [6fcc6255f4eb63135e4c714d65638c524f8278bc](https://github.com/dOpensource/dsiprouter/commit/6fcc6255f4eb63135e4c714d65638c524f8278bc)  \n> Date: Sun, 13 Oct 2019 02:43:02 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6fcc6255f4eb63135e4c714d65638c524f8278bc)\n[//]: # (START_SECTION dda982bfbd1c8a2f3879197c08b04775eb95be82)\n### Fix pre-commit hook\n\n> Commit: [dda982bfbd1c8a2f3879197c08b04775eb95be82](https://github.com/dOpensource/dsiprouter/commit/dda982bfbd1c8a2f3879197c08b04775eb95be82)  \n> Date: Tue, 8 Oct 2019 06:20:48 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix bug where git stash apply would result in conflicts\n\n\n---\n\n[//]: # (END_SECTION dda982bfbd1c8a2f3879197c08b04775eb95be82)\n[//]: # (START_SECTION 3db8d9052c1a7e486657de0b16945b9e2f6cec2b)\n### Increase Efficiency of CHANGELOG creation\n\n> Commit: [3db8d9052c1a7e486657de0b16945b9e2f6cec2b](https://github.com/dOpensource/dsiprouter/commit/3db8d9052c1a7e486657de0b16945b9e2f6cec2b)  \n> Date: Tue, 8 Oct 2019 03:26:36 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- updates to post-commit hook made 25% execution time decrease\n\n\n---\n\n[//]: # (END_SECTION 3db8d9052c1a7e486657de0b16945b9e2f6cec2b)\n[//]: # (START_SECTION 2d57dbb72c691d6b333499718de50c954ff8acca)\n### Add dsiprouter to list of tracked services for consul\n\n> Commit: [2d57dbb72c691d6b333499718de50c954ff8acca](https://github.com/dOpensource/dsiprouter/commit/2d57dbb72c691d6b333499718de50c954ff8acca)  \n> Date: Sat, 5 Oct 2019 19:07:46 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 2d57dbb72c691d6b333499718de50c954ff8acca)\n[//]: # (START_SECTION b1e0868ef0edb473718fdfc8494ea0e3fe54722c)\n### DB Session Management Updates and Feature Additions\n\n> Commit: [b1e0868ef0edb473718fdfc8494ea0e3fe54722c](https://github.com/dOpensource/dsiprouter/commit/b1e0868ef0edb473718fdfc8494ea0e3fe54722c)  \n> Date: Fri, 4 Oct 2019 20:37:33 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- overhaul DB session management architecture to support multithreading\n- update app to close all db sessions and connections when stopping\n- update galera, group replication, and kamcluster scripts to fix some small install bugs\n- update cluster install scripts to support firewalld\n- add support for consul cluster installation and configuration\n- update exception and endpoint debug functions to have more sane defaults\n- move filehandling.py to util module\n- fix syslogging bugs in dsiprouter\n- update dsiprouter syslog format to be more useful\n- fix various bugs in api_routes code\n- fix various bugs related to incorrect session handling\n- add utility functions ti shared_lib for HA scripts\n- fix regression with kamailio config updates on install\n- add DummySession class to fix exception handling logic flow\n- fix enterprise_enabled global to really be global\n\n\n---\n\n[//]: # (END_SECTION b1e0868ef0edb473718fdfc8494ea0e3fe54722c)\n[//]: # (START_SECTION 41529c00be9ed9586eec0e9407c83cff258c0c8e)\n### Merge Commit [c88b214251333b7350b10b27fa2dae683ef3b602](https://github.com/dOpensource/dsiprouter/commit/c88b214251333b7350b10b27fa2dae683ef3b602) From OSS Repo\n\n> Commit: [41529c00be9ed9586eec0e9407c83cff258c0c8e](https://github.com/dOpensource/dsiprouter/commit/41529c00be9ed9586eec0e9407c83cff258c0c8e)  \n> Date: Wed, 2 Oct 2019 16:59:11 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 41529c00be9ed9586eec0e9407c83cff258c0c8e)\n[//]: # (START_SECTION c88b214251333b7350b10b27fa2dae683ef3b602)\n### Hotfix for 401/407 reply not sent on Pass Through Auth\n\n> Commit: [c88b214251333b7350b10b27fa2dae683ef3b602](https://github.com/dOpensource/dsiprouter/commit/c88b214251333b7350b10b27fa2dae683ef3b602)  \n> Date: Wed, 2 Oct 2019 16:53:56 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION c88b214251333b7350b10b27fa2dae683ef3b602)\n[//]: # (START_SECTION c7bb17fc14cbbc27ac46b7ee34dd8287301a76db)\n### Update RTPEngine Default Config\n\n> Commit: [c7bb17fc14cbbc27ac46b7ee34dd8287301a76db](https://github.com/dOpensource/dsiprouter/commit/c7bb17fc14cbbc27ac46b7ee34dd8287301a76db)  \n> Date: Tue, 1 Oct 2019 17:58:13 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- change default config to use explicit ip definition to avoid confusion\n\n\n---\n\n[//]: # (END_SECTION c7bb17fc14cbbc27ac46b7ee34dd8287301a76db)\n[//]: # (START_SECTION 5defb56428352d12eb1a9ff27755c37cf9c7cac2)\n### Update Enterprise and OSS Features\n\n> Commit: [5defb56428352d12eb1a9ff27755c37cf9c7cac2](https://github.com/dOpensource/dsiprouter/commit/5defb56428352d12eb1a9ff27755c37cf9c7cac2)  \n> Date: Tue, 1 Oct 2019 16:40:26 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- merge HA branch into v0.60+ent\n- improve portability and reliability of HA install scripts\n- fix iptables save bug in HA install\n- fix merging functions in HA install\n- add new standardized git workflow for contributors and option to install in repo\n- add / update git hooks: pre-commit, post-commit, prepare-commit-msg, commit-msg\n- add / upfate git configs: .gitignore, .gitattributes, .gitconfig\n- add merge conflict drivers for git hook generated files, such as CHANGELOG\n- add full support for storing settings in DB with security\n- add asymmetric, symmetric, and hashing functions\n- add setcredentials command for convenience\n- add support for settings updates through domain sockets\n- add support for settings updates through prcoess signaling (reload)\n- add functions to support ipc with dsiprouter process\n- add support for hot reload of updated settings\n- add option to enable lcr on install\n- update in progress work on AA Group Replication install\n- update asterisk prefix conversions to be reusable and moved to conversions.py\n- update all credentials and settings to be stored securely\n- update support for settings updates through env variables to only debug mode\n- update READEME to reflect enterprise version and display enterprise features\n- update a couple shared.py functions to be more robust\n- update kam config update functions to support all the dynamic settings\n- remove deprecated lcr module (it is already part of core dsiprouter)\n\n\n---\n\n[//]: # (END_SECTION 5defb56428352d12eb1a9ff27755c37cf9c7cac2)\n[//]: # (START_SECTION a92d2e3aa03be1a7bc634f5ba71e085a8a48fe89)\n### Slight Tweak to dr_gateways trigger\n\n> Commit: [a92d2e3aa03be1a7bc634f5ba71e085a8a48fe89](https://github.com/dOpensource/dsiprouter/commit/a92d2e3aa03be1a7bc634f5ba71e085a8a48fe89)  \n> Date: Tue, 1 Oct 2019 14:06:23 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update trigger to support multi-row inserts\n\n\n---\n\n[//]: # (END_SECTION a92d2e3aa03be1a7bc634f5ba71e085a8a48fe89)\n[//]: # (START_SECTION 3988878bb233c3354cb4f3b3aef7d30c73afc465)\n### Merge branch v0.523+ent into v0.60+ent\n\n> Commit: [3988878bb233c3354cb4f3b3aef7d30c73afc465](https://github.com/dOpensource/dsiprouter/commit/3988878bb233c3354cb4f3b3aef7d30c73afc465)  \n> Date: Mon, 30 Sep 2019 20:51:38 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 3988878bb233c3354cb4f3b3aef7d30c73afc465)\n[//]: # (START_SECTION 586cfb543c7062b0df0d0c5bec468af01cf7fa53)\n### Bug Fixes and Forwarding Feature Update\n\n> Commit: [586cfb543c7062b0df0d0c5bec468af01cf7fa53](https://github.com/dOpensource/dsiprouter/commit/586cfb543c7062b0df0d0c5bec468af01cf7fa53)  \n> Date: Thu, 26 Sep 2019 23:37:28 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update dsip_lib.sh kam config funcs to use a better separator\n- update dr_gateways db mapping class to fit new updates\n- update sql files to match utf-8 encoding from previous commit\n- update dsip_forwarding.sql to use DSIP_ID set at install\n- update kam to track src and dest gwgroup for call limiting\n- update kam SEND_NOTIFICATION route to support dynamic gwid and gwgroupid\n- create trigger for dr_gateways to support feature updates\n- fix rtpengine install detection (typo)\n- fix and improve various sql queries in resistrar\n\n\n---\n\n[//]: # (END_SECTION 586cfb543c7062b0df0d0c5bec468af01cf7fa53)\n[//]: # (START_SECTION 643c07c7d544ad83df7bb855cd90aab8651adbb1)\n### Update to DID Forwarding\n\n> Commit: [643c07c7d544ad83df7bb855cd90aab8651adbb1](https://github.com/dOpensource/dsiprouter/commit/643c07c7d544ad83df7bb855cd90aab8651adbb1)  \n> Date: Mon, 23 Sep 2019 19:25:54 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add feature to allow DID routing without specifying gwgroup\n- update inbound mapping route to accomodate new feature\n- update gwgroup selection to allow default option\n- update toggleElemDisabled() to be more robust\n- change hardfwd toggle button to set gwgroup selection to default option\n- move inbound mapping js logic from main to its own file\n- fix bug where failover forwarding would overwrite fwd info\n\n\n---\n\n[//]: # (END_SECTION 643c07c7d544ad83df7bb855cd90aab8651adbb1)\n[//]: # (START_SECTION 17f31180eeac77b4377c5a854bd40ad613e01210)\n### Add Security Features and Database Settings Update\n\n> Commit: [17f31180eeac77b4377c5a854bd40ad613e01210](https://github.com/dOpensource/dsiprouter/commit/17f31180eeac77b4377c5a854bd40ad613e01210)  \n> Date: Fri, 20 Sep 2019 16:01:15 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add automatic db/file settings update (on startup) based on mode\n- add database dump for v0.60+ent\n- add security functions for hashing (pbkdf2_hmac) and encrpyting (AES256) credentials\n- add secure credential usage to db connection functions\n- add setcredentials command to allow user to set credentials securely using script\n- add root priviledge check to dsiprouter script\n- add securing permissions on kam cfg to dsiprouter script\n- add private AES key generation to dsiprouter script\n- add option to configure kam db hosts from install command\n- add option to configure dsip id from install command\n- update getConfigAttrib() and setConfigAttrib() to support byte string literals\n- update kam cfg update functions to use a better delimiter when parsing\n- update install script to secure credentials by default\n- update updateConfig() function to support byte string literals\n- update MakeFile to run dsiprouter in debug mode and allow credential hot swapping\n- update testing scripts to use credential hot swapping\n- update install script to be more efficient by isolating a few functions\n- update displayLoginIfno() to be more reliable and show kam credentials\n- update dsip_forwarding.sql to support multiple dsiprouter instances\n- change secure credential env loading only to debug mode\n- change dsip/api/mail credentials auth to use encrypted credentials\n- change dsip credentials auth to compare hashes\n- change default db characterset to utf-8 (should always be used moving forward)\n- change default admin password generation to use /dev/urandom\n- fix kam cfg DBLUSTER settings update logic\n- fix kam cfg DBURL settings update logic\n- fix mail default sender bug\n- fix byte string storage bug by adding encoding/decoding to security functions\n- fix table detection bug when running installSQL() in a few modules\n- fix schema load order for dsip_forwarding.sql\n- fix kam cfg symlink not created issue\n- fix is rtpengine installed check\n\n\n---\n\n[//]: # (END_SECTION 17f31180eeac77b4377c5a854bd40ad613e01210)\n[//]: # (START_SECTION 4dee94abce5cf59e8a14f54a57dd64ee480aab01)\n### Forwarding fixes and Misc Updates\n\n> Commit: [4dee94abce5cf59e8a14f54a57dd64ee480aab01](https://github.com/dOpensource/dsiprouter/commit/4dee94abce5cf59e8a14f54a57dd64ee480aab01)  \n> Date: Fri, 13 Sep 2019 11:35:41 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix forwarding dr_groupid update\n- allow fwding rule create/delete on update\n- fix outbound route dr_groupid filtering\n- update kamreload cmd with reload for new htables\n- update harfwd and failfwd dr_rule matching on dr_groupid\n- clean up kam config\n- reset defaults to prod values\n\n\n---\n\n[//]: # (END_SECTION 4dee94abce5cf59e8a14f54a57dd64ee480aab01)\n[//]: # (START_SECTION 9298bd4d47c9210c8ed8c353d14fc4d06e9b9744)\n### Fixes - Fixed the Endpoint Group Modal - Fixed issues with the Call Limit not working with User/Pass Registration - Fixed issues with the API\n\n> Commit: [9298bd4d47c9210c8ed8c353d14fc4d06e9b9744](https://github.com/dOpensource/dsiprouter/commit/9298bd4d47c9210c8ed8c353d14fc4d06e9b9744)  \n> Date: Fri, 13 Sep 2019 02:17:08 +0000  \n> Author: root (root@dev-siprouter01.ynyybpir3miebggok1eqcyxpaf.gx.internal.cloudapp.net)  \n> Committer: root (root@dev-siprouter01.ynyybpir3miebggok1eqcyxpaf.gx.internal.cloudapp.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9298bd4d47c9210c8ed8c353d14fc4d06e9b9744)\n[//]: # (START_SECTION ea4bc9f5aa3517aea230ab662dde65592ec5a0a2)\n### v0523 Fixes\n\n> Commit: [ea4bc9f5aa3517aea230ab662dde65592ec5a0a2](https://github.com/dOpensource/dsiprouter/commit/ea4bc9f5aa3517aea230ab662dde65592ec5a0a2)  \n> Date: Wed, 11 Sep 2019 20:03:40 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fixed invalid GCE instance detection\n- fix hardfwd and failfwd always insert issue\n- fix typo in inbound mapping route\n- fix inbound mapping modal update for fwd's\n- fix hardfwd and failfwd matching logic\n- fix inbound mapping edit modal fwd buttons toggle update\n- added prefix mapping htable and db view\n- added dsip_settings db table with a subset of the settings\n- refactor username/password to follow naming convention\n- added new features to install process\n- added sql dump of v0.523+ent to testing sql files\n\n\n---\n\n[//]: # (END_SECTION ea4bc9f5aa3517aea230ab662dde65592ec5a0a2)\n[//]: # (START_SECTION 50b50a73de61b9c6d6482b1baa15c6b8b8483baf)\n### v0523 Fixes\n\n> Commit: [50b50a73de61b9c6d6482b1baa15c6b8b8483baf](https://github.com/dOpensource/dsiprouter/commit/50b50a73de61b9c6d6482b1baa15c6b8b8483baf)  \n> Date: Wed, 11 Sep 2019 20:03:40 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fixed invalid GCE instance detection\n- fix hardfwd and failfwd always insert issue\n- fix typo in inbound mapping route\n- fix inbound mapping modal update for fwd's\n- fix hardfwd and failfwd matching logic\n- fix inbound mapping edit modal fwd buttons toggle update\n- added prefix mapping htable and db view\n- added dsip_settings db table with a subset of the settings\n- refactor username/password to follow naming convention\n- added new features to install process\n- added sql dump of v0.523+ent to testing sql files\n\n\n---\n\n[//]: # (END_SECTION 50b50a73de61b9c6d6482b1baa15c6b8b8483baf)\n[//]: # (START_SECTION d2c8da09ff1faa9d405d24c9798c41711cecf996)\n### Added support for dynamically setting up DR gateways and gateway list when an endpont registers\n\n> Commit: [d2c8da09ff1faa9d405d24c9798c41711cecf996](https://github.com/dOpensource/dsiprouter/commit/d2c8da09ff1faa9d405d24c9798c41711cecf996)  \n> Date: Wed, 11 Sep 2019 13:31:06 +0000  \n> Author: root (root@dev-siprouter01.ynyybpir3miebggok1eqcyxpaf.gx.internal.cloudapp.net)  \n> Committer: root (root@dev-siprouter01.ynyybpir3miebggok1eqcyxpaf.gx.internal.cloudapp.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d2c8da09ff1faa9d405d24c9798c41711cecf996)\n[//]: # (START_SECTION 3f6b43155a0af4c5dacf7e493eae2c49caeac858)\n### Fixed a regression in the Carrier Groups Add Carrier Modal.\n\n> Commit: [3f6b43155a0af4c5dacf7e493eae2c49caeac858](https://github.com/dOpensource/dsiprouter/commit/3f6b43155a0af4c5dacf7e493eae2c49caeac858)  \n> Date: Thu, 5 Sep 2019 07:23:48 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3f6b43155a0af4c5dacf7e493eae2c49caeac858)\n[//]: # (START_SECTION d9f60de3ad0efb4241b060fb174227ec8700f90f)\n### Refactored the Endpoint Groups\n\n> Commit: [d9f60de3ad0efb4241b060fb174227ec8700f90f](https://github.com/dOpensource/dsiprouter/commit/d9f60de3ad0efb4241b060fb174227ec8700f90f)  \n> Date: Fri, 30 Aug 2019 11:49:12 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d9f60de3ad0efb4241b060fb174227ec8700f90f)\n[//]: # (START_SECTION df0dcf4dcdd6bcf3f573c3d6dcc9eae9db6b9bb5)\n### The DSIP_API_TOKEN value will be admin before install\n\n> Commit: [df0dcf4dcdd6bcf3f573c3d6dcc9eae9db6b9bb5](https://github.com/dOpensource/dsiprouter/commit/df0dcf4dcdd6bcf3f573c3d6dcc9eae9db6b9bb5)  \n> Date: Fri, 30 Aug 2019 07:46:37 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION df0dcf4dcdd6bcf3f573c3d6dcc9eae9db6b9bb5)\n[//]: # (START_SECTION 4eb68a4fc61ea3b88fa0c3361393e240b12a1e71)\n### Fixed issues that resulted from the merge\n\n> Commit: [4eb68a4fc61ea3b88fa0c3361393e240b12a1e71](https://github.com/dOpensource/dsiprouter/commit/4eb68a4fc61ea3b88fa0c3361393e240b12a1e71)  \n> Date: Fri, 30 Aug 2019 07:42:15 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4eb68a4fc61ea3b88fa0c3361393e240b12a1e71)\n[//]: # (START_SECTION 74a3b0670179ce95695a9969fd1fb549de988f0f)\n### Fixed merge issue\n\n> Commit: [74a3b0670179ce95695a9969fd1fb549de988f0f](https://github.com/dOpensource/dsiprouter/commit/74a3b0670179ce95695a9969fd1fb549de988f0f)  \n> Date: Fri, 30 Aug 2019 03:30:54 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 74a3b0670179ce95695a9969fd1fb549de988f0f)\n[//]: # (START_SECTION 9e74d60fb0df2a1b90fd16109e14c1309a2743b7)\n### Enhancements - Added CDR's to the GUI and via a RESTFul endpoint - Added the ability to update an endpont group record\n\n> Commit: [9e74d60fb0df2a1b90fd16109e14c1309a2743b7](https://github.com/dOpensource/dsiprouter/commit/9e74d60fb0df2a1b90fd16109e14c1309a2743b7)  \n> Date: Fri, 30 Aug 2019 03:15:15 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9e74d60fb0df2a1b90fd16109e14c1309a2743b7)\n[//]: # (START_SECTION 2ce8f4bea06100605980b80b424e62e988ae1088)\n###   - update call limit to use gwgroup   - fix hardfwd and failfwd routing logic   - update notification feature to use bearer token   - fix looping bug with failover fwd   - move enpoint groups js to fix conflict   - add insert,update,delete triggers for gw2gwroup table   - update dsip fwding to match on prefix instead of gwgroup   - make gui templates more standardized (description field)   - update inbound mapping to use gwgroups   - add hardfwd and failfwd to inbound mapping   - change templates to show hostname support in drouting   - NOTE: inbound mapping updated but still needs work   - update kam reload in api to match gui   - fix misc issues in api_routes   - add new icons for forwarding   - clear add modals when opening again\n\n> Commit: [2ce8f4bea06100605980b80b424e62e988ae1088](https://github.com/dOpensource/dsiprouter/commit/2ce8f4bea06100605980b80b424e62e988ae1088)  \n> Date: Thu, 29 Aug 2019 01:49:27 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 2ce8f4bea06100605980b80b424e62e988ae1088)\n[//]: # (START_SECTION a72dd21df0278f8c70539083ae28dfa1b1fb93e2)\n### EndpointGroup Bug Fixes and CDR API - Fixed issues with saving and deleting EndpointGroups - Implemented a CDR RestFul API for requesting Call Detail Record (CDR) Information\n\n> Commit: [a72dd21df0278f8c70539083ae28dfa1b1fb93e2](https://github.com/dOpensource/dsiprouter/commit/a72dd21df0278f8c70539083ae28dfa1b1fb93e2)  \n> Date: Wed, 28 Aug 2019 16:45:07 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a72dd21df0278f8c70539083ae28dfa1b1fb93e2)\n[//]: # (START_SECTION 9ce9add4478a6fcb0f4c328ef1424c02fcf082ad)\n### Fixed API Token Security function\n\n> Commit: [9ce9add4478a6fcb0f4c328ef1424c02fcf082ad](https://github.com/dOpensource/dsiprouter/commit/9ce9add4478a6fcb0f4c328ef1424c02fcf082ad)  \n> Date: Mon, 26 Aug 2019 13:42:34 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9ce9add4478a6fcb0f4c328ef1424c02fcf082ad)\n[//]: # (START_SECTION 6d3f5e2e1b6fc5ac57c411d4adb91691b9e96ef6)\n### Merging in Notificaition changes\n\n> Commit: [6d3f5e2e1b6fc5ac57c411d4adb91691b9e96ef6](https://github.com/dOpensource/dsiprouter/commit/6d3f5e2e1b6fc5ac57c411d4adb91691b9e96ef6)  \n> Date: Mon, 26 Aug 2019 08:50:04 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6d3f5e2e1b6fc5ac57c411d4adb91691b9e96ef6)\n[//]: # (START_SECTION a60d94f60fd7a9fa869ef1aa4101338e2e982938)\n### EndpointGroups - Added API's for updating and deleting EndpointGroups - Added the supporting UI components\n\n> Commit: [a60d94f60fd7a9fa869ef1aa4101338e2e982938](https://github.com/dOpensource/dsiprouter/commit/a60d94f60fd7a9fa869ef1aa4101338e2e982938)  \n> Date: Mon, 26 Aug 2019 08:34:21 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a60d94f60fd7a9fa869ef1aa4101338e2e982938)\n[//]: # (START_SECTION 41adfd261f7f4544d0a982a5c09cddf455afa97b)\n### Add Enterprise Features Backend Support\n\n> Commit: [41adfd261f7f4544d0a982a5c09cddf455afa97b](https://github.com/dOpensource/dsiprouter/commit/41adfd261f7f4544d0a982a5c09cddf455afa97b)  \n> Date: Sun, 25 Aug 2019 18:45:59 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- make notifications a separate callable route\n- add support for endpoint failure notifications\n- update dsip_hardfwd table\n- update dsip_failfwd table\n- improve email notification messages\n- add improvements to flag handling in kamailio routes\n- add support for hard forwarding to DID\n- add support for failover forwarding to DID\n- update inbound mapping lcr flag handling\n- update some defaults for TESTING in settings.py (remove for production)\n- fix missing servernat option for configurekam command in dsiprouter.sh\n- update command options in dsiprouter.sh\n- fix #!ifdef mismatch in kamailio.cfg\n\n\n---\n\n[//]: # (END_SECTION 41adfd261f7f4544d0a982a5c09cddf455afa97b)\n[//]: # (START_SECTION 9a8c7cb2648822d9247d083f80a352f337ff70e0)\n### Notification API Feature Backend Support\n\n> Commit: [9a8c7cb2648822d9247d083f80a352f337ff70e0](https://github.com/dOpensource/dsiprouter/commit/9a8c7cb2648822d9247d083f80a352f337ff70e0)  \n> Date: Sat, 24 Aug 2019 21:44:47 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add kamailio route for notifications / http requests\n- add gw2gwroup table\n- add tables for hard forwarding and failover forwarding\n- add email notification support\n- add file upload support\n- add async func wrapper\n- add endpoint for notifications to API\n- add email settings in settings.py\n- move API security functions to API routes\n- add http_async_client module to install scripts\n\n\n---\n\n[//]: # (END_SECTION 9a8c7cb2648822d9247d083f80a352f337ff70e0)\n[//]: # (START_SECTION d8c506a48ab502f34b44bd173e6e9b836764d6b3)\n### Added support for adding endpoints within an Endpoint Group\n\n> Commit: [d8c506a48ab502f34b44bd173e6e9b836764d6b3](https://github.com/dOpensource/dsiprouter/commit/d8c506a48ab502f34b44bd173e6e9b836764d6b3)  \n> Date: Fri, 23 Aug 2019 11:00:09 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d8c506a48ab502f34b44bd173e6e9b836764d6b3)\n[//]: # (START_SECTION bc4884df2a3ad22b2ed32eb044b66ab298ad4ffc)\n### Added database table for gateway to gateway group lookup - gwip2gwgroup\n\n> Commit: [bc4884df2a3ad22b2ed32eb044b66ab298ad4ffc](https://github.com/dOpensource/dsiprouter/commit/bc4884df2a3ad22b2ed32eb044b66ab298ad4ffc)  \n> Date: Wed, 21 Aug 2019 15:28:24 +0000  \n> Author: root (root@dsip0523entMack.localdomain)  \n> Committer: root (root@dsip0523entMack.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bc4884df2a3ad22b2ed32eb044b66ab298ad4ffc)\n[//]: # (START_SECTION 1e1f55b0670ea5b1d03a4d9ac56610bf2845bf4c)\n### Fixed issue with merge - forgot to fix conflict\n\n> Commit: [1e1f55b0670ea5b1d03a4d9ac56610bf2845bf4c](https://github.com/dOpensource/dsiprouter/commit/1e1f55b0670ea5b1d03a4d9ac56610bf2845bf4c)  \n> Date: Wed, 21 Aug 2019 07:58:18 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1e1f55b0670ea5b1d03a4d9ac56610bf2845bf4c)\n[//]: # (START_SECTION ed8786a06b8623938d82c9a1c38982b36df11139)\n### Fixed issue with merge - forgot to fix conflict\n\n> Commit: [ed8786a06b8623938d82c9a1c38982b36df11139](https://github.com/dOpensource/dsiprouter/commit/ed8786a06b8623938d82c9a1c38982b36df11139)  \n> Date: Wed, 21 Aug 2019 07:55:41 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ed8786a06b8623938d82c9a1c38982b36df11139)\n[//]: # (START_SECTION 096c9254dc09665a7f9645281306c25fe9ba257b)\n### Fixed issues with the Endpoint API and dSIPNotification SQL\n\n> Commit: [096c9254dc09665a7f9645281306c25fe9ba257b](https://github.com/dOpensource/dsiprouter/commit/096c9254dc09665a7f9645281306c25fe9ba257b)  \n> Date: Wed, 21 Aug 2019 07:45:30 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 096c9254dc09665a7f9645281306c25fe9ba257b)\n[//]: # (START_SECTION d4747c96ab72b8f2766b5797080d9917363a753f)\n### Fixed the mail settins\n\n> Commit: [d4747c96ab72b8f2766b5797080d9917363a753f](https://github.com/dOpensource/dsiprouter/commit/d4747c96ab72b8f2766b5797080d9917363a753f)  \n> Date: Wed, 21 Aug 2019 02:56:42 +0000  \n> Author: root (root@dsip0523entMerge.localdomain)  \n> Committer: root (root@dsip0523entMerge.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d4747c96ab72b8f2766b5797080d9917363a753f)\n[//]: # (START_SECTION 97f71a42a0a32e1c93904327ac8c195feabde1ec)\n### Fixed the version number\n\n> Commit: [97f71a42a0a32e1c93904327ac8c195feabde1ec](https://github.com/dOpensource/dsiprouter/commit/97f71a42a0a32e1c93904327ac8c195feabde1ec)  \n> Date: Wed, 21 Aug 2019 02:47:05 +0000  \n> Author: root (root@dsip0523entMerge.localdomain)  \n> Committer: root (root@dsip0523entMerge.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 97f71a42a0a32e1c93904327ac8c195feabde1ec)\n[//]: # (START_SECTION f2eded53fd2b0e9e03af1deb658f0d1315c90f99)\n### Fixed minor issues: - dsip_calllimit table was not being installed at install time - The email settings for the notifiation service was not in the settings.py file\n\n> Commit: [f2eded53fd2b0e9e03af1deb658f0d1315c90f99](https://github.com/dOpensource/dsiprouter/commit/f2eded53fd2b0e9e03af1deb658f0d1315c90f99)  \n> Date: Wed, 21 Aug 2019 02:45:09 +0000  \n> Author: root (root@dsip0523entMerge.localdomain)  \n> Committer: root (root@dsip0523entMerge.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f2eded53fd2b0e9e03af1deb658f0d1315c90f99)\n[//]: # (START_SECTION eee1276a68eb23b3df81a8f8f0ec0f5ba293ee2e)\n### Docker Compose: - Added the dsip_notification schema to the SQL file that is used for priming the database of the MySQL container\n\n> Commit: [eee1276a68eb23b3df81a8f8f0ec0f5ba293ee2e](https://github.com/dOpensource/dsiprouter/commit/eee1276a68eb23b3df81a8f8f0ec0f5ba293ee2e)  \n> Date: Wed, 21 Aug 2019 02:07:39 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eee1276a68eb23b3df81a8f8f0ec0f5ba293ee2e)\n[//]: # (START_SECTION 807b1a2ef9913086bd2eb27aee47b17d872be14b)\n### Docker Compose: - Added the dsip_notification schema to the SQL file that is used for priming the database of the MySQL container\n\n> Commit: [807b1a2ef9913086bd2eb27aee47b17d872be14b](https://github.com/dOpensource/dsiprouter/commit/807b1a2ef9913086bd2eb27aee47b17d872be14b)  \n> Date: Tue, 20 Aug 2019 21:50:20 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 807b1a2ef9913086bd2eb27aee47b17d872be14b)\n[//]: # (START_SECTION 2f960dfd6dda36d6154656d1ee439abfa44db357)\n### Added support for adding endpoints\n\n> Commit: [2f960dfd6dda36d6154656d1ee439abfa44db357](https://github.com/dOpensource/dsiprouter/commit/2f960dfd6dda36d6154656d1ee439abfa44db357)  \n> Date: Tue, 20 Aug 2019 08:10:38 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2f960dfd6dda36d6154656d1ee439abfa44db357)\n[//]: # (START_SECTION 05983fa408408f5f0d5ec0541db5c61c28a55b8c)\n### Added logic to store an endpoint group\n\n> Commit: [05983fa408408f5f0d5ec0541db5c61c28a55b8c](https://github.com/dOpensource/dsiprouter/commit/05983fa408408f5f0d5ec0541db5c61c28a55b8c)  \n> Date: Mon, 19 Aug 2019 07:01:53 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 05983fa408408f5f0d5ec0541db5c61c28a55b8c)\n[//]: # (START_SECTION 6ed938930ea697c7ed89332b610fd4b057da66d4)\n### Create RESTFul API to add an endpoint group\n\n> Commit: [6ed938930ea697c7ed89332b610fd4b057da66d4](https://github.com/dOpensource/dsiprouter/commit/6ed938930ea697c7ed89332b610fd4b057da66d4)  \n> Date: Fri, 16 Aug 2019 14:47:11 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6ed938930ea697c7ed89332b610fd4b057da66d4)\n[//]: # (START_SECTION 74f3517bc57aade400a68b128da983acf4b0c11a)\n### Fixed issues with the docker compose changes\n\n> Commit: [74f3517bc57aade400a68b128da983acf4b0c11a](https://github.com/dOpensource/dsiprouter/commit/74f3517bc57aade400a68b128da983acf4b0c11a)  \n> Date: Fri, 16 Aug 2019 14:39:05 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 74f3517bc57aade400a68b128da983acf4b0c11a)\n[//]: # (START_SECTION 6a0ca8f77b62006bbd58a5d6ff0897e3c24ee99c)\n### Added logic to create the Call Limit Schema\n\n> Commit: [6a0ca8f77b62006bbd58a5d6ff0897e3c24ee99c](https://github.com/dOpensource/dsiprouter/commit/6a0ca8f77b62006bbd58a5d6ff0897e3c24ee99c)  \n> Date: Tue, 13 Aug 2019 02:45:45 +0000  \n> Author: root (root@dsip0523entMack-0.localdomain)  \n> Committer: root (root@dsip0523entMack-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6a0ca8f77b62006bbd58a5d6ff0897e3c24ee99c)\n[//]: # (START_SECTION 9e487891682a7f7b00211139e64e069e146e8736)\n### Updated to handle different version of Python being already installed on the system\n\n> Commit: [9e487891682a7f7b00211139e64e069e146e8736](https://github.com/dOpensource/dsiprouter/commit/9e487891682a7f7b00211139e64e069e146e8736)  \n> Date: Mon, 12 Aug 2019 12:49:25 +0000  \n> Author: root (root@ip-172-31-23-165.us-east-2.compute.internal)  \n> Committer: root (root@ip-172-31-23-165.us-east-2.compute.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9e487891682a7f7b00211139e64e069e146e8736)\n[//]: # (START_SECTION de5c873f83ac38efb8b9ba0f5b9a0c6109d3178b)\n### FusionPBX Provisioning Services: - The docker container now starts up with a self signed cert\n\n> Commit: [de5c873f83ac38efb8b9ba0f5b9a0c6109d3178b](https://github.com/dOpensource/dsiprouter/commit/de5c873f83ac38efb8b9ba0f5b9a0c6109d3178b)  \n> Date: Wed, 7 Aug 2019 12:00:45 +0000  \n> Author: root (root@dsip0523-qa-0.localdomain)  \n> Committer: root (root@dsip0523-qa-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION de5c873f83ac38efb8b9ba0f5b9a0c6109d3178b)\n[//]: # (START_SECTION 6939b1cdbfb0c98d611fefbaa935462dddeabea7)\n### Fixed an self-signed cert configurtion. The Country portion was set to USA, versus US\n\n> Commit: [6939b1cdbfb0c98d611fefbaa935462dddeabea7](https://github.com/dOpensource/dsiprouter/commit/6939b1cdbfb0c98d611fefbaa935462dddeabea7)  \n> Date: Wed, 7 Aug 2019 05:21:11 +0000  \n> Author: Mack Hendricks (mack@goflyball.com)  \n> Committer: Mack Hendricks (mack@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6939b1cdbfb0c98d611fefbaa935462dddeabea7)\n[//]: # (START_SECTION ab513ab3a8c9e9e74e0aee9e404b33c6cbad8cd9)\n### Carrier Groups: - Fixed an error that occured when creating a carrier that used Username/Password auth\n\n> Commit: [ab513ab3a8c9e9e74e0aee9e404b33c6cbad8cd9](https://github.com/dOpensource/dsiprouter/commit/ab513ab3a8c9e9e74e0aee9e404b33c6cbad8cd9)  \n> Date: Wed, 7 Aug 2019 04:47:40 +0000  \n> Author: root (root@dsip0523qa-0.localdomain)  \n> Committer: root (root@dsip0523qa-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ab513ab3a8c9e9e74e0aee9e404b33c6cbad8cd9)\n[//]: # (START_SECTION 3c6a47ce1599c071b84814e64cc22bfe70f4e74a)\n### Domains - Local Subscriber: - Fixed authentication logic.  It will now properly authenticate against the subscriber table\n\n> Commit: [3c6a47ce1599c071b84814e64cc22bfe70f4e74a](https://github.com/dOpensource/dsiprouter/commit/3c6a47ce1599c071b84814e64cc22bfe70f4e74a)  \n> Date: Mon, 5 Aug 2019 22:09:48 +0000  \n> Author: root (root@dsip0523qa-0.localdomain)  \n> Committer: root (root@dsip0523qa-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3c6a47ce1599c071b84814e64cc22bfe70f4e74a)\n[//]: # (START_SECTION a5498c61858525d9781dfbe7c3dea27194ef0b04)\n### Update settings.py\n\n> Commit: [a5498c61858525d9781dfbe7c3dea27194ef0b04](https://github.com/dOpensource/dsiprouter/commit/a5498c61858525d9781dfbe7c3dea27194ef0b04)  \n> Date: Mon, 5 Aug 2019 06:36:16 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a5498c61858525d9781dfbe7c3dea27194ef0b04)\n[//]: # (START_SECTION 8531cbfd4662a25a66a14e73c4f914adeb71145c)\n### Update settings.py\n\n> Commit: [8531cbfd4662a25a66a14e73c4f914adeb71145c](https://github.com/dOpensource/dsiprouter/commit/8531cbfd4662a25a66a14e73c4f914adeb71145c)  \n> Date: Mon, 5 Aug 2019 06:33:53 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8531cbfd4662a25a66a14e73c4f914adeb71145c)\n[//]: # (START_SECTION 3111739f6130a87ac9c379617732a3ae040b590f)\n### Update settings.py\n\n> Commit: [3111739f6130a87ac9c379617732a3ae040b590f](https://github.com/dOpensource/dsiprouter/commit/3111739f6130a87ac9c379617732a3ae040b590f)  \n> Date: Mon, 5 Aug 2019 06:33:26 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3111739f6130a87ac9c379617732a3ae040b590f)\n[//]: # (START_SECTION 0da71dd8bf4cd3919d035f1039baedbd228f684e)\n### Update index.rst\n\n> Commit: [0da71dd8bf4cd3919d035f1039baedbd228f684e](https://github.com/dOpensource/dsiprouter/commit/0da71dd8bf4cd3919d035f1039baedbd228f684e)  \n> Date: Mon, 5 Aug 2019 05:44:29 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0da71dd8bf4cd3919d035f1039baedbd228f684e)\n[//]: # (START_SECTION 56a77e02cec99cd19a00e54e9eddea62040af0cf)\n### Domain Mapping: - Added the domain_list_hash field to support syncing with FusioinPBX\n\n> Commit: [56a77e02cec99cd19a00e54e9eddea62040af0cf](https://github.com/dOpensource/dsiprouter/commit/56a77e02cec99cd19a00e54e9eddea62040af0cf)  \n> Date: Mon, 5 Aug 2019 02:34:49 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 56a77e02cec99cd19a00e54e9eddea62040af0cf)\n[//]: # (START_SECTION b47ec8a86d55548b785676fd1ba431386b4e4dae)\n### Update upgrade_0.522_to_0.523.rst\n\n> Commit: [b47ec8a86d55548b785676fd1ba431386b4e4dae](https://github.com/dOpensource/dsiprouter/commit/b47ec8a86d55548b785676fd1ba431386b4e4dae)  \n> Date: Sun, 4 Aug 2019 22:33:23 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b47ec8a86d55548b785676fd1ba431386b4e4dae)\n[//]: # (START_SECTION fcd85f373341fd8421801364e362acd30ef2ebe4)\n### Update upgrade_0.522_to_0.523.rst\n\n> Commit: [fcd85f373341fd8421801364e362acd30ef2ebe4](https://github.com/dOpensource/dsiprouter/commit/fcd85f373341fd8421801364e362acd30ef2ebe4)  \n> Date: Sun, 4 Aug 2019 22:29:26 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fcd85f373341fd8421801364e362acd30ef2ebe4)\n[//]: # (START_SECTION a0dd65902d3535e222b9a259744401f54df2c95e)\n### Update upgrade_0.522_to_0.523.rst\n\n> Commit: [a0dd65902d3535e222b9a259744401f54df2c95e](https://github.com/dOpensource/dsiprouter/commit/a0dd65902d3535e222b9a259744401f54df2c95e)  \n> Date: Sun, 4 Aug 2019 22:27:32 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a0dd65902d3535e222b9a259744401f54df2c95e)\n[//]: # (START_SECTION 550c842e6a2e45e1eb3493c65aeec3fc3977bb0b)\n### Update upgrade_0.522_to_0.523.rst\n\n> Commit: [550c842e6a2e45e1eb3493c65aeec3fc3977bb0b](https://github.com/dOpensource/dsiprouter/commit/550c842e6a2e45e1eb3493c65aeec3fc3977bb0b)  \n> Date: Sun, 4 Aug 2019 22:25:54 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 550c842e6a2e45e1eb3493c65aeec3fc3977bb0b)\n[//]: # (START_SECTION 6585ed0b8a888855de07660cbd45e5ddbffde34e)\n### Rename upgrade_0522_to_0523.rst to upgrade_0.522_to_0.523.rst\n\n> Commit: [6585ed0b8a888855de07660cbd45e5ddbffde34e](https://github.com/dOpensource/dsiprouter/commit/6585ed0b8a888855de07660cbd45e5ddbffde34e)  \n> Date: Sun, 4 Aug 2019 22:23:35 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6585ed0b8a888855de07660cbd45e5ddbffde34e)\n[//]: # (START_SECTION a2355994144a5319aaba781c68ccec9cc1341b61)\n### Update upgrade.rst\n\n> Commit: [a2355994144a5319aaba781c68ccec9cc1341b61](https://github.com/dOpensource/dsiprouter/commit/a2355994144a5319aaba781c68ccec9cc1341b61)  \n> Date: Sun, 4 Aug 2019 22:18:18 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a2355994144a5319aaba781c68ccec9cc1341b61)\n[//]: # (START_SECTION 35bd7a2206f730e73e49b74b623d115e5be330bc)\n### Update upgrade.rst\n\n> Commit: [35bd7a2206f730e73e49b74b623d115e5be330bc](https://github.com/dOpensource/dsiprouter/commit/35bd7a2206f730e73e49b74b623d115e5be330bc)  \n> Date: Sun, 4 Aug 2019 22:17:42 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 35bd7a2206f730e73e49b74b623d115e5be330bc)\n[//]: # (START_SECTION fa4ae601d9472a0d7fe9e0d45529ec6f56c28147)\n### Update upgrade.rst\n\n> Commit: [fa4ae601d9472a0d7fe9e0d45529ec6f56c28147](https://github.com/dOpensource/dsiprouter/commit/fa4ae601d9472a0d7fe9e0d45529ec6f56c28147)  \n> Date: Sun, 4 Aug 2019 22:17:25 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fa4ae601d9472a0d7fe9e0d45529ec6f56c28147)\n[//]: # (START_SECTION 40a3da0adf309c1f7831f10abcd512275771a9ca)\n### Update upgrade.rst\n\n> Commit: [40a3da0adf309c1f7831f10abcd512275771a9ca](https://github.com/dOpensource/dsiprouter/commit/40a3da0adf309c1f7831f10abcd512275771a9ca)  \n> Date: Sun, 4 Aug 2019 22:14:53 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 40a3da0adf309c1f7831f10abcd512275771a9ca)\n[//]: # (START_SECTION 0469c1961ed0b19ba46027db7d537446a9ba2fca)\n### Update upgrade.rst\n\n> Commit: [0469c1961ed0b19ba46027db7d537446a9ba2fca](https://github.com/dOpensource/dsiprouter/commit/0469c1961ed0b19ba46027db7d537446a9ba2fca)  \n> Date: Sun, 4 Aug 2019 22:13:08 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0469c1961ed0b19ba46027db7d537446a9ba2fca)\n[//]: # (START_SECTION 8d6defe515f156e06444884592426c62871ac95f)\n### Update upgrade.rst\n\n> Commit: [8d6defe515f156e06444884592426c62871ac95f](https://github.com/dOpensource/dsiprouter/commit/8d6defe515f156e06444884592426c62871ac95f)  \n> Date: Sun, 4 Aug 2019 22:08:59 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8d6defe515f156e06444884592426c62871ac95f)\n[//]: # (START_SECTION e0d1cb3accc65ccf3f0279e576eabb2ab4712e84)\n### Create upgrade.rst\n\n> Commit: [e0d1cb3accc65ccf3f0279e576eabb2ab4712e84](https://github.com/dOpensource/dsiprouter/commit/e0d1cb3accc65ccf3f0279e576eabb2ab4712e84)  \n> Date: Sun, 4 Aug 2019 22:07:02 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e0d1cb3accc65ccf3f0279e576eabb2ab4712e84)\n[//]: # (START_SECTION 76f93a93638bbb8fb8bc0e5c0a714b0f01e480bd)\n### Create upgrade_0522_to_0523.rst\n\n> Commit: [76f93a93638bbb8fb8bc0e5c0a714b0f01e480bd](https://github.com/dOpensource/dsiprouter/commit/76f93a93638bbb8fb8bc0e5c0a714b0f01e480bd)  \n> Date: Sun, 4 Aug 2019 22:06:16 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 76f93a93638bbb8fb8bc0e5c0a714b0f01e480bd)\n[//]: # (START_SECTION c1007d77f7c4539a299657fbefc14b5a87d527a1)\n### Rename upgrade.rst to upgrade_0.50_to_0.51.rst\n\n> Commit: [c1007d77f7c4539a299657fbefc14b5a87d527a1](https://github.com/dOpensource/dsiprouter/commit/c1007d77f7c4539a299657fbefc14b5a87d527a1)  \n> Date: Sun, 4 Aug 2019 22:04:44 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c1007d77f7c4539a299657fbefc14b5a87d527a1)\n[//]: # (START_SECTION b0d6fac4b564797f4f356f9dc81372df9c61e92f)\n### Changed the table of contents\n\n> Commit: [b0d6fac4b564797f4f356f9dc81372df9c61e92f](https://github.com/dOpensource/dsiprouter/commit/b0d6fac4b564797f4f356f9dc81372df9c61e92f)  \n> Date: Sun, 4 Aug 2019 21:57:21 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Moved the API section from the bottom.\n\n\n---\n\n[//]: # (END_SECTION b0d6fac4b564797f4f356f9dc81372df9c61e92f)\n[//]: # (START_SECTION 4d74524706b5b3ee9765518a616079174e10f017)\n### FusionPBX Domain Routing Sync: - Added logic that will generate a hash of domain names during the sync.  The sync will only run if the hash changes - Added logic to create a self-signed certificate for nginx.  This will allow the service to start up using SSL  Fixes #193\n\n> Commit: [4d74524706b5b3ee9765518a616079174e10f017](https://github.com/dOpensource/dsiprouter/commit/4d74524706b5b3ee9765518a616079174e10f017)  \n> Date: Mon, 5 Aug 2019 01:47:29 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4d74524706b5b3ee9765518a616079174e10f017)\n[//]: # (START_SECTION edd4cbca59a1c748928ab7ee81b8caeccc628e8d)\n### FusionPBX Domain Routing Sync: - Added logic that will generate a hash of domain names during the sync.  The sync will only run if the hash changes - Added logic to create a self-signed certificate for nginx.  This will allow the service to start up using SSL\n\n> Commit: [edd4cbca59a1c748928ab7ee81b8caeccc628e8d](https://github.com/dOpensource/dsiprouter/commit/edd4cbca59a1c748928ab7ee81b8caeccc628e8d)  \n> Date: Mon, 5 Aug 2019 01:42:28 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION edd4cbca59a1c748928ab7ee81b8caeccc628e8d)\n[//]: # (START_SECTION 7e905c1c721018cd7a0336d2b630ed273fe9b6b3)\n### Domain Support for Local Subscriber Table - Added logic to reload the dispatcher table - Added logic to probe each server defined within a Domain\n\n> Commit: [7e905c1c721018cd7a0336d2b630ed273fe9b6b3](https://github.com/dOpensource/dsiprouter/commit/7e905c1c721018cd7a0336d2b630ed273fe9b6b3)  \n> Date: Sat, 3 Aug 2019 00:30:10 +0000  \n> Author: root (root@demo-dsiprouter-0.localdomain)  \n> Committer: root (root@demo-dsiprouter-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7e905c1c721018cd7a0336d2b630ed273fe9b6b3)\n[//]: # (START_SECTION 91b08445116f6b25b51ee1fcd1d27d262e0a5309)\n### PBX INVITE TIMER - Changed the logic so that INVITE messages from PBX's that receive a SIP 100 message will be assigned a different INVITE timer timeout - Fixes #195\n\n> Commit: [91b08445116f6b25b51ee1fcd1d27d262e0a5309](https://github.com/dOpensource/dsiprouter/commit/91b08445116f6b25b51ee1fcd1d27d262e0a5309)  \n> Date: Thu, 1 Aug 2019 11:14:01 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 91b08445116f6b25b51ee1fcd1d27d262e0a5309)\n[//]: # (START_SECTION 7c99508394adb92cb499e62312db871e21a09fc6)\n### PBX INVITE TIMER -  Increased the INVITE TIMER by two once we see a SIP 100 Message. This will give the endpoint more time to respond to the invite and it will trigger the secondary server if it doesn't answer - Fixes #195\n\n> Commit: [7c99508394adb92cb499e62312db871e21a09fc6](https://github.com/dOpensource/dsiprouter/commit/7c99508394adb92cb499e62312db871e21a09fc6)  \n> Date: Wed, 31 Jul 2019 06:35:11 +0000  \n> Author: root (root@dsip0523-0.localdomain)  \n> Committer: root (root@dsip0523-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7c99508394adb92cb499e62312db871e21a09fc6)\n[//]: # (START_SECTION bde414d405a8051901fddf90415cb91f82a444da)\n### Redesign of PBX page: - The term PBX is switched to Endpoint to represent a more generic use - Added tabs to the Add modal for each configuration area\n\n> Commit: [bde414d405a8051901fddf90415cb91f82a444da](https://github.com/dOpensource/dsiprouter/commit/bde414d405a8051901fddf90415cb91f82a444da)  \n> Date: Tue, 30 Jul 2019 19:17:07 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bde414d405a8051901fddf90415cb91f82a444da)\n[//]: # (START_SECTION 73fb41db8345debc967eb50dcf1f1c461a9ac499)\n### Docker support  - Added dockerfiles for dSIPRouter and MySQL  - Added a docker-compose configuration to allow the dSIPRouter GUI to spin up with a docker-compose up  - Added environment variables to allow the dSIP usernamae, password and kamailio database settings can be set on runtime\n\n> Commit: [73fb41db8345debc967eb50dcf1f1c461a9ac499](https://github.com/dOpensource/dsiprouter/commit/73fb41db8345debc967eb50dcf1f1c461a9ac499)  \n> Date: Mon, 29 Jul 2019 14:29:50 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 73fb41db8345debc967eb50dcf1f1c461a9ac499)\n[//]: # (START_SECTION ecc3f8ca6dc41d000495851de0631b1cc64ce344)\n### Refactoring the PBX/Endpoint page\n\n> Commit: [ecc3f8ca6dc41d000495851de0631b1cc64ce344](https://github.com/dOpensource/dsiprouter/commit/ecc3f8ca6dc41d000495851de0631b1cc64ce344)  \n> Date: Fri, 26 Jul 2019 01:42:05 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ecc3f8ca6dc41d000495851de0631b1cc64ce344)\n[//]: # (START_SECTION 04094157b805103337eaca75891ce1121c63cb44)\n### Disabled verbose output for a test\n\n> Commit: [04094157b805103337eaca75891ce1121c63cb44](https://github.com/dOpensource/dsiprouter/commit/04094157b805103337eaca75891ce1121c63cb44)  \n> Date: Wed, 24 Jul 2019 13:30:30 +0000  \n> Author: Mack Hendricks (mack@goflyball.com)  \n> Committer: Mack Hendricks (mack@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 04094157b805103337eaca75891ce1121c63cb44)\n[//]: # (START_SECTION 05e699c9f4bc2a91241038e16b80b426320d005a)\n### Removed the ExecStart command that was reseting the dSIPRouter password to the instanceid of the instance.  Except for Amazon images\n\n> Commit: [05e699c9f4bc2a91241038e16b80b426320d005a](https://github.com/dOpensource/dsiprouter/commit/05e699c9f4bc2a91241038e16b80b426320d005a)  \n> Date: Wed, 24 Jul 2019 13:25:47 +0000  \n> Author: Mack Hendricks (mack@goflyball.com)  \n> Committer: Mack Hendricks (mack@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 05e699c9f4bc2a91241038e16b80b426320d005a)\n[//]: # (START_SECTION 43300c6e1639679564a12f7aafdb577edfcc20da)\n### Python repo issue - > The yum package manager couldn't install python36u-pip because of a conflict with the python36 packages > which are in the epel-release repo.  We now remove the python36 libraries and install Python from the ius repo\n\n> Commit: [43300c6e1639679564a12f7aafdb577edfcc20da](https://github.com/dOpensource/dsiprouter/commit/43300c6e1639679564a12f7aafdb577edfcc20da)  \n> Date: Tue, 23 Jul 2019 16:11:44 +0000  \n> Author: root (root@dsip-centOS7.6)  \n> Committer: root (root@dsip-centOS7.6)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 43300c6e1639679564a12f7aafdb577edfcc20da)\n[//]: # (START_SECTION 2490692a8f8b5913d529a81a70f5f05e11871085)\n### Added Call Limit Support to the Kamailio configuration and fixed the dSIPRouter logic to handle it\n\n> Commit: [2490692a8f8b5913d529a81a70f5f05e11871085](https://github.com/dOpensource/dsiprouter/commit/2490692a8f8b5913d529a81a70f5f05e11871085)  \n> Date: Mon, 22 Jul 2019 11:17:17 +0000  \n> Author: root (root@dsip0522ent-0.localdomain)  \n> Committer: root (root@dsip0522ent-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2490692a8f8b5913d529a81a70f5f05e11871085)\n[//]: # (START_SECTION 4d31a7f92e1ad98f3309708dc23194c9c001d3b3)\n### Added logic to manage call limits\n\n> Commit: [4d31a7f92e1ad98f3309708dc23194c9c001d3b3](https://github.com/dOpensource/dsiprouter/commit/4d31a7f92e1ad98f3309708dc23194c9c001d3b3)  \n> Date: Fri, 19 Jul 2019 14:19:19 +0000  \n> Author: root (root@dsip0522ent-0.localdomain)  \n> Committer: root (root@dsip0522ent-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4d31a7f92e1ad98f3309708dc23194c9c001d3b3)\n[//]: # (START_SECTION d270313ca80adfae3b66015a59a40eb65c3bf498)\n### First commit of enterprise\n\n> Commit: [d270313ca80adfae3b66015a59a40eb65c3bf498](https://github.com/dOpensource/dsiprouter/commit/d270313ca80adfae3b66015a59a40eb65c3bf498)  \n> Date: Thu, 18 Jul 2019 19:10:43 +0000  \n> Author: root (root@dsip0522ent-0.localdomain)  \n> Committer: root (root@dsip0522ent-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d270313ca80adfae3b66015a59a40eb65c3bf498)\n[//]: # (START_SECTION 5eebed34cd7055bece9fb0bd46c7689c1927dcf0)\n### Update api.rst\n\n> Commit: [5eebed34cd7055bece9fb0bd46c7689c1927dcf0](https://github.com/dOpensource/dsiprouter/commit/5eebed34cd7055bece9fb0bd46c7689c1927dcf0)  \n> Date: Wed, 3 Jul 2019 18:49:23 -0400  \n> Author: Omari S. King (46901954+OmariKing@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5eebed34cd7055bece9fb0bd46c7689c1927dcf0)\n[//]: # (START_SECTION f8d363e6e24e942988aaa48a6fcdbf092a65f936)\n### Update Docs\n\n> Commit: [f8d363e6e24e942988aaa48a6fcdbf092a65f936](https://github.com/dOpensource/dsiprouter/commit/f8d363e6e24e942988aaa48a6fcdbf092a65f936)  \n> Date: Wed, 3 Jul 2019 17:21:15 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix couple broken links\n- fix `api.rst`\n\n\n---\n\n[//]: # (END_SECTION f8d363e6e24e942988aaa48a6fcdbf092a65f936)\n[//]: # (START_SECTION 1a93a8d468627bddba6244aa27a210ded5fe232a)\n### Update API Docs\n\n> Commit: [1a93a8d468627bddba6244aa27a210ded5fe232a](https://github.com/dOpensource/dsiprouter/commit/1a93a8d468627bddba6244aa27a210ded5fe232a)  \n> Date: Wed, 3 Jul 2019 16:36:46 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 1a93a8d468627bddba6244aa27a210ded5fe232a)\n[//]: # (START_SECTION 2b647fd576e2c58129686b480b5b32f76f9c0a2d)\n### Update api.rst\n\n> Commit: [2b647fd576e2c58129686b480b5b32f76f9c0a2d](https://github.com/dOpensource/dsiprouter/commit/2b647fd576e2c58129686b480b5b32f76f9c0a2d)  \n> Date: Wed, 3 Jul 2019 16:10:17 -0400  \n> Author: Omari S. King (46901954+OmariKing@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2b647fd576e2c58129686b480b5b32f76f9c0a2d)\n[//]: # (START_SECTION dec8a4c87057cc31951b297e0796bfea37751e17)\n### Merge Documentation Fixes\n\n> Commit: [dec8a4c87057cc31951b297e0796bfea37751e17](https://github.com/dOpensource/dsiprouter/commit/dec8a4c87057cc31951b297e0796bfea37751e17)  \n> Date: Wed, 3 Jul 2019 15:30:09 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION dec8a4c87057cc31951b297e0796bfea37751e17)\n[//]: # (START_SECTION 7ec9412c035ffda8374d701956805021f8ba1bd0)\n### Update api.rst\n\n> Commit: [7ec9412c035ffda8374d701956805021f8ba1bd0](https://github.com/dOpensource/dsiprouter/commit/7ec9412c035ffda8374d701956805021f8ba1bd0)  \n> Date: Wed, 3 Jul 2019 15:08:56 -0400  \n> Author: Omari S. King (46901954+OmariKing@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7ec9412c035ffda8374d701956805021f8ba1bd0)\n[//]: # (START_SECTION 22d40857924ef8d7a4e0e61c9e527891500d3d7c)\n### Fix inconsistencies in documentation\n\n> Commit: [22d40857924ef8d7a4e0e61c9e527891500d3d7c](https://github.com/dOpensource/dsiprouter/commit/22d40857924ef8d7a4e0e61c9e527891500d3d7c)  \n> Date: Wed, 3 Jul 2019 14:54:12 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 22d40857924ef8d7a4e0e61c9e527891500d3d7c)\n[//]: # (START_SECTION 1dfaa1f43594c6139c2ffcfe76c33af85e596e5d)\n### Merge Documentation Changes\n\n> Commit: [1dfaa1f43594c6139c2ffcfe76c33af85e596e5d](https://github.com/dOpensource/dsiprouter/commit/1dfaa1f43594c6139c2ffcfe76c33af85e596e5d)  \n> Date: Wed, 3 Jul 2019 13:17:11 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Update api.rst\n\n\n---\n\n[//]: # (END_SECTION 1dfaa1f43594c6139c2ffcfe76c33af85e596e5d)\n[//]: # (START_SECTION 91cb29f477cb92b35ee77870445f225c798a1cde)\n### Merge documentation Updates\n\n> Commit: [91cb29f477cb92b35ee77870445f225c798a1cde](https://github.com/dOpensource/dsiprouter/commit/91cb29f477cb92b35ee77870445f225c798a1cde)  \n> Date: Wed, 3 Jul 2019 13:13:52 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Update domains.rst\n\n\n---\n\n[//]: # (END_SECTION 91cb29f477cb92b35ee77870445f225c798a1cde)\n[//]: # (START_SECTION fd2fd068e5b3d537ccc027d1c14330660cd4a030)\n### Update api.rst\n\n> Commit: [fd2fd068e5b3d537ccc027d1c14330660cd4a030](https://github.com/dOpensource/dsiprouter/commit/fd2fd068e5b3d537ccc027d1c14330660cd4a030)  \n> Date: Wed, 3 Jul 2019 11:40:51 -0400  \n> Author: Omari S. King (46901954+OmariKing@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fd2fd068e5b3d537ccc027d1c14330660cd4a030)\n[//]: # (START_SECTION b7d2c765ff46acd6c9df645f97b9b181715257d2)\n### Update api.rst\n\n> Commit: [b7d2c765ff46acd6c9df645f97b9b181715257d2](https://github.com/dOpensource/dsiprouter/commit/b7d2c765ff46acd6c9df645f97b9b181715257d2)  \n> Date: Wed, 3 Jul 2019 11:30:49 -0400  \n> Author: Omari S. King (46901954+OmariKing@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b7d2c765ff46acd6c9df645f97b9b181715257d2)\n[//]: # (START_SECTION 18e679561335e0fdf259c8c21b664724ebc03e84)\n### Update api.rst\n\n> Commit: [18e679561335e0fdf259c8c21b664724ebc03e84](https://github.com/dOpensource/dsiprouter/commit/18e679561335e0fdf259c8c21b664724ebc03e84)  \n> Date: Wed, 3 Jul 2019 11:29:43 -0400  \n> Author: Omari S. King (46901954+OmariKing@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 18e679561335e0fdf259c8c21b664724ebc03e84)\n[//]: # (START_SECTION d2b0eb4eafdef691e71691cf6c1da85ac07b9fd9)\n### Update api.rst\n\n> Commit: [d2b0eb4eafdef691e71691cf6c1da85ac07b9fd9](https://github.com/dOpensource/dsiprouter/commit/d2b0eb4eafdef691e71691cf6c1da85ac07b9fd9)  \n> Date: Wed, 3 Jul 2019 11:20:48 -0400  \n> Author: Omari S. King (46901954+OmariKing@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d2b0eb4eafdef691e71691cf6c1da85ac07b9fd9)\n[//]: # (START_SECTION f6137cc2fc34a4ae334c710c0c237c8f089d7e20)\n### Update api.rst\n\n> Commit: [f6137cc2fc34a4ae334c710c0c237c8f089d7e20](https://github.com/dOpensource/dsiprouter/commit/f6137cc2fc34a4ae334c710c0c237c8f089d7e20)  \n> Date: Wed, 3 Jul 2019 11:10:39 -0400  \n> Author: Omari S. King (46901954+OmariKing@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f6137cc2fc34a4ae334c710c0c237c8f089d7e20)\n[//]: # (START_SECTION aea0913b894ef74d3935df3ff3198ccb86dc89e6)\n### Update index.rst\n\n> Commit: [aea0913b894ef74d3935df3ff3198ccb86dc89e6](https://github.com/dOpensource/dsiprouter/commit/aea0913b894ef74d3935df3ff3198ccb86dc89e6)  \n> Date: Wed, 3 Jul 2019 11:01:13 -0400  \n> Author: Omari S. King (46901954+OmariKing@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION aea0913b894ef74d3935df3ff3198ccb86dc89e6)\n[//]: # (START_SECTION d3d4628ac44663d42115e68c3195cc57cf48d4c7)\n### Update and rename API.rst to api.rst\n\n> Commit: [d3d4628ac44663d42115e68c3195cc57cf48d4c7](https://github.com/dOpensource/dsiprouter/commit/d3d4628ac44663d42115e68c3195cc57cf48d4c7)  \n> Date: Wed, 3 Jul 2019 10:58:19 -0400  \n> Author: Omari S. King (46901954+OmariKing@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d3d4628ac44663d42115e68c3195cc57cf48d4c7)\n[//]: # (START_SECTION dcb2af1cdd32868cc3c900defddea926bb85be0f)\n### Update Inbound Mapping Endpoint\n\n> Commit: [dcb2af1cdd32868cc3c900defddea926bb85be0f](https://github.com/dOpensource/dsiprouter/commit/dcb2af1cdd32868cc3c900defddea926bb85be0f)  \n> Date: Wed, 3 Jul 2019 10:55:01 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- change endpoint path to /api/v1/inboundmapping\n\n\n---\n\n[//]: # (END_SECTION dcb2af1cdd32868cc3c900defddea926bb85be0f)\n[//]: # (START_SECTION 786ff9b649a0a9d6f836c4eda390165034573d44)\n### Update index.rst\n\n> Commit: [786ff9b649a0a9d6f836c4eda390165034573d44](https://github.com/dOpensource/dsiprouter/commit/786ff9b649a0a9d6f836c4eda390165034573d44)  \n> Date: Wed, 3 Jul 2019 10:39:17 -0400  \n> Author: Omari S. King (46901954+OmariKing@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 786ff9b649a0a9d6f836c4eda390165034573d44)\n[//]: # (START_SECTION 403543b8e778f048dbef3238e38ddca4707b38a2)\n### Update index.rst\n\n> Commit: [403543b8e778f048dbef3238e38ddca4707b38a2](https://github.com/dOpensource/dsiprouter/commit/403543b8e778f048dbef3238e38ddca4707b38a2)  \n> Date: Wed, 3 Jul 2019 10:38:37 -0400  \n> Author: Omari S. King (46901954+OmariKing@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 403543b8e778f048dbef3238e38ddca4707b38a2)\n[//]: # (START_SECTION 82f0cbe7e117718aaf97860090bc01edfd939a93)\n### Create API.rst\n\n> Commit: [82f0cbe7e117718aaf97860090bc01edfd939a93](https://github.com/dOpensource/dsiprouter/commit/82f0cbe7e117718aaf97860090bc01edfd939a93)  \n> Date: Wed, 3 Jul 2019 10:26:08 -0400  \n> Author: Omari S. King (46901954+OmariKing@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 82f0cbe7e117718aaf97860090bc01edfd939a93)\n[//]: # (START_SECTION cce099319717c5fa6335cedab8512035b0d2e6b1)\n### Make Primary PBX Required in GUI\n\n> Commit: [cce099319717c5fa6335cedab8512035b0d2e6b1](https://github.com/dOpensource/dsiprouter/commit/cce099319717c5fa6335cedab8512035b0d2e6b1)  \n> Date: Tue, 2 Jul 2019 21:50:58 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #181\n- make select field for primary pbx required in front end\n\n\n---\n\n[//]: # (END_SECTION cce099319717c5fa6335cedab8512035b0d2e6b1)\n[//]: # (START_SECTION f0b772396303c122ef9ebaad2b4c17d34f15f2b2)\n### Inbound DID Mapping Through API\n\n> Commit: [f0b772396303c122ef9ebaad2b4c17d34f15f2b2](https://github.com/dOpensource/dsiprouter/commit/f0b772396303c122ef9ebaad2b4c17d34f15f2b2)  \n> Date: Tue, 2 Jul 2019 21:02:21 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #77\n- create new API DID Mapping endpoint for GET,POST,PUT,DELETE\n- add tests to API test `19.sh`\n- fix creation of duplicate inbound DID's\n- update exception handling to be more useful\n- update globals\n- add TODO comments\n- update rowToDict function\n- make `error.html` prettier / more palatable\n\n\n---\n\n[//]: # (END_SECTION f0b772396303c122ef9ebaad2b4c17d34f15f2b2)\n[//]: # (START_SECTION 428b1d2c2324fda6afd6ad43f3d0ff697350ae9f)\n### Default to IPv4\n\n> Commit: [428b1d2c2324fda6afd6ad43f3d0ff697350ae9f](https://github.com/dOpensource/dsiprouter/commit/428b1d2c2324fda6afd6ad43f3d0ff697350ae9f)  \n> Date: Mon, 1 Jul 2019 12:32:07 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #183\n- set ip resolution to ipv4 by default\n- note: ipv6 support is not fully supported yet\n\n\n---\n\n[//]: # (END_SECTION 428b1d2c2324fda6afd6ad43f3d0ff697350ae9f)\n[//]: # (START_SECTION 47cc12a4f06fb26c611be63bf340624cc80e955b)\n### Fix Debian v09 mysqlclient Dependency Regression\n\n> Commit: [47cc12a4f06fb26c611be63bf340624cc80e955b](https://github.com/dOpensource/dsiprouter/commit/47cc12a4f06fb26c611be63bf340624cc80e955b)  \n> Date: Sun, 30 Jun 2019 19:53:54 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add case for new package name default-libmysqlclient-dev\n\n\n---\n\n[//]: # (END_SECTION 47cc12a4f06fb26c611be63bf340624cc80e955b)\n[//]: # (START_SECTION c59f76096c9ade54041cdfc8d4319683ddf1f133)\n### Fix Debian v09 mysqlclient Dependency Regression\n\n> Commit: [c59f76096c9ade54041cdfc8d4319683ddf1f133](https://github.com/dOpensource/dsiprouter/commit/c59f76096c9ade54041cdfc8d4319683ddf1f133)  \n> Date: Sun, 30 Jun 2019 19:53:54 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add case for new package name default-libmysqlclient-dev\n\n\n---\n\n[//]: # (END_SECTION c59f76096c9ade54041cdfc8d4319683ddf1f133)\n[//]: # (START_SECTION 5e4b7f22a106a7562cdfc6328ac6349b6eaa317f)\n### testing: Fixed Domain Pass-Thru using FreePBX test - The test will run the test on the externalip that it finds and then will try to run it on the internalip\n\n> Commit: [5e4b7f22a106a7562cdfc6328ac6349b6eaa317f](https://github.com/dOpensource/dsiprouter/commit/5e4b7f22a106a7562cdfc6328ac6349b6eaa317f)  \n> Date: Fri, 28 Jun 2019 16:06:18 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5e4b7f22a106a7562cdfc6328ac6349b6eaa317f)\n[//]: # (START_SECTION 77d625cf97b5d206a64809153a85ae6052c1797a)\n### testing: Fixed Domain Pass-Thru using FreePBX test - The test will run the test on the externalip that it finds and then will try to run it on the internalip\n\n> Commit: [77d625cf97b5d206a64809153a85ae6052c1797a](https://github.com/dOpensource/dsiprouter/commit/77d625cf97b5d206a64809153a85ae6052c1797a)  \n> Date: Fri, 28 Jun 2019 16:03:47 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 77d625cf97b5d206a64809153a85ae6052c1797a)\n[//]: # (START_SECTION d131a82d6ade3c8f05713f7445b9452826d25d72)\n### Merge v0.522 commits onto v0.523\n\n> Commit: [d131a82d6ade3c8f05713f7445b9452826d25d72](https://github.com/dOpensource/dsiprouter/commit/d131a82d6ade3c8f05713f7445b9452826d25d72)  \n> Date: Thu, 27 Jun 2019 14:15:09 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION d131a82d6ade3c8f05713f7445b9452826d25d72)\n[//]: # (START_SECTION 3971a4df0b2e68e7826c8850edfa74d0678c2120)\n### Remove Unused Billing Calls in Kamailio Config\n\n> Commit: [3971a4df0b2e68e7826c8850edfa74d0678c2120](https://github.com/dOpensource/dsiprouter/commit/3971a4df0b2e68e7826c8850edfa74d0678c2120)  \n> Date: Thu, 27 Jun 2019 12:35:12 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #182\n- Resolves #88\n- commented out call to kamailio_rating in kam cfg\n\n\n---\n\n[//]: # (END_SECTION 3971a4df0b2e68e7826c8850edfa74d0678c2120)\n[//]: # (START_SECTION 83e79d97322144cadd49e99c4d568455c00e3fd7)\n### Allow Excluding Libraries in Requirements git Hook\n\n> Commit: [83e79d97322144cadd49e99c4d568455c00e3fd7](https://github.com/dOpensource/dsiprouter/commit/83e79d97322144cadd49e99c4d568455c00e3fd7)  \n> Date: Thu, 27 Jun 2019 11:42:59 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update pre-commit hook to allow exluding libraries\n- add docker_py to excluded libraries\n\n\n---\n\n[//]: # (END_SECTION 83e79d97322144cadd49e99c4d568455c00e3fd7)\n[//]: # (START_SECTION 4b037caa02962d80745655b5e1ef6858ad04ea2b)\n### Kam Cluster Stability Improvements\n\n> Commit: [4b037caa02962d80745655b5e1ef6858ad04ea2b](https://github.com/dOpensource/dsiprouter/commit/4b037caa02962d80745655b5e1ef6858ad04ea2b)  \n> Date: Thu, 14 Mar 2019 10:48:56 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update resource wait times for pcs\n\n\n---\n\n[//]: # (END_SECTION 4b037caa02962d80745655b5e1ef6858ad04ea2b)\n[//]: # (START_SECTION 0bd4654bb6b58218dff7e1a437c38f23860c17f1)\n### Fix Cloud Stability Issues\n\n> Commit: [0bd4654bb6b58218dff7e1a437c38f23860c17f1](https://github.com/dOpensource/dsiprouter/commit/0bd4654bb6b58218dff7e1a437c38f23860c17f1)  \n> Date: Wed, 26 Jun 2019 17:37:31 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix sipsak install fail due to memory allocation errors\n- fix apt and yum locking errors on install\n- fix dependency mismatching for mysldb\n- upadate requirements hook to allow non stdlib deps\n- add initial support getting GCE, AZURE, and DO images\n- fix centos false positive build errors\n- cleanup dsiprouter install debug statements\n- fix motd banner for centos and debian < v9\n- fix debian user password reset on cloud images\n- added a couple TODO's for install scripts\n\n\n---\n\n[//]: # (END_SECTION 0bd4654bb6b58218dff7e1a437c38f23860c17f1)\n[//]: # (START_SECTION e0d64dcc93e0fad10bd2f47c62c4b2a39fdee7ad)\n### Update requirements.txt\n\n> Commit: [e0d64dcc93e0fad10bd2f47c62c4b2a39fdee7ad](https://github.com/dOpensource/dsiprouter/commit/e0d64dcc93e0fad10bd2f47c62c4b2a39fdee7ad)  \n> Date: Thu, 27 Jun 2019 07:57:40 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Putting back the required libraries\n\n\n---\n\n[//]: # (END_SECTION e0d64dcc93e0fad10bd2f47c62c4b2a39fdee7ad)\n[//]: # (START_SECTION eea03615c43deaad5bdf2989e4dbf4dc6209575c)\n### Security Bug Modification\n\n> Commit: [eea03615c43deaad5bdf2989e4dbf4dc6209575c](https://github.com/dOpensource/dsiprouter/commit/eea03615c43deaad5bdf2989e4dbf4dc6209575c)  \n> Date: Tue, 25 Jun 2019 18:00:12 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- ammending commit c8756c929ccce9793a1e1717439a257c97f22203\n- Resolves #185\n- add host check\n- allow cloud-init to re-add keys\n\n\n---\n\n[//]: # (END_SECTION eea03615c43deaad5bdf2989e4dbf4dc6209575c)\n[//]: # (START_SECTION f0adeca772448524d7e14aecece343a4a22fb36a)\n### kamailio.cfg: The SERVERNAT mode will now cause Kamailio to listen on TCP at 127.0.0.1:5060\n\n> Commit: [f0adeca772448524d7e14aecece343a4a22fb36a](https://github.com/dOpensource/dsiprouter/commit/f0adeca772448524d7e14aecece343a4a22fb36a)  \n> Date: Wed, 26 Jun 2019 18:09:28 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f0adeca772448524d7e14aecece343a4a22fb36a)\n[//]: # (START_SECTION a15d049d16a32896d73267da550d8f2826518526)\n### Update kamailio51_dsiprouter.cfg\n\n> Commit: [a15d049d16a32896d73267da550d8f2826518526](https://github.com/dOpensource/dsiprouter/commit/a15d049d16a32896d73267da550d8f2826518526)  \n> Date: Mon, 24 Jun 2019 13:02:29 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Changing the INVITE timer to a more standard timeout of 32 secs\n\n\n---\n\n[//]: # (END_SECTION a15d049d16a32896d73267da550d8f2826518526)\n[//]: # (START_SECTION 42dbd37b3d0ebdf11991af51142ce4938d4bce99)\n### Update kamailio51_dsiprouter.cfg\n\n> Commit: [42dbd37b3d0ebdf11991af51142ce4938d4bce99](https://github.com/dOpensource/dsiprouter/commit/42dbd37b3d0ebdf11991af51142ce4938d4bce99)  \n> Date: Mon, 24 Jun 2019 13:02:29 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Changing the INVITE timer to a more standard timeout of 32 secs\n\n\n---\n\n[//]: # (END_SECTION 42dbd37b3d0ebdf11991af51142ce4938d4bce99)\n[//]: # (START_SECTION c8756c929ccce9793a1e1717439a257c97f22203)\n### Cloud Config Security Updates\n\n> Commit: [c8756c929ccce9793a1e1717439a257c97f22203](https://github.com/dOpensource/dsiprouter/commit/c8756c929ccce9793a1e1717439a257c97f22203)  \n> Date: Mon, 24 Jun 2019 12:25:40 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #185\n- fix cloud-init overwriting sshd_config\n- add ASLR check\n- fix grub prompt issue in snapshot configs for ubuntu\n- switch cloud build script to v0.522\n\n\n---\n\n[//]: # (END_SECTION c8756c929ccce9793a1e1717439a257c97f22203)\n[//]: # (START_SECTION edfe59b7a42884380acc1344d17663000f75eff2)\n### Rewrote the reload API\n\n> Commit: [edfe59b7a42884380acc1344d17663000f75eff2](https://github.com/dOpensource/dsiprouter/commit/edfe59b7a42884380acc1344d17663000f75eff2)  \n> Date: Fri, 31 May 2019 05:30:36 +0000  \n> Author: root (root@ip-172-31-13-3.us-east-2.compute.internal)  \n> Committer: root (root@ip-172-31-13-3.us-east-2.compute.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION edfe59b7a42884380acc1344d17663000f75eff2)\n[//]: # (START_SECTION 5d739ba1d7f82408c41d858329ff9234f1ccd439)\n### Set it back so that it binds to all interfaces. This will cause the dashboard not to work on some OS builds\n\n> Commit: [5d739ba1d7f82408c41d858329ff9234f1ccd439](https://github.com/dOpensource/dsiprouter/commit/5d739ba1d7f82408c41d858329ff9234f1ccd439)  \n> Date: Tue, 28 May 2019 08:50:12 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5d739ba1d7f82408c41d858329ff9234f1ccd439)\n[//]: # (START_SECTION 6513f4410cb8923286d35492d48458efd5eb1d22)\n### -Fixed the provisioning server template to default to 443 -Fixed the fusionpbx sync script to handle 443 properly\n\n> Commit: [6513f4410cb8923286d35492d48458efd5eb1d22](https://github.com/dOpensource/dsiprouter/commit/6513f4410cb8923286d35492d48458efd5eb1d22)  \n> Date: Tue, 28 May 2019 08:36:16 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6513f4410cb8923286d35492d48458efd5eb1d22)\n[//]: # (START_SECTION 7ffc2a2a3d8727cfec8d298e85a81a5c3fb57bcb)\n### Use python docker module versus docker_py\n\n> Commit: [7ffc2a2a3d8727cfec8d298e85a81a5c3fb57bcb](https://github.com/dOpensource/dsiprouter/commit/7ffc2a2a3d8727cfec8d298e85a81a5c3fb57bcb)  \n> Date: Tue, 28 May 2019 07:07:40 +0000  \n> Author: root (root@p0.detroitpbx.com)  \n> Committer: root (root@p0.detroitpbx.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7ffc2a2a3d8727cfec8d298e85a81a5c3fb57bcb)\n[//]: # (START_SECTION 2bdd089bffb74d6c25e5f171748d16ebaea7c200)\n### Fixed an error that occurs when there are no FusionPBX sources to sync domain info\n\n> Commit: [2bdd089bffb74d6c25e5f171748d16ebaea7c200](https://github.com/dOpensource/dsiprouter/commit/2bdd089bffb74d6c25e5f171748d16ebaea7c200)  \n> Date: Sun, 26 May 2019 17:14:31 +0000  \n> Author: root (root@demo-dsiprouter-0.localdomain)  \n> Committer: root (root@demo-dsiprouter-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2bdd089bffb74d6c25e5f171748d16ebaea7c200)\n[//]: # (START_SECTION af9dbcda856289029402349ed003efbe0f10e95e)\n### Fixed a typo\n\n> Commit: [af9dbcda856289029402349ed003efbe0f10e95e](https://github.com/dOpensource/dsiprouter/commit/af9dbcda856289029402349ed003efbe0f10e95e)  \n> Date: Sat, 25 May 2019 10:01:48 +0000  \n> Author: root (root@demo-dsiprouter-0.localdomain)  \n> Committer: root (root@demo-dsiprouter-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION af9dbcda856289029402349ed003efbe0f10e95e)\n[//]: # (START_SECTION 6a82177b68c97bf88752b5dfb6ecf85d9abbd100)\n### Fixed an issue with listening on udp and tcp ports\n\n> Commit: [6a82177b68c97bf88752b5dfb6ecf85d9abbd100](https://github.com/dOpensource/dsiprouter/commit/6a82177b68c97bf88752b5dfb6ecf85d9abbd100)  \n> Date: Sat, 25 May 2019 09:29:37 +0000  \n> Author: root (root@demo-dsiprouter-0.localdomain)  \n> Committer: root (root@demo-dsiprouter-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6a82177b68c97bf88752b5dfb6ecf85d9abbd100)\n[//]: # (START_SECTION b1dd12b4f0a9d8727d561a1a6b3395ae9c684211)\n### Explicitly added a listen attribute for tcp. Fixed issue #170\n\n> Commit: [b1dd12b4f0a9d8727d561a1a6b3395ae9c684211](https://github.com/dOpensource/dsiprouter/commit/b1dd12b4f0a9d8727d561a1a6b3395ae9c684211)  \n> Date: Wed, 22 May 2019 15:54:05 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b1dd12b4f0a9d8727d561a1a6b3395ae9c684211)\n[//]: # (START_SECTION e04b046b75d3a0a31e7eebbcf30e36f10edb55e2)\n### Fixed #164\n\n> Commit: [e04b046b75d3a0a31e7eebbcf30e36f10edb55e2](https://github.com/dOpensource/dsiprouter/commit/e04b046b75d3a0a31e7eebbcf30e36f10edb55e2)  \n> Date: Mon, 20 May 2019 19:58:01 +0000  \n> Author: root (root@OmariDev-0.localdomain)  \n> Committer: root (root@OmariDev-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e04b046b75d3a0a31e7eebbcf30e36f10edb55e2)\n[//]: # (START_SECTION e665ba2b11285ecac9ee987fd1bc4a1ab6b68978)\n### Fixed a regression from Primary/Secondary Failover Enhancement\n\n> Commit: [e665ba2b11285ecac9ee987fd1bc4a1ab6b68978](https://github.com/dOpensource/dsiprouter/commit/e665ba2b11285ecac9ee987fd1bc4a1ab6b68978)  \n> Date: Fri, 17 May 2019 17:59:45 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e665ba2b11285ecac9ee987fd1bc4a1ab6b68978)\n[//]: # (START_SECTION b2da46c1581c79c6d176dacf7d17f622d36e7b23)\n### Update README.md\n\n> Commit: [b2da46c1581c79c6d176dacf7d17f622d36e7b23](https://github.com/dOpensource/dsiprouter/commit/b2da46c1581c79c6d176dacf7d17f622d36e7b23)  \n> Date: Thu, 16 May 2019 13:39:24 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b2da46c1581c79c6d176dacf7d17f622d36e7b23)\n[//]: # (START_SECTION 4c8d56dffb72abe22830054d8936f750578f91b2)\n### #163 fixed\n\n> Commit: [4c8d56dffb72abe22830054d8936f750578f91b2](https://github.com/dOpensource/dsiprouter/commit/4c8d56dffb72abe22830054d8936f750578f91b2)  \n> Date: Wed, 15 May 2019 20:16:27 +0000  \n> Author: root (root@OmariDev-0.localdomain)  \n> Committer: root (root@OmariDev-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4c8d56dffb72abe22830054d8936f750578f91b2)\n[//]: # (START_SECTION 702cf8a46262a9eb2e4848faaa34697cfa83bd52)\n### - The admin password is now being set properly on Debian - non AWS - Fixed the spacing when displaying the password info\n\n> Commit: [702cf8a46262a9eb2e4848faaa34697cfa83bd52](https://github.com/dOpensource/dsiprouter/commit/702cf8a46262a9eb2e4848faaa34697cfa83bd52)  \n> Date: Wed, 15 May 2019 07:57:23 +0000  \n> Author: root (root@dSIPRouterJenkins-0.localdomain)  \n> Committer: root (root@dSIPRouterJenkins-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 702cf8a46262a9eb2e4848faaa34697cfa83bd52)\n[//]: # (START_SECTION 8ce7b4dab224dd31a175f1284cae93b91204eab0)\n### fixed #160\n\n> Commit: [8ce7b4dab224dd31a175f1284cae93b91204eab0](https://github.com/dOpensource/dsiprouter/commit/8ce7b4dab224dd31a175f1284cae93b91204eab0)  \n> Date: Tue, 14 May 2019 21:24:08 +0000  \n> Author: root (root@OmariDev-0.localdomain)  \n> Committer: root (root@OmariDev-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8ce7b4dab224dd31a175f1284cae93b91204eab0)\n[//]: # (START_SECTION 743d40d88ffc853de5daef780fcb8297222a7d54)\n### Added the ability to clean up expired leases to our system wide cron script\n\n> Commit: [743d40d88ffc853de5daef780fcb8297222a7d54](https://github.com/dOpensource/dsiprouter/commit/743d40d88ffc853de5daef780fcb8297222a7d54)  \n> Date: Tue, 14 May 2019 11:23:07 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 743d40d88ffc853de5daef780fcb8297222a7d54)\n[//]: # (START_SECTION ae30e7972b8a261a7ce0d4f1a9d908e5547bf70d)\n### Startup and General Fixes\n\n> Commit: [ae30e7972b8a261a7ce0d4f1a9d908e5547bf70d](https://github.com/dOpensource/dsiprouter/commit/ae30e7972b8a261a7ce0d4f1a9d908e5547bf70d)  \n> Date: Tue, 14 May 2019 07:16:21 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix dsiprouter startup issue on centos and amazon linux\n- fix ssh server security holes\n- add git contributing pre-commit hook\n- update cloud instance checks\n- fix ubuntu unattended upgrade dialog issue\n- add custom MOTD banner\n- revert debug settings to production as default\n- improve ipv4 and ipv6 checks\n\n\n---\n\n[//]: # (END_SECTION ae30e7972b8a261a7ce0d4f1a9d908e5547bf70d)\n[//]: # (START_SECTION 2cfc720178153e19c8d537a5aa8ea2cdab0975f9)\n### Fixed LCR bug, Added a configuration parameter that allows the PBX INVITE timer to be changed globally during runtime\n\n> Commit: [2cfc720178153e19c8d537a5aa8ea2cdab0975f9](https://github.com/dOpensource/dsiprouter/commit/2cfc720178153e19c8d537a5aa8ea2cdab0975f9)  \n> Date: Fri, 10 May 2019 21:57:14 +0000  \n> Author: root (root@dSIPRouterMackTest-0.localdomain)  \n> Committer: root (root@dSIPRouterMackTest-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2cfc720178153e19c8d537a5aa8ea2cdab0975f9)\n[//]: # (START_SECTION 1d43d2ec96bfefb7132cb6b236dcf8ca06f6a09c)\n### Updated the .gitignore file\n\n> Commit: [1d43d2ec96bfefb7132cb6b236dcf8ca06f6a09c](https://github.com/dOpensource/dsiprouter/commit/1d43d2ec96bfefb7132cb6b236dcf8ca06f6a09c)  \n> Date: Fri, 10 May 2019 21:54:48 +0000  \n> Author: root (root@dSIPRouterMackTest-0.localdomain)  \n> Committer: root (root@dSIPRouterMackTest-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1d43d2ec96bfefb7132cb6b236dcf8ca06f6a09c)\n[//]: # (START_SECTION 07d8e0f85b48c54ca8da920ec3153fcf02ff7a17)\n### Removed old files\n\n> Commit: [07d8e0f85b48c54ca8da920ec3153fcf02ff7a17](https://github.com/dOpensource/dsiprouter/commit/07d8e0f85b48c54ca8da920ec3153fcf02ff7a17)  \n> Date: Fri, 10 May 2019 21:50:09 +0000  \n> Author: root (root@dSIPRouterMackTest-0.localdomain)  \n> Committer: root (root@dSIPRouterMackTest-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 07d8e0f85b48c54ca8da920ec3153fcf02ff7a17)\n[//]: # (START_SECTION 740e37bc645b340a2093c85114b1dcf00c7d5057)\n### Edits to carriergroups.js\n\n> Commit: [740e37bc645b340a2093c85114b1dcf00c7d5057](https://github.com/dOpensource/dsiprouter/commit/740e37bc645b340a2093c85114b1dcf00c7d5057)  \n> Date: Fri, 10 May 2019 17:08:55 +0000  \n> Author: root (root@OmariDev-0.localdomain)  \n> Committer: root (root@OmariDev-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 740e37bc645b340a2093c85114b1dcf00c7d5057)\n[//]: # (START_SECTION 7afa13f6302b3793a7ad04bb492f031c84d80f55)\n### Added initial support for a Kamailio Reload API\n\n> Commit: [7afa13f6302b3793a7ad04bb492f031c84d80f55](https://github.com/dOpensource/dsiprouter/commit/7afa13f6302b3793a7ad04bb492f031c84d80f55)  \n> Date: Thu, 9 May 2019 02:17:14 +0000  \n> Author: root (root@dSIPRouterMackTest-0.localdomain)  \n> Committer: root (root@dSIPRouterMackTest-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7afa13f6302b3793a7ad04bb492f031c84d80f55)\n[//]: # (START_SECTION ad06c1cfaa2de146df9dab483f00fb43a2295f6b)\n### Enabled the reload button when enabling and disabling an endpoint for maintenance\n\n> Commit: [ad06c1cfaa2de146df9dab483f00fb43a2295f6b](https://github.com/dOpensource/dsiprouter/commit/ad06c1cfaa2de146df9dab483f00fb43a2295f6b)  \n> Date: Wed, 8 May 2019 10:02:25 +0000  \n> Author: root (root@dSIPRouterMackTest-0.localdomain)  \n> Committer: root (root@dSIPRouterMackTest-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ad06c1cfaa2de146df9dab483f00fb43a2295f6b)\n[//]: # (START_SECTION e7a573fa5ae649a379a6385557a42a7112ef095e)\n### Added dsiprouter.sh to /usr/local/bin via a symbolic link\n\n> Commit: [e7a573fa5ae649a379a6385557a42a7112ef095e](https://github.com/dOpensource/dsiprouter/commit/e7a573fa5ae649a379a6385557a42a7112ef095e)  \n> Date: Wed, 8 May 2019 08:37:26 +0000  \n> Author: root (root@dSIPRouterJenkins-0.localdomain)  \n> Committer: root (root@dSIPRouterJenkins-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e7a573fa5ae649a379a6385557a42a7112ef095e)\n[//]: # (START_SECTION c8f1c13f26238b9ce8ff5fe9b161210ba70ed48e)\n### Fixed an issue with creating new carriergroups\n\n> Commit: [c8f1c13f26238b9ce8ff5fe9b161210ba70ed48e](https://github.com/dOpensource/dsiprouter/commit/c8f1c13f26238b9ce8ff5fe9b161210ba70ed48e)  \n> Date: Wed, 8 May 2019 07:50:00 +0000  \n> Author: root (root@dSIPRouterJenkins-0.localdomain)  \n> Committer: root (root@dSIPRouterJenkins-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c8f1c13f26238b9ce8ff5fe9b161210ba70ed48e)\n[//]: # (START_SECTION 7e9de543000ce6b6633b33eec1d82d6854f3dee7)\n### Added final logic to support gui and backend support for PBX failover and Endpoint Maintence\n\n> Commit: [7e9de543000ce6b6633b33eec1d82d6854f3dee7](https://github.com/dOpensource/dsiprouter/commit/7e9de543000ce6b6633b33eec1d82d6854f3dee7)  \n> Date: Tue, 7 May 2019 02:17:34 +0000  \n> Author: root (root@dSIPRouterMack0522-0.localdomain)  \n> Committer: root (root@dSIPRouterMack0522-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7e9de543000ce6b6633b33eec1d82d6854f3dee7)\n[//]: # (START_SECTION c3b788bdc8b2f59208a0c1f9f74cedd3513a75b0)\n### Update domains.rst\n\n> Commit: [c3b788bdc8b2f59208a0c1f9f74cedd3513a75b0](https://github.com/dOpensource/dsiprouter/commit/c3b788bdc8b2f59208a0c1f9f74cedd3513a75b0)  \n> Date: Mon, 6 May 2019 13:30:04 -0400  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c3b788bdc8b2f59208a0c1f9f74cedd3513a75b0)\n[//]: # (START_SECTION b666e3fa075ea5245a6aa43d9cbfe1007097c340)\n### Add files via upload\n\n> Commit: [b666e3fa075ea5245a6aa43d9cbfe1007097c340](https://github.com/dOpensource/dsiprouter/commit/b666e3fa075ea5245a6aa43d9cbfe1007097c340)  \n> Date: Mon, 6 May 2019 13:16:21 -0400  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b666e3fa075ea5245a6aa43d9cbfe1007097c340)\n[//]: # (START_SECTION 6afb800cba67485ac08beb712307b45ea95b64c7)\n### Added logic to update the endpoint api, added logic to display an indicator when a pbx is in maintenance mode\n\n> Commit: [6afb800cba67485ac08beb712307b45ea95b64c7](https://github.com/dOpensource/dsiprouter/commit/6afb800cba67485ac08beb712307b45ea95b64c7)  \n> Date: Mon, 6 May 2019 16:22:22 +0200  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6afb800cba67485ac08beb712307b45ea95b64c7)\n[//]: # (START_SECTION 2c8dd08c247cb459e0803564e21a8dc5841ed9d9)\n### Added API for updating an endpoint and ability to put an endpoint in maintenance mode\n\n> Commit: [2c8dd08c247cb459e0803564e21a8dc5841ed9d9](https://github.com/dOpensource/dsiprouter/commit/2c8dd08c247cb459e0803564e21a8dc5841ed9d9)  \n> Date: Fri, 3 May 2019 18:54:00 +0000  \n> Author: root (root@dSIPRouterMack0522-0.localdomain)  \n> Committer: root (root@dSIPRouterMack0522-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2c8dd08c247cb459e0803564e21a8dc5841ed9d9)\n[//]: # (START_SECTION 257d9be36a877688a002ee68473ba0fdcf47ea12)\n### Added the ability to REVOKE a lease\n\n> Commit: [257d9be36a877688a002ee68473ba0fdcf47ea12](https://github.com/dOpensource/dsiprouter/commit/257d9be36a877688a002ee68473ba0fdcf47ea12)  \n> Date: Tue, 30 Apr 2019 08:24:14 +0000  \n> Author: root (root@dSIPRouterMack0522-0.localdomain)  \n> Committer: root (root@dSIPRouterMack0522-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 257d9be36a877688a002ee68473ba0fdcf47ea12)\n[//]: # (START_SECTION 1a6cddb03a08c61e26b2dd32b9773e020b8f589a)\n### Turned off debugging\n\n> Commit: [1a6cddb03a08c61e26b2dd32b9773e020b8f589a](https://github.com/dOpensource/dsiprouter/commit/1a6cddb03a08c61e26b2dd32b9773e020b8f589a)  \n> Date: Tue, 30 Apr 2019 06:29:05 +0000  \n> Author: root (root@dSIPRouterMack0522-0.localdomain)  \n> Committer: root (root@dSIPRouterMack0522-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1a6cddb03a08c61e26b2dd32b9773e020b8f589a)\n[//]: # (START_SECTION 09313aa08dc8cccc7f27e03d5f6e6661d5e2b73d)\n### Initial commit of the install script for the API module\n\n> Commit: [09313aa08dc8cccc7f27e03d5f6e6661d5e2b73d](https://github.com/dOpensource/dsiprouter/commit/09313aa08dc8cccc7f27e03d5f6e6661d5e2b73d)  \n> Date: Mon, 29 Apr 2019 21:24:59 +0000  \n> Author: root (root@dSIPRouterMack0.522-0)  \n> Committer: root (root@dSIPRouterMack0.522-0)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 09313aa08dc8cccc7f27e03d5f6e6661d5e2b73d)\n[//]: # (START_SECTION 5697532b99955d48dd8bdc38db85b86616bcd781)\n### Removed dashboard.js from root directory\n\n> Commit: [5697532b99955d48dd8bdc38db85b86616bcd781](https://github.com/dOpensource/dsiprouter/commit/5697532b99955d48dd8bdc38db85b86616bcd781)  \n> Date: Mon, 29 Apr 2019 21:07:31 +0000  \n> Author: root (root@dSIPRouterMack0.522-0)  \n> Committer: root (root@dSIPRouterMack0.522-0)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5697532b99955d48dd8bdc38db85b86616bcd781)\n[//]: # (START_SECTION c2d3945ca539d59fb3533774c99718ed8529c121)\n### Inbound DID's will try the Primary and then the Secondary PBX.  The user will receive a 502 Service not available if both fail\n\n> Commit: [c2d3945ca539d59fb3533774c99718ed8529c121](https://github.com/dOpensource/dsiprouter/commit/c2d3945ca539d59fb3533774c99718ed8529c121)  \n> Date: Mon, 29 Apr 2019 21:04:03 +0000  \n> Author: root (root@dSIPRouterMack0.522-0)  \n> Committer: root (root@dSIPRouterMack0.522-0)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c2d3945ca539d59fb3533774c99718ed8529c121)\n[//]: # (START_SECTION a3a3fc5a7e4b658806e0bba562e6b703cf0f25f7)\n### Initial commit of the Endpoint API\n\n> Commit: [a3a3fc5a7e4b658806e0bba562e6b703cf0f25f7](https://github.com/dOpensource/dsiprouter/commit/a3a3fc5a7e4b658806e0bba562e6b703cf0f25f7)  \n> Date: Mon, 29 Apr 2019 20:42:45 +0000  \n> Author: root (root@dSIPRouterMack0.522-0)  \n> Committer: root (root@dSIPRouterMack0.522-0)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a3a3fc5a7e4b658806e0bba562e6b703cf0f25f7)\n[//]: # (START_SECTION 6880c7142972aabcdb864fc48a786478ffb8be49)\n### Added a security model for our API framework\n\n> Commit: [6880c7142972aabcdb864fc48a786478ffb8be49](https://github.com/dOpensource/dsiprouter/commit/6880c7142972aabcdb864fc48a786478ffb8be49)  \n> Date: Sun, 28 Apr 2019 03:53:21 +0000  \n> Author: root (root@dSIPRouterMack0522-0.localdomain)  \n> Committer: root (root@dSIPRouterMack0522-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6880c7142972aabcdb864fc48a786478ffb8be49)\n[//]: # (START_SECTION 1fb03173971da31cdc9a60cf8f8823cfe0eb6a4d)\n### Removed AMI changes that were made - going back to the orig\n\n> Commit: [1fb03173971da31cdc9a60cf8f8823cfe0eb6a4d](https://github.com/dOpensource/dsiprouter/commit/1fb03173971da31cdc9a60cf8f8823cfe0eb6a4d)  \n> Date: Mon, 22 Apr 2019 00:04:46 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1fb03173971da31cdc9a60cf8f8823cfe0eb6a4d)\n[//]: # (START_SECTION 46d70a337e287f93b05db5d9bf57c08eaa41e535)\n### Temporarily Removing AMI Checks to get Jenkins working\n\n> Commit: [46d70a337e287f93b05db5d9bf57c08eaa41e535](https://github.com/dOpensource/dsiprouter/commit/46d70a337e287f93b05db5d9bf57c08eaa41e535)  \n> Date: Sun, 21 Apr 2019 23:59:38 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 46d70a337e287f93b05db5d9bf57c08eaa41e535)\n[//]: # (START_SECTION b9ce7647ba8f7e4d49f8824ca24b7914c1ef4404)\n### Adding parameter to curl command for AMI check\n\n> Commit: [b9ce7647ba8f7e4d49f8824ca24b7914c1ef4404](https://github.com/dOpensource/dsiprouter/commit/b9ce7647ba8f7e4d49f8824ca24b7914c1ef4404)  \n> Date: Sun, 21 Apr 2019 23:49:26 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b9ce7647ba8f7e4d49f8824ca24b7914c1ef4404)\n[//]: # (START_SECTION 3e78e39922364d8022190c8e188e07744e35b8ff)\n### Update README.md\n\n> Commit: [3e78e39922364d8022190c8e188e07744e35b8ff](https://github.com/dOpensource/dsiprouter/commit/3e78e39922364d8022190c8e188e07744e35b8ff)  \n> Date: Sun, 21 Apr 2019 23:00:16 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3e78e39922364d8022190c8e188e07744e35b8ff)\n[//]: # (START_SECTION 00900d28e831b38c5dc78aa3d66b2a4938d0d3a8)\n### Update settings.py\n\n> Commit: [00900d28e831b38c5dc78aa3d66b2a4938d0d3a8](https://github.com/dOpensource/dsiprouter/commit/00900d28e831b38c5dc78aa3d66b2a4938d0d3a8)  \n> Date: Wed, 17 Apr 2019 07:19:41 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 00900d28e831b38c5dc78aa3d66b2a4938d0d3a8)\n[//]: # (START_SECTION 32d04837b45a8d15a6d0ef3fba608383f4a67197)\n### Fixed #130 - l_username field of the uac_reg table will be populated with the gateway group id.  This will get rid of the error messages in the Kamailio log\n\n> Commit: [32d04837b45a8d15a6d0ef3fba608383f4a67197](https://github.com/dOpensource/dsiprouter/commit/32d04837b45a8d15a6d0ef3fba608383f4a67197)  \n> Date: Tue, 16 Apr 2019 11:18:50 +0000  \n> Author: root (root@dSIPRouterMack-0.localdomain)  \n> Committer: root (root@dSIPRouterMack-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 32d04837b45a8d15a6d0ef3fba608383f4a67197)\n[//]: # (START_SECTION e562ab294d1654152a4b8bad1e64590212d6e26d)\n### Added the default value for l_username so that the system has it during bootup\n\n> Commit: [e562ab294d1654152a4b8bad1e64590212d6e26d](https://github.com/dOpensource/dsiprouter/commit/e562ab294d1654152a4b8bad1e64590212d6e26d)  \n> Date: Tue, 16 Apr 2019 11:08:55 +0000  \n> Author: root (root@dSIPRouterMack-0.localdomain)  \n> Committer: root (root@dSIPRouterMack-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e562ab294d1654152a4b8bad1e64590212d6e26d)\n[//]: # (START_SECTION 9997109f3b7c7d397b5af4203765da6d582f9e83)\n### Added unit test to test if known carrier ip's are being blocked by the PIKE module Fixed #148\n\n> Commit: [9997109f3b7c7d397b5af4203765da6d582f9e83](https://github.com/dOpensource/dsiprouter/commit/9997109f3b7c7d397b5af4203765da6d582f9e83)  \n> Date: Mon, 15 Apr 2019 19:19:47 +0000  \n> Author: root (root@dSIPRouterMack-0.localdomain)  \n> Committer: root (root@dSIPRouterMack-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9997109f3b7c7d397b5af4203765da6d582f9e83)\n[//]: # (START_SECTION 2eebe2e8e8fa3dd9924d95bcf2725f4f58146337)\n### Working unit test for Domain Pass-Thru using FreePBX\n\n> Commit: [2eebe2e8e8fa3dd9924d95bcf2725f4f58146337](https://github.com/dOpensource/dsiprouter/commit/2eebe2e8e8fa3dd9924d95bcf2725f4f58146337)  \n> Date: Mon, 15 Apr 2019 10:49:32 +0000  \n> Author: root (root@dSIPRouterMack-0.localdomain)  \n> Committer: root (root@dSIPRouterMack-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2eebe2e8e8fa3dd9924d95bcf2725f4f58146337)\n[//]: # (START_SECTION 156950f559f1c3cfc05cf1b69396fd39fc4343bc)\n### Update README.md\n\n> Commit: [156950f559f1c3cfc05cf1b69396fd39fc4343bc](https://github.com/dOpensource/dsiprouter/commit/156950f559f1c3cfc05cf1b69396fd39fc4343bc)  \n> Date: Sun, 14 Apr 2019 18:57:49 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 156950f559f1c3cfc05cf1b69396fd39fc4343bc)\n[//]: # (START_SECTION b1bd959c9dd83b36c94f1fe2ae7ddf038e58f8ca)\n### Added basic exception handling\n\n> Commit: [b1bd959c9dd83b36c94f1fe2ae7ddf038e58f8ca](https://github.com/dOpensource/dsiprouter/commit/b1bd959c9dd83b36c94f1fe2ae7ddf038e58f8ca)  \n> Date: Sun, 14 Apr 2019 19:07:11 +0000  \n> Author: root (root@dSIPRouterMack-0.localdomain)  \n> Committer: root (root@dSIPRouterMack-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b1bd959c9dd83b36c94f1fe2ae7ddf038e58f8ca)\n[//]: # (START_SECTION fae5015141acbff318d770f2dfa73f031c5fcbf8)\n### Removed the old docker-py python library\n\n> Commit: [fae5015141acbff318d770f2dfa73f031c5fcbf8](https://github.com/dOpensource/dsiprouter/commit/fae5015141acbff318d770f2dfa73f031c5fcbf8)  \n> Date: Sun, 14 Apr 2019 18:46:04 +0000  \n> Author: root (root@dSIPRouterMack-0.localdomain)  \n> Committer: root (root@dSIPRouterMack-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fae5015141acbff318d770f2dfa73f031c5fcbf8)\n[//]: # (START_SECTION 2d32a49fe117828144eb2f0503429b7c411816eb)\n### Fixed #144 and fixed a regression with the FusionPBX Enable/Disable button in the PBX section\n\n> Commit: [2d32a49fe117828144eb2f0503429b7c411816eb](https://github.com/dOpensource/dsiprouter/commit/2d32a49fe117828144eb2f0503429b7c411816eb)  \n> Date: Sun, 14 Apr 2019 18:16:36 +0000  \n> Author: root (root@dSIPRouterMack-0.localdomain)  \n> Committer: root (root@dSIPRouterMack-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2d32a49fe117828144eb2f0503429b7c411816eb)\n[//]: # (START_SECTION 2b9d82e43ce6c4d1bc50d1a7f90b6657c2549633)\n### Removed the old docker-py library and added the docker\n\n> Commit: [2b9d82e43ce6c4d1bc50d1a7f90b6657c2549633](https://github.com/dOpensource/dsiprouter/commit/2b9d82e43ce6c4d1bc50d1a7f90b6657c2549633)  \n> Date: Sun, 14 Apr 2019 03:41:11 +0000  \n> Author: root (root@dSIPRouterMack-0.localdomain)  \n> Committer: root (root@dSIPRouterMack-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2b9d82e43ce6c4d1bc50d1a7f90b6657c2549633)\n[//]: # (START_SECTION 61a20614269616a9a3f212eee62c988a6e98d531)\n### Initial commit of unit test 17, which will be used for testing Domain Pass-Thru\n\n> Commit: [61a20614269616a9a3f212eee62c988a6e98d531](https://github.com/dOpensource/dsiprouter/commit/61a20614269616a9a3f212eee62c988a6e98d531)  \n> Date: Sat, 13 Apr 2019 23:49:13 +0000  \n> Author: root (root@dSIPRouterMack-0.localdomain)  \n> Committer: root (root@dSIPRouterMack-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 61a20614269616a9a3f212eee62c988a6e98d531)\n[//]: # (START_SECTION 75a875d51968f5c1e13ad65ab41e67da6e979c72)\n### Fixed the Reload Kamailio button so that it reload the Domain module when pressed\n\n> Commit: [75a875d51968f5c1e13ad65ab41e67da6e979c72](https://github.com/dOpensource/dsiprouter/commit/75a875d51968f5c1e13ad65ab41e67da6e979c72)  \n> Date: Sat, 13 Apr 2019 21:58:02 +0000  \n> Author: root (root@dSIPRouterMack-0.localdomain)  \n> Committer: root (root@dSIPRouterMack-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 75a875d51968f5c1e13ad65ab41e67da6e979c72)\n[//]: # (START_SECTION 8bb17331350a9111b5aca33e0869177d47562ed2)\n### Fixed #145, Fixed #139, Fixed #142 - The Domain functionality has been fixed.  Adding, Removing and Deleting DDomains has been fixed.  Also, the parameter needed to route traffic when using pass-thru authentication has been fixed\n\n> Commit: [8bb17331350a9111b5aca33e0869177d47562ed2](https://github.com/dOpensource/dsiprouter/commit/8bb17331350a9111b5aca33e0869177d47562ed2)  \n> Date: Sat, 13 Apr 2019 17:24:37 +0000  \n> Author: root (root@dSIPRouterNicole2.localdomain)  \n> Committer: root (root@dSIPRouterNicole2.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8bb17331350a9111b5aca33e0869177d47562ed2)\n[//]: # (START_SECTION 87811202be34c5202ee3525cb5c2fda1c06f9d28)\n### Added sngrep back to Debian 8 and 9 installs.  Fixed #147\n\n> Commit: [87811202be34c5202ee3525cb5c2fda1c06f9d28](https://github.com/dOpensource/dsiprouter/commit/87811202be34c5202ee3525cb5c2fda1c06f9d28)  \n> Date: Thu, 11 Apr 2019 14:44:47 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 87811202be34c5202ee3525cb5c2fda1c06f9d28)\n[//]: # (START_SECTION daa6bbd9c0543d2687d9d5166f1a8b0b1c29ecc8)\n### Fixed the AWS test\n\n> Commit: [daa6bbd9c0543d2687d9d5166f1a8b0b1c29ecc8](https://github.com/dOpensource/dsiprouter/commit/daa6bbd9c0543d2687d9d5166f1a8b0b1c29ecc8)  \n> Date: Thu, 11 Apr 2019 09:59:33 +0000  \n> Author: root (root@ip-172-31-18-84.ec2.internal)  \n> Committer: root (root@ip-172-31-18-84.ec2.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION daa6bbd9c0543d2687d9d5166f1a8b0b1c29ecc8)\n[//]: # (START_SECTION 4456d9b0414e7458bfa977830d14b32a312de50d)\n### Removed the test exit command from the install script\n\n> Commit: [4456d9b0414e7458bfa977830d14b32a312de50d](https://github.com/dOpensource/dsiprouter/commit/4456d9b0414e7458bfa977830d14b32a312de50d)  \n> Date: Thu, 11 Apr 2019 09:50:05 +0000  \n> Author: root (root@ip-172-31-18-84.ec2.internal)  \n> Committer: root (root@ip-172-31-18-84.ec2.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4456d9b0414e7458bfa977830d14b32a312de50d)\n[//]: # (START_SECTION c2cab69eadc89cacaef04837c068830abf219cd1)\n### Fixed the function in the testing harness that's responsible for validating if an instance is an EC2 instance on Amazon\n\n> Commit: [c2cab69eadc89cacaef04837c068830abf219cd1](https://github.com/dOpensource/dsiprouter/commit/c2cab69eadc89cacaef04837c068830abf219cd1)  \n> Date: Thu, 11 Apr 2019 03:54:00 +0000  \n> Author: Mack Hendricks (mack@dopensource.comm)  \n> Committer: Mack Hendricks (mack@dopensource.comm)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c2cab69eadc89cacaef04837c068830abf219cd1)\n[//]: # (START_SECTION 7ac6f555eb29477b3cb60d39f576ecceaf35859d)\n### Fixed the URL endpont used to validate if the instance is an EC2 instancing running on Amazon\n\n> Commit: [7ac6f555eb29477b3cb60d39f576ecceaf35859d](https://github.com/dOpensource/dsiprouter/commit/7ac6f555eb29477b3cb60d39f576ecceaf35859d)  \n> Date: Thu, 11 Apr 2019 03:52:47 +0000  \n> Author: Mack Hendricks (mack@dopensource.comm)  \n> Committer: Mack Hendricks (mack@dopensource.comm)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7ac6f555eb29477b3cb60d39f576ecceaf35859d)\n[//]: # (START_SECTION 42a866d4c75503843b0a7471d1f3ec6c4065b1d8)\n### Update README.md\n\n> Commit: [42a866d4c75503843b0a7471d1f3ec6c4065b1d8](https://github.com/dOpensource/dsiprouter/commit/42a866d4c75503843b0a7471d1f3ec6c4065b1d8)  \n> Date: Mon, 1 Apr 2019 07:11:11 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 42a866d4c75503843b0a7471d1f3ec6c4065b1d8)\n[//]: # (START_SECTION ff11825a7e043d30879b81b6e9436ad248e068ef)\n### Update README.md\n\n> Commit: [ff11825a7e043d30879b81b6e9436ad248e068ef](https://github.com/dOpensource/dsiprouter/commit/ff11825a7e043d30879b81b6e9436ad248e068ef)  \n> Date: Mon, 1 Apr 2019 07:10:32 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ff11825a7e043d30879b81b6e9436ad248e068ef)\n[//]: # (START_SECTION 36db3ecf92117d20a83581c018e83c09b89ef3a7)\n### Update README.md\n\n> Commit: [36db3ecf92117d20a83581c018e83c09b89ef3a7](https://github.com/dOpensource/dsiprouter/commit/36db3ecf92117d20a83581c018e83c09b89ef3a7)  \n> Date: Mon, 1 Apr 2019 07:10:04 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 36db3ecf92117d20a83581c018e83c09b89ef3a7)\n[//]: # (START_SECTION 3b15194e64db505f314e1e6f980eca588fb1aafd)\n### Update installing.rst\n\n> Commit: [3b15194e64db505f314e1e6f980eca588fb1aafd](https://github.com/dOpensource/dsiprouter/commit/3b15194e64db505f314e1e6f980eca588fb1aafd)  \n> Date: Mon, 1 Apr 2019 06:47:40 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@dSIPRouterMackMaster-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3b15194e64db505f314e1e6f980eca588fb1aafd)\n[//]: # (START_SECTION 6f6d1ea3a0b3bb1089e59b710357e83abd331287)\n### Update command_line_options.rst\n\n> Commit: [6f6d1ea3a0b3bb1089e59b710357e83abd331287](https://github.com/dOpensource/dsiprouter/commit/6f6d1ea3a0b3bb1089e59b710357e83abd331287)  \n> Date: Mon, 1 Apr 2019 06:45:55 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@dSIPRouterMackMaster-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6f6d1ea3a0b3bb1089e59b710357e83abd331287)\n[//]: # (START_SECTION b534497026ee08ca01f7a3d15c4fde24376cdb4a)\n### Update installing.rst\n\n> Commit: [b534497026ee08ca01f7a3d15c4fde24376cdb4a](https://github.com/dOpensource/dsiprouter/commit/b534497026ee08ca01f7a3d15c4fde24376cdb4a)  \n> Date: Mon, 1 Apr 2019 06:45:31 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@dSIPRouterMackMaster-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b534497026ee08ca01f7a3d15c4fde24376cdb4a)\n[//]: # (START_SECTION 2e8a0fd7cdea999ae4e90f9f1db1bf36aad95e00)\n### Update command_line_options.rst\n\n> Commit: [2e8a0fd7cdea999ae4e90f9f1db1bf36aad95e00](https://github.com/dOpensource/dsiprouter/commit/2e8a0fd7cdea999ae4e90f9f1db1bf36aad95e00)  \n> Date: Mon, 1 Apr 2019 06:42:59 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@dSIPRouterMackMaster-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2e8a0fd7cdea999ae4e90f9f1db1bf36aad95e00)\n[//]: # (START_SECTION 00fc1334ea6dcb933d56e9231a98f97b6976afa4)\n### Update command_line_options.rst\n\n> Commit: [00fc1334ea6dcb933d56e9231a98f97b6976afa4](https://github.com/dOpensource/dsiprouter/commit/00fc1334ea6dcb933d56e9231a98f97b6976afa4)  \n> Date: Mon, 1 Apr 2019 06:40:42 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@dSIPRouterMackMaster-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 00fc1334ea6dcb933d56e9231a98f97b6976afa4)\n[//]: # (START_SECTION f8d704d99cce7f00696f0aa9ca6272fb7f04c005)\n### Update command_line_options.rst\n\n> Commit: [f8d704d99cce7f00696f0aa9ca6272fb7f04c005](https://github.com/dOpensource/dsiprouter/commit/f8d704d99cce7f00696f0aa9ca6272fb7f04c005)  \n> Date: Mon, 1 Apr 2019 06:38:38 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@dSIPRouterMackMaster-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f8d704d99cce7f00696f0aa9ca6272fb7f04c005)\n[//]: # (START_SECTION c469f0ba9886092a80dcb7b317a0fceecd9ce82d)\n### Update command_line_options.rst\n\n> Commit: [c469f0ba9886092a80dcb7b317a0fceecd9ce82d](https://github.com/dOpensource/dsiprouter/commit/c469f0ba9886092a80dcb7b317a0fceecd9ce82d)  \n> Date: Mon, 1 Apr 2019 06:35:20 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@dSIPRouterMackMaster-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c469f0ba9886092a80dcb7b317a0fceecd9ce82d)\n[//]: # (START_SECTION 0c20533d57f9a7b501c48a7e7d91b046a4cf1454)\n### Update command_line_options.rst\n\n> Commit: [0c20533d57f9a7b501c48a7e7d91b046a4cf1454](https://github.com/dOpensource/dsiprouter/commit/0c20533d57f9a7b501c48a7e7d91b046a4cf1454)  \n> Date: Mon, 1 Apr 2019 06:25:09 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@dSIPRouterMackMaster-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0c20533d57f9a7b501c48a7e7d91b046a4cf1454)\n[//]: # (START_SECTION 107f39e5db77df3f782b1a45d233e4f2b028dc2c)\n### Update installing.rst\n\n> Commit: [107f39e5db77df3f782b1a45d233e4f2b028dc2c](https://github.com/dOpensource/dsiprouter/commit/107f39e5db77df3f782b1a45d233e4f2b028dc2c)  \n> Date: Mon, 1 Apr 2019 05:55:36 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@dSIPRouterMackMaster-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 107f39e5db77df3f782b1a45d233e4f2b028dc2c)\n[//]: # (START_SECTION 3bc03c149ff90926257ff722f82ef9bdcdd16d20)\n### Update installing.rst\n\n> Commit: [3bc03c149ff90926257ff722f82ef9bdcdd16d20](https://github.com/dOpensource/dsiprouter/commit/3bc03c149ff90926257ff722f82ef9bdcdd16d20)  \n> Date: Mon, 1 Apr 2019 05:52:35 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@dSIPRouterMackMaster-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3bc03c149ff90926257ff722f82ef9bdcdd16d20)\n[//]: # (START_SECTION a1dd9044d6dc834354326a1cb23d30e018f7921e)\n### Update installing.rst\n\n> Commit: [a1dd9044d6dc834354326a1cb23d30e018f7921e](https://github.com/dOpensource/dsiprouter/commit/a1dd9044d6dc834354326a1cb23d30e018f7921e)  \n> Date: Mon, 1 Apr 2019 05:49:43 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@dSIPRouterMackMaster-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a1dd9044d6dc834354326a1cb23d30e018f7921e)\n[//]: # (START_SECTION a1f1377d8f50f44d6635b3eec779da2aa53ee279)\n### Update centos-install.rst\n\n> Commit: [a1f1377d8f50f44d6635b3eec779da2aa53ee279](https://github.com/dOpensource/dsiprouter/commit/a1f1377d8f50f44d6635b3eec779da2aa53ee279)  \n> Date: Mon, 1 Apr 2019 05:31:37 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@dSIPRouterMackMaster-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a1f1377d8f50f44d6635b3eec779da2aa53ee279)\n[//]: # (START_SECTION b8916a7cb5330c098b9d207305bd75656c98bc95)\n### Update debian_install.rst\n\n> Commit: [b8916a7cb5330c098b9d207305bd75656c98bc95](https://github.com/dOpensource/dsiprouter/commit/b8916a7cb5330c098b9d207305bd75656c98bc95)  \n> Date: Sun, 31 Mar 2019 10:55:37 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@dSIPRouterMackMaster-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b8916a7cb5330c098b9d207305bd75656c98bc95)\n[//]: # (START_SECTION c190908d01f3d219cc55add4d95d5416eaa7b332)\n### Updated Debian Install Docs\n\n> Commit: [c190908d01f3d219cc55add4d95d5416eaa7b332](https://github.com/dOpensource/dsiprouter/commit/c190908d01f3d219cc55add4d95d5416eaa7b332)  \n> Date: Sun, 31 Mar 2019 10:54:02 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@dSIPRouterMackMaster-0.localdomain)  \n> Signed:   \n\n\n- Modified the docs to reflect the new install options\n\n\n---\n\n[//]: # (END_SECTION c190908d01f3d219cc55add4d95d5416eaa7b332)\n[//]: # (START_SECTION c394864619651a0fd186f0d12d520a25a669e3bb)\n### Update installing.rst\n\n> Commit: [c394864619651a0fd186f0d12d520a25a669e3bb](https://github.com/dOpensource/dsiprouter/commit/c394864619651a0fd186f0d12d520a25a669e3bb)  \n> Date: Mon, 1 Apr 2019 06:47:40 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c394864619651a0fd186f0d12d520a25a669e3bb)\n[//]: # (START_SECTION a35858ff9118b1e851b49132eea9581d532537da)\n### Update command_line_options.rst\n\n> Commit: [a35858ff9118b1e851b49132eea9581d532537da](https://github.com/dOpensource/dsiprouter/commit/a35858ff9118b1e851b49132eea9581d532537da)  \n> Date: Mon, 1 Apr 2019 06:45:55 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a35858ff9118b1e851b49132eea9581d532537da)\n[//]: # (START_SECTION 8ebf5fba21f99bf904f38569e3bda3306ae8fa9e)\n### Update installing.rst\n\n> Commit: [8ebf5fba21f99bf904f38569e3bda3306ae8fa9e](https://github.com/dOpensource/dsiprouter/commit/8ebf5fba21f99bf904f38569e3bda3306ae8fa9e)  \n> Date: Mon, 1 Apr 2019 06:45:31 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8ebf5fba21f99bf904f38569e3bda3306ae8fa9e)\n[//]: # (START_SECTION 75d76e36326e649ff354d6848dd07b34f8456dd9)\n### Update command_line_options.rst\n\n> Commit: [75d76e36326e649ff354d6848dd07b34f8456dd9](https://github.com/dOpensource/dsiprouter/commit/75d76e36326e649ff354d6848dd07b34f8456dd9)  \n> Date: Mon, 1 Apr 2019 06:42:59 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 75d76e36326e649ff354d6848dd07b34f8456dd9)\n[//]: # (START_SECTION fa1964b57de7a486694a979d393625c4224ae34c)\n### Update command_line_options.rst\n\n> Commit: [fa1964b57de7a486694a979d393625c4224ae34c](https://github.com/dOpensource/dsiprouter/commit/fa1964b57de7a486694a979d393625c4224ae34c)  \n> Date: Mon, 1 Apr 2019 06:40:42 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fa1964b57de7a486694a979d393625c4224ae34c)\n[//]: # (START_SECTION f79dc044b38d7cb50bedc6c8ab5860d0c6c361eb)\n### Update command_line_options.rst\n\n> Commit: [f79dc044b38d7cb50bedc6c8ab5860d0c6c361eb](https://github.com/dOpensource/dsiprouter/commit/f79dc044b38d7cb50bedc6c8ab5860d0c6c361eb)  \n> Date: Mon, 1 Apr 2019 06:38:38 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f79dc044b38d7cb50bedc6c8ab5860d0c6c361eb)\n[//]: # (START_SECTION 6204fdb79d99d8a6ec90185d14fffb2ec624693b)\n### Update command_line_options.rst\n\n> Commit: [6204fdb79d99d8a6ec90185d14fffb2ec624693b](https://github.com/dOpensource/dsiprouter/commit/6204fdb79d99d8a6ec90185d14fffb2ec624693b)  \n> Date: Mon, 1 Apr 2019 06:35:20 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6204fdb79d99d8a6ec90185d14fffb2ec624693b)\n[//]: # (START_SECTION c6640ff2ecf78225565be2778b9447bad0415571)\n### Update command_line_options.rst\n\n> Commit: [c6640ff2ecf78225565be2778b9447bad0415571](https://github.com/dOpensource/dsiprouter/commit/c6640ff2ecf78225565be2778b9447bad0415571)  \n> Date: Mon, 1 Apr 2019 06:25:09 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c6640ff2ecf78225565be2778b9447bad0415571)\n[//]: # (START_SECTION c855821012ed71159c14759768f173a0fa754495)\n### Update installing.rst\n\n> Commit: [c855821012ed71159c14759768f173a0fa754495](https://github.com/dOpensource/dsiprouter/commit/c855821012ed71159c14759768f173a0fa754495)  \n> Date: Mon, 1 Apr 2019 05:55:36 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c855821012ed71159c14759768f173a0fa754495)\n[//]: # (START_SECTION 9da93ccdb5043c45a03ea9efab26d69dda6df426)\n### Update installing.rst\n\n> Commit: [9da93ccdb5043c45a03ea9efab26d69dda6df426](https://github.com/dOpensource/dsiprouter/commit/9da93ccdb5043c45a03ea9efab26d69dda6df426)  \n> Date: Mon, 1 Apr 2019 05:52:35 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9da93ccdb5043c45a03ea9efab26d69dda6df426)\n[//]: # (START_SECTION e3cf3de293dbf4361f384539ef9248b495464953)\n### Update installing.rst\n\n> Commit: [e3cf3de293dbf4361f384539ef9248b495464953](https://github.com/dOpensource/dsiprouter/commit/e3cf3de293dbf4361f384539ef9248b495464953)  \n> Date: Mon, 1 Apr 2019 05:49:43 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e3cf3de293dbf4361f384539ef9248b495464953)\n[//]: # (START_SECTION 5929359151b00d7eda040110ab236d2a529137b8)\n### Update centos-install.rst\n\n> Commit: [5929359151b00d7eda040110ab236d2a529137b8](https://github.com/dOpensource/dsiprouter/commit/5929359151b00d7eda040110ab236d2a529137b8)  \n> Date: Mon, 1 Apr 2019 05:31:37 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5929359151b00d7eda040110ab236d2a529137b8)\n[//]: # (START_SECTION 8667c490c5f269cc10e1574e24ba940c7016630e)\n### Update debian_install.rst\n\n> Commit: [8667c490c5f269cc10e1574e24ba940c7016630e](https://github.com/dOpensource/dsiprouter/commit/8667c490c5f269cc10e1574e24ba940c7016630e)  \n> Date: Sun, 31 Mar 2019 10:55:37 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8667c490c5f269cc10e1574e24ba940c7016630e)\n[//]: # (START_SECTION ed9162b21b4851a8443c2fc9e4f86f24da57f536)\n### Updated Debian Install Docs\n\n> Commit: [ed9162b21b4851a8443c2fc9e4f86f24da57f536](https://github.com/dOpensource/dsiprouter/commit/ed9162b21b4851a8443c2fc9e4f86f24da57f536)  \n> Date: Sun, 31 Mar 2019 10:54:02 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Modified the docs to reflect the new install options\n\n\n---\n\n[//]: # (END_SECTION ed9162b21b4851a8443c2fc9e4f86f24da57f536)\n[//]: # (START_SECTION 309020346d1d02532ba6479eb38c5b46fcbe1422)\n### Initial commit of an active Dashboard\n\n> Commit: [309020346d1d02532ba6479eb38c5b46fcbe1422](https://github.com/dOpensource/dsiprouter/commit/309020346d1d02532ba6479eb38c5b46fcbe1422)  \n> Date: Sat, 30 Mar 2019 23:48:14 +0000  \n> Author: root (root@dSIPRouterMackDev-0.localdomain)  \n> Committer: root (root@dSIPRouterMackDev-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 309020346d1d02532ba6479eb38c5b46fcbe1422)\n[//]: # (START_SECTION 13996bebd47fe648fb374aae3517c6e8ae828478)\n### Final AMI Updates for Release v0.52\n\n> Commit: [13996bebd47fe648fb374aae3517c6e8ae828478](https://github.com/dOpensource/dsiprouter/commit/13996bebd47fe648fb374aae3517c6e8ae828478)  \n> Date: Wed, 27 Mar 2019 21:29:11 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix double password reset issue\n- cleanup unneeded extra files\n- update dsiprouter service\n- allow color printing function use inline\n- added and updated comments / TODO's\n- allow DB driver selection\n- fix uninstall dsiprouter to not fail on pip cmd\n- fix configurePythonSettings issue\n- add support for debian8 (jessie)\n- add support for ubuntu 16.04 (xenial)\n- add support for amazon linux 2\n- fix false negatives on install starting services\n- add color to usage cmd output\n\n\n---\n\n[//]: # (END_SECTION 13996bebd47fe648fb374aae3517c6e8ae828478)\n[//]: # (START_SECTION a711d7e9597a015d2fed25a7a0bd10df04bdc9ab)\n### Update dsiprouter.sh\n\n> Commit: [a711d7e9597a015d2fed25a7a0bd10df04bdc9ab](https://github.com/dOpensource/dsiprouter/commit/a711d7e9597a015d2fed25a7a0bd10df04bdc9ab)  \n> Date: Wed, 27 Mar 2019 03:27:16 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Removed generatePassword from the displayLoginInfo function\n\n\n---\n\n[//]: # (END_SECTION a711d7e9597a015d2fed25a7a0bd10df04bdc9ab)\n[//]: # (START_SECTION 20cbff25f55c0a305e3ef1528d785c581988e7d5)\n### Update Version Number for Release v0.52\n\n> Commit: [20cbff25f55c0a305e3ef1528d785c581988e7d5](https://github.com/dOpensource/dsiprouter/commit/20cbff25f55c0a305e3ef1528d785c581988e7d5)  \n> Date: Tue, 26 Mar 2019 09:59:59 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 20cbff25f55c0a305e3ef1528d785c581988e7d5)\n[//]: # (START_SECTION 886179285c09f239edbd573b620257fcad3f3c01)\n###   - Fixes #103   - deprecate Debian v7   - deprecate Debian v8   - change CentOS RTPEngine install to RPM build   - fix startup issues with dsip-init service on AWS   - added dpkg defaults during script execution\n\n> Commit: [886179285c09f239edbd573b620257fcad3f3c01](https://github.com/dOpensource/dsiprouter/commit/886179285c09f239edbd573b620257fcad3f3c01)  \n> Date: Tue, 26 Mar 2019 08:12:01 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 886179285c09f239edbd573b620257fcad3f3c01)\n[//]: # (START_SECTION 8ea6bb8ccc48d688e73d0ef614f33ee28d0c8a77)\n### Merge feature-ami Branch Into dev Branch\n\n> Commit: [8ea6bb8ccc48d688e73d0ef614f33ee28d0c8a77](https://github.com/dOpensource/dsiprouter/commit/8ea6bb8ccc48d688e73d0ef614f33ee28d0c8a77)  \n> Date: Mon, 25 Mar 2019 15:41:44 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- rename JSONRPC test to `16.sh`\n- merge feature-ami commits onto dev branch\n\n\n---\n\n[//]: # (END_SECTION 8ea6bb8ccc48d688e73d0ef614f33ee28d0c8a77)\n[//]: # (START_SECTION b168f8ede2ed17483fa46c52baec81acdcbdbca5)\n### Fixup Firewalld Commands\n\n> Commit: [b168f8ede2ed17483fa46c52baec81acdcbdbca5](https://github.com/dOpensource/dsiprouter/commit/b168f8ede2ed17483fa46c52baec81acdcbdbca5)  \n> Date: Mon, 25 Mar 2019 15:01:28 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add tcp port for jsonrpc access\n- cleanup centos commands\n\n\n---\n\n[//]: # (END_SECTION b168f8ede2ed17483fa46c52baec81acdcbdbca5)\n[//]: # (START_SECTION 63647ef1e00134c14307c65d29b2539d482607a3)\n###   - fix mariadb centos startup regression   - fix module sql install username conflict   - set default for ssl variables to avoid errors   - move displaying login info back to after logo   - update a few sed cmds to be more reliable\n\n> Commit: [63647ef1e00134c14307c65d29b2539d482607a3](https://github.com/dOpensource/dsiprouter/commit/63647ef1e00134c14307c65d29b2539d482607a3)  \n> Date: Mon, 25 Mar 2019 14:49:38 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n\n\n---\n\n[//]: # (END_SECTION 63647ef1e00134c14307c65d29b2539d482607a3)\n[//]: # (START_SECTION f653ce88a5fe3c38de000f0287eb44023067ab6c)\n### AMI Feature Fixes\n\n> Commit: [f653ce88a5fe3c38de000f0287eb44023067ab6c](https://github.com/dOpensource/dsiprouter/commit/f653ce88a5fe3c38de000f0287eb44023067ab6c)  \n> Date: Mon, 25 Mar 2019 10:53:34 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- update AMI funtions to run with systemd\n- add seperate log file for dsip cloud installs\n- fix broken paths\n- make kam cfg actual readable (spaces not tabs)\n- add test for syslog service\n- add test for AMI requirements\n- add test for dsip GUI login\n- add dev files for next tests to make\n- fix test sorting to work past 10\n- add work on custom redirection function\n- fix login logic in routes and HTTP return codes\n\n\n---\n\n[//]: # (END_SECTION f653ce88a5fe3c38de000f0287eb44023067ab6c)\n[//]: # (START_SECTION cd6fb18aa0d9c911a71e02a430555f4fb27a9bfd)\n### Added a unit test to validate that JSON over HTTP access to Kamailio RPC Commands is working correctly\n\n> Commit: [cd6fb18aa0d9c911a71e02a430555f4fb27a9bfd](https://github.com/dOpensource/dsiprouter/commit/cd6fb18aa0d9c911a71e02a430555f4fb27a9bfd)  \n> Date: Sat, 23 Mar 2019 12:01:01 +0000  \n> Author: root (root@dSIPRouterMackDev-0.localdomain)  \n> Committer: root (root@dSIPRouterMackDev-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cd6fb18aa0d9c911a71e02a430555f4fb27a9bfd)\n[//]: # (START_SECTION 756b12c6da7d180b2b09d9be3f8873bc93774366)\n### Added supported jsonrpc over http on tcp port 5060\n\n> Commit: [756b12c6da7d180b2b09d9be3f8873bc93774366](https://github.com/dOpensource/dsiprouter/commit/756b12c6da7d180b2b09d9be3f8873bc93774366)  \n> Date: Sat, 23 Mar 2019 03:48:17 +0000  \n> Author: root (root@dSIPRouterMackDev-0.localdomain)  \n> Committer: root (root@dSIPRouterMackDev-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 756b12c6da7d180b2b09d9be3f8873bc93774366)\n[//]: # (START_SECTION 1c534efabbce6c81be33b28a2fce4bab307fee70)\n### Moved the creation of the LCR schema to the main install script and deprecated the LCR module\n\n> Commit: [1c534efabbce6c81be33b28a2fce4bab307fee70](https://github.com/dOpensource/dsiprouter/commit/1c534efabbce6c81be33b28a2fce4bab307fee70)  \n> Date: Sat, 23 Mar 2019 00:10:40 +0000  \n> Author: root (root@dSIPRouterMackDev-0.localdomain)  \n> Committer: root (root@dSIPRouterMackDev-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1c534efabbce6c81be33b28a2fce4bab307fee70)\n[//]: # (START_SECTION fb3226afbb6a91e96b04670d362cc18192fa70d4)\n### Fixed a regression with the gateway list import\n\n> Commit: [fb3226afbb6a91e96b04670d362cc18192fa70d4](https://github.com/dOpensource/dsiprouter/commit/fb3226afbb6a91e96b04670d362cc18192fa70d4)  \n> Date: Fri, 22 Mar 2019 23:36:42 +0000  \n> Author: root (root@dSIPRouterMackDev-0.localdomain)  \n> Committer: root (root@dSIPRouterMackDev-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fb3226afbb6a91e96b04670d362cc18192fa70d4)\n[//]: # (START_SECTION 93a1ed46a904cf3f15fb12cc75fe4cf8c3d8ecf9)\n### Fixed a regression with dr_gw_lists not being copied over to the /tmp/defaults directory\n\n> Commit: [93a1ed46a904cf3f15fb12cc75fe4cf8c3d8ecf9](https://github.com/dOpensource/dsiprouter/commit/93a1ed46a904cf3f15fb12cc75fe4cf8c3d8ecf9)  \n> Date: Fri, 22 Mar 2019 22:47:42 +0000  \n> Author: root (root@dSIPRouterMackDev-0.localdomain)  \n> Committer: root (root@dSIPRouterMackDev-0.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 93a1ed46a904cf3f15fb12cc75fe4cf8c3d8ecf9)\n[//]: # (START_SECTION 259f64c4abff05757940c5cef61e152f7296d451)\n### dSIPRouter Installation Overhaul\n\n> Commit: [259f64c4abff05757940c5cef61e152f7296d451](https://github.com/dOpensource/dsiprouter/commit/259f64c4abff05757940c5cef61e152f7296d451)  \n> Date: Thu, 21 Mar 2019 12:31:35 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #42\n- Resolves #103\n- add localhost to bind addresses for testing\n- wrap LCR routing in #!ifdef WITH_LCR\n- fix RTPEngine service startup issue\n- update rtpengine service file\n- update dsiprouter service file\n- add debian support for rtpengine systemd service\n- add debian support for kernel packet forwarding\n- fix non-root user kernel packet forwarding support\n- make rtpengine service namespace cross platform compat\n- make centos mariadb service namespace alias to mysql.service\n- fix tests for reg, auth, and DOS\n- create service check tests\n- update test formatting to be cleaner\n- update tests documentation\n- update test Makefile to sort test execution\n- fix debian AMI instable repo lists\n- make getExternalIP function match logic from shared.py\n- create structure for systemd startup dependencies\n- add dsip-init systemd resource\n- fix AMI image creation service startup issues\n- add detailed debugging options in dsiprouter.sh\n- add colored output and cleanup script output\n- fix python dependency removal order in uninstall funcs\n- add dependency installation for sipsak\n- finish separating service install logic to independent functions\n- update install/uninstall options to allow for independent installs\n- improve path check logic to avoid duplicates\n- fix dr_gw_lists import regression (path issue)\n- change logo color (no orange in 8-bit so we use cyan now)\n\n\n---\n\n[//]: # (END_SECTION 259f64c4abff05757940c5cef61e152f7296d451)\n[//]: # (START_SECTION a3df4ce4e01a93797428ecaa5ffd14e8ccfe6d4f)\n### Allow Domain Editing\n\n> Commit: [a3df4ce4e01a93797428ecaa5ffd14e8ccfe6d4f](https://github.com/dOpensource/dsiprouter/commit/a3df4ce4e01a93797428ecaa5ffd14e8ccfe6d4f)  \n> Date: Mon, 18 Mar 2019 18:21:32 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #87\n- simplify domain routing\n- allow editing in domain route\n- update pre commit script\n\n\n---\n\n[//]: # (END_SECTION a3df4ce4e01a93797428ecaa5ffd14e8ccfe6d4f)\n[//]: # (START_SECTION a87041465ba90930947920b9075673ec7884e0cf)\n### Update kamailio51_dsiprouter.tpl\n\n> Commit: [a87041465ba90930947920b9075673ec7884e0cf](https://github.com/dOpensource/dsiprouter/commit/a87041465ba90930947920b9075673ec7884e0cf)  \n> Date: Mon, 18 Mar 2019 11:57:38 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a87041465ba90930947920b9075673ec7884e0cf)\n[//]: # (START_SECTION a2af37311be5ad693a7d76f859f157095eaa6e4b)\n### Fix for Google Cloud Mysql\n\n> Commit: [a2af37311be5ad693a7d76f859f157095eaa6e4b](https://github.com/dOpensource/dsiprouter/commit/a2af37311be5ad693a7d76f859f157095eaa6e4b)  \n> Date: Fri, 15 Mar 2019 16:15:22 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #47\n- add defaults for carrier form\n- remove uneeded DB drivers\n\n\n---\n\n[//]: # (END_SECTION a2af37311be5ad693a7d76f859f157095eaa6e4b)\n[//]: # (START_SECTION eb706a5a3ac8bf004d015a9de54ada8f7063a3fb)\n### Fix Regressions\n\n> Commit: [eb706a5a3ac8bf004d015a9de54ada8f7063a3fb](https://github.com/dOpensource/dsiprouter/commit/eb706a5a3ac8bf004d015a9de54ada8f7063a3fb)  \n> Date: Thu, 14 Mar 2019 21:55:41 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- re-add configureKamailio command to install\n- fix DSIP_KAMAILIO_CONFIG_FILE path\n- remove uneeded kam code from hotfix\n\n\n---\n\n[//]: # (END_SECTION eb706a5a3ac8bf004d015a9de54ada8f7063a3fb)\n[//]: # (START_SECTION f42eb0e6477757d050c07be2adec952672eaa083)\n### Fix DID Notes DB Update\n\n> Commit: [f42eb0e6477757d050c07be2adec952672eaa083](https://github.com/dOpensource/dsiprouter/commit/f42eb0e6477757d050c07be2adec952672eaa083)  \n> Date: Thu, 14 Mar 2019 21:27:03 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #123\n- set form defaults on db update for inbound did route\n\n\n---\n\n[//]: # (END_SECTION f42eb0e6477757d050c07be2adec952672eaa083)\n[//]: # (START_SECTION f22b141b1ad277f14c0e3dcad9506275a024d985)\n### General Updates Cleanup Repo\n\n> Commit: [f22b141b1ad277f14c0e3dcad9506275a024d985](https://github.com/dOpensource/dsiprouter/commit/f22b141b1ad277f14c0e3dcad9506275a024d985)  \n> Date: Thu, 14 Mar 2019 10:42:40 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- move rtpengine install to seperate dir\n- update git resources\n- fix merging issues with modules\n- seperate kamailio install function and logic\n- add printing functions / colors to install\n- update requirements.txt install for stability\n- add mysql imports for pipreqs pre-commit updates\n- update module sql merging\n\n\n---\n\n[//]: # (END_SECTION f22b141b1ad277f14c0e3dcad9506275a024d985)\n[//]: # (START_SECTION 58cd4786f055db39c15980bf0574f3415e49bd4f)\n### Tighten Install for Release\n\n> Commit: [58cd4786f055db39c15980bf0574f3415e49bd4f](https://github.com/dOpensource/dsiprouter/commit/58cd4786f055db39c15980bf0574f3415e49bd4f)  \n> Date: Wed, 13 Mar 2019 10:24:40 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- fix cluster resource waiting stability\n- fix intermittent route editing issue\n- add centos support for galera replication\n- fix centos plugin auth issues\n\n\n---\n\n[//]: # (END_SECTION 58cd4786f055db39c15980bf0574f3415e49bd4f)\n[//]: # (START_SECTION 880278affeb5729cb5f860903337bca3e53eb5f7)\n### Added support for emergency numbers 911-999 Fixes: #121\n\n> Commit: [880278affeb5729cb5f860903337bca3e53eb5f7](https://github.com/dOpensource/dsiprouter/commit/880278affeb5729cb5f860903337bca3e53eb5f7)  \n> Date: Sun, 10 Mar 2019 23:06:11 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 880278affeb5729cb5f860903337bca3e53eb5f7)\n[//]: # (START_SECTION 17b35aea596261032c6183327689184e52515363)\n### LCR Dynamic Prefix Routing\n\n> Commit: [17b35aea596261032c6183327689184e52515363](https://github.com/dOpensource/dsiprouter/commit/17b35aea596261032c6183327689184e52515363)  \n> Date: Fri, 8 Mar 2019 18:07:06 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- Resolves #122\n- add dynamic routing for LCR module (similar to dRouting matches)\n- make LCR prefix length configurable in kam config\n- update both kamailio template and config files\n- general cleanup on kam configs\n- update internal IP resolution\n- update PATH resolution (fix logic bug)\n- fix dsiprouter logrotate path\n\n\n---\n\n[//]: # (END_SECTION 17b35aea596261032c6183327689184e52515363)\n[//]: # (START_SECTION eb82e5ab73697559cdaa5359ce2eb664d2fabe14)\n### Add 2 new HA Features\n\n> Commit: [eb82e5ab73697559cdaa5359ce2eb664d2fabe14](https://github.com/dOpensource/dsiprouter/commit/eb82e5ab73697559cdaa5359ce2eb664d2fabe14)  \n> Date: Fri, 8 Mar 2019 10:14:31 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- mysql galera replication\n- pacemaker / corosync cluster with floating ip\n\n\n---\n\n[//]: # (END_SECTION eb82e5ab73697559cdaa5359ce2eb664d2fabe14)\n[//]: # (START_SECTION e55ab3e4eb8ea9a798c761bdf31e3428759cce2e)\n### Make Project root more reliable\n\n> Commit: [e55ab3e4eb8ea9a798c761bdf31e3428759cce2e](https://github.com/dOpensource/dsiprouter/commit/e55ab3e4eb8ea9a798c761bdf31e3428759cce2e)  \n> Date: Wed, 6 Mar 2019 16:05:08 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add redundancy checking for project root dir\n\n\n---\n\n[//]: # (END_SECTION e55ab3e4eb8ea9a798c761bdf31e3428759cce2e)\n[//]: # (START_SECTION d887aa56a5c40c44f67735af585696fdc624a6f5)\n### Update Internal IP Resolution\n\n> Commit: [d887aa56a5c40c44f67735af585696fdc624a6f5](https://github.com/dOpensource/dsiprouter/commit/d887aa56a5c40c44f67735af585696fdc624a6f5)  \n> Date: Tue, 5 Mar 2019 23:19:55 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- changed internal ip resolution based on default route\n- fix rtpengine config update function\n- add rtpcfg variable for later use\n\n\n---\n\n[//]: # (END_SECTION d887aa56a5c40c44f67735af585696fdc624a6f5)\n[//]: # (START_SECTION d87203edbf363e7e64eb4af1adfc1403d7e93bd9)\n### Fix kamailio configure Bugs\n\n> Commit: [d87203edbf363e7e64eb4af1adfc1403d7e93bd9](https://github.com/dOpensource/dsiprouter/commit/d87203edbf363e7e64eb4af1adfc1403d7e93bd9)  \n> Date: Tue, 5 Mar 2019 19:25:15 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- drop dr_custom_rules on fresh kam configure\n- remove hung locks when adding user\n\n\n---\n\n[//]: # (END_SECTION d87203edbf363e7e64eb4af1adfc1403d7e93bd9)\n[//]: # (START_SECTION f90689cc8579169b4705ba603dad1cda5b427ced)\n### Bug Fixes\n\n> Commit: [f90689cc8579169b4705ba603dad1cda5b427ced](https://github.com/dOpensource/dsiprouter/commit/f90689cc8579169b4705ba603dad1cda5b427ced)  \n> Date: Tue, 5 Mar 2019 16:02:37 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed: Tyler Moore (devopsec) <tmoore@goflyball.com>  \n\n\n- add curl timeout on AWS check\n- make PBX local digit length check globalls configurable\n- fix typos\n- fix line breaks\n- automate merging table data during install\n\n\n---\n\n[//]: # (END_SECTION f90689cc8579169b4705ba603dad1cda5b427ced)\n[//]: # (START_SECTION 2014b89f5c3911f4b37bdde167e713217fe9ebd9)\n### Add Mysql Replication Scripts\n\n> Commit: [2014b89f5c3911f4b37bdde167e713217fe9ebd9](https://github.com/dOpensource/dsiprouter/commit/2014b89f5c3911f4b37bdde167e713217fe9ebd9)  \n> Date: Tue, 26 Feb 2019 11:58:06 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- support for group replication\n- support for galera replication\n- mysql size check script added\n\n\n---\n\n[//]: # (END_SECTION 2014b89f5c3911f4b37bdde167e713217fe9ebd9)\n[//]: # (START_SECTION de65083142747932c726e75b451791b50eae56c8)\n### Update kamailio51_dsiprouter.tpl\n\n> Commit: [de65083142747932c726e75b451791b50eae56c8](https://github.com/dOpensource/dsiprouter/commit/de65083142747932c726e75b451791b50eae56c8)  \n> Date: Thu, 21 Feb 2019 16:43:09 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Set the domain flag: register_myself to 0.  This flag was causing Kamailio to get stuck in a continuous loop  when receiving an ACK from an endpoint.  This is due to the fact that Kamailio sees the domains in the domains table reside on the Kamailio server with the register_myself flag being set to 1\n\n\n---\n\n[//]: # (END_SECTION de65083142747932c726e75b451791b50eae56c8)\n[//]: # (START_SECTION b22bf3ddef855c9ccd95430cbfaae3099d44540a)\n### Add Useful Scripts To Resources\n\n> Commit: [b22bf3ddef855c9ccd95430cbfaae3099d44540a](https://github.com/dOpensource/dsiprouter/commit/b22bf3ddef855c9ccd95430cbfaae3099d44540a)  \n> Date: Wed, 20 Feb 2019 15:12:14 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- add changelog merging script\n- add some python testing scripts\n\n\n---\n\n[//]: # (END_SECTION b22bf3ddef855c9ccd95430cbfaae3099d44540a)\n[//]: # (START_SECTION 1648791889665b29540d093f2cc38f4ef6a87ff9)\n### Update RTPengine On Reload and Install Fixes\n\n> Commit: [1648791889665b29540d093f2cc38f4ef6a87ff9](https://github.com/dOpensource/dsiprouter/commit/1648791889665b29540d093f2cc38f4ef6a87ff9)  \n> Date: Tue, 19 Feb 2019 10:50:46 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Resolves #115\n- update rtpengine config on reboot\n- fix misc issues with install script\n- fix adding user issue\n- update exception function\n\n\n---\n\n[//]: # (END_SECTION 1648791889665b29540d093f2cc38f4ef6a87ff9)\n[//]: # (START_SECTION fc0dc02dde3ff3dcdd62d95edcdfeb44e2348f4a)\n### Initial commit\n\n> Commit: [fc0dc02dde3ff3dcdd62d95edcdfeb44e2348f4a](https://github.com/dOpensource/dsiprouter/commit/fc0dc02dde3ff3dcdd62d95edcdfeb44e2348f4a)  \n> Date: Fri, 15 Feb 2019 16:31:20 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fc0dc02dde3ff3dcdd62d95edcdfeb44e2348f4a)\n[//]: # (START_SECTION ee97002db899a95857307e328775d2db7064e399)\n### Update use-cases.rst\n\n> Commit: [ee97002db899a95857307e328775d2db7064e399](https://github.com/dOpensource/dsiprouter/commit/ee97002db899a95857307e328775d2db7064e399)  \n> Date: Thu, 14 Feb 2019 09:55:09 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ee97002db899a95857307e328775d2db7064e399)\n[//]: # (START_SECTION 414109b4d53ef973361d5ceaefade981d1b43eed)\n### Update use-cases.rst\n\n> Commit: [414109b4d53ef973361d5ceaefade981d1b43eed](https://github.com/dOpensource/dsiprouter/commit/414109b4d53ef973361d5ceaefade981d1b43eed)  \n> Date: Wed, 13 Feb 2019 17:48:32 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 414109b4d53ef973361d5ceaefade981d1b43eed)\n[//]: # (START_SECTION 763485c1819485fd5455cd53814a7a5293870a2f)\n### Add files via upload\n\n> Commit: [763485c1819485fd5455cd53814a7a5293870a2f](https://github.com/dOpensource/dsiprouter/commit/763485c1819485fd5455cd53814a7a5293870a2f)  \n> Date: Wed, 13 Feb 2019 17:42:30 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 763485c1819485fd5455cd53814a7a5293870a2f)\n[//]: # (START_SECTION b0380ce7373a8053058e1dcc3bc436e241bc3717)\n### Update use-cases.rst\n\n> Commit: [b0380ce7373a8053058e1dcc3bc436e241bc3717](https://github.com/dOpensource/dsiprouter/commit/b0380ce7373a8053058e1dcc3bc436e241bc3717)  \n> Date: Wed, 13 Feb 2019 17:37:18 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b0380ce7373a8053058e1dcc3bc436e241bc3717)\n[//]: # (START_SECTION 6bcb68bb0c651266c30f0e9962f2eec80c771b5c)\n### Update use-cases.rst\n\n> Commit: [6bcb68bb0c651266c30f0e9962f2eec80c771b5c](https://github.com/dOpensource/dsiprouter/commit/6bcb68bb0c651266c30f0e9962f2eec80c771b5c)  \n> Date: Wed, 13 Feb 2019 15:25:06 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6bcb68bb0c651266c30f0e9962f2eec80c771b5c)\n[//]: # (START_SECTION b40709212a5dc2820ae9b737a584f291bdd29669)\n### Update use-cases.rst\n\n> Commit: [b40709212a5dc2820ae9b737a584f291bdd29669](https://github.com/dOpensource/dsiprouter/commit/b40709212a5dc2820ae9b737a584f291bdd29669)  \n> Date: Wed, 13 Feb 2019 15:23:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b40709212a5dc2820ae9b737a584f291bdd29669)\n[//]: # (START_SECTION 2dc2a5bd7646b376e9cad23f4f5db917d4068adc)\n### Update ngcp-rtpengine-daemon.init\n\n> Commit: [2dc2a5bd7646b376e9cad23f4f5db917d4068adc](https://github.com/dOpensource/dsiprouter/commit/2dc2a5bd7646b376e9cad23f4f5db917d4068adc)  \n> Date: Wed, 13 Feb 2019 13:41:59 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- Fixed an issue with a redirect\n\n\n---\n\n[//]: # (END_SECTION 2dc2a5bd7646b376e9cad23f4f5db917d4068adc)\n[//]: # (START_SECTION 08e71702a2601e75310cebcb95faa5194ba549d3)\n### Fix Bugs in GUI\n\n> Commit: [08e71702a2601e75310cebcb95faa5194ba549d3](https://github.com/dOpensource/dsiprouter/commit/08e71702a2601e75310cebcb95faa5194ba549d3)  \n> Date: Mon, 11 Feb 2019 17:28:28 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fix datatable auto width resolution issue\n- fix db connection issue\n- add dsiprouter flag definitions\n\n\n---\n\n[//]: # (END_SECTION 08e71702a2601e75310cebcb95faa5194ba549d3)\n[//]: # (START_SECTION 171513f9e25c57b52492b481999681db7e0997ba)\n### Update use-cases.rst\n\n> Commit: [171513f9e25c57b52492b481999681db7e0997ba](https://github.com/dOpensource/dsiprouter/commit/171513f9e25c57b52492b481999681db7e0997ba)  \n> Date: Fri, 8 Feb 2019 23:17:40 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 171513f9e25c57b52492b481999681db7e0997ba)\n[//]: # (START_SECTION a14098100a655d494e48b4e847bb70f4d13d6476)\n### Update use-cases.rst\n\n> Commit: [a14098100a655d494e48b4e847bb70f4d13d6476](https://github.com/dOpensource/dsiprouter/commit/a14098100a655d494e48b4e847bb70f4d13d6476)  \n> Date: Fri, 8 Feb 2019 23:09:57 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a14098100a655d494e48b4e847bb70f4d13d6476)\n[//]: # (START_SECTION efd9e88fcf3e19f1ef43ff5879d9c742572b1a47)\n### Add files via upload\n\n> Commit: [efd9e88fcf3e19f1ef43ff5879d9c742572b1a47](https://github.com/dOpensource/dsiprouter/commit/efd9e88fcf3e19f1ef43ff5879d9c742572b1a47)  \n> Date: Fri, 8 Feb 2019 23:03:28 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION efd9e88fcf3e19f1ef43ff5879d9c742572b1a47)\n[//]: # (START_SECTION b60953ebe93c0577e2ee2ea292301ecf0a090aee)\n### Update use-cases.rst\n\n> Commit: [b60953ebe93c0577e2ee2ea292301ecf0a090aee](https://github.com/dOpensource/dsiprouter/commit/b60953ebe93c0577e2ee2ea292301ecf0a090aee)  \n> Date: Fri, 8 Feb 2019 23:01:39 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b60953ebe93c0577e2ee2ea292301ecf0a090aee)\n[//]: # (START_SECTION c3faeda237dc394ece62a71cf701930ff5ff6e9f)\n### Update use-cases.rst\n\n> Commit: [c3faeda237dc394ece62a71cf701930ff5ff6e9f](https://github.com/dOpensource/dsiprouter/commit/c3faeda237dc394ece62a71cf701930ff5ff6e9f)  \n> Date: Fri, 8 Feb 2019 22:31:24 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c3faeda237dc394ece62a71cf701930ff5ff6e9f)\n[//]: # (START_SECTION 8be2229d5c810a3152573c1a5c5d0c6f93021eb2)\n### Update use-cases.rst\n\n> Commit: [8be2229d5c810a3152573c1a5c5d0c6f93021eb2](https://github.com/dOpensource/dsiprouter/commit/8be2229d5c810a3152573c1a5c5d0c6f93021eb2)  \n> Date: Fri, 8 Feb 2019 22:22:15 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8be2229d5c810a3152573c1a5c5d0c6f93021eb2)\n[//]: # (START_SECTION 7f5bd28b10d793afb822d01fa2ed8bca6f78b697)\n### Update use-cases.rst\n\n> Commit: [7f5bd28b10d793afb822d01fa2ed8bca6f78b697](https://github.com/dOpensource/dsiprouter/commit/7f5bd28b10d793afb822d01fa2ed8bca6f78b697)  \n> Date: Fri, 8 Feb 2019 22:20:05 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7f5bd28b10d793afb822d01fa2ed8bca6f78b697)\n[//]: # (START_SECTION c92d4688407e0173c8e6ad66369a8f15e8400587)\n### Update use-cases.rst\n\n> Commit: [c92d4688407e0173c8e6ad66369a8f15e8400587](https://github.com/dOpensource/dsiprouter/commit/c92d4688407e0173c8e6ad66369a8f15e8400587)  \n> Date: Fri, 8 Feb 2019 22:18:12 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c92d4688407e0173c8e6ad66369a8f15e8400587)\n[//]: # (START_SECTION 870d44939d3046d8b79f0bd59f5e6d1cda31e97f)\n### Update use-cases.rst\n\n> Commit: [870d44939d3046d8b79f0bd59f5e6d1cda31e97f](https://github.com/dOpensource/dsiprouter/commit/870d44939d3046d8b79f0bd59f5e6d1cda31e97f)  \n> Date: Fri, 8 Feb 2019 14:47:59 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 870d44939d3046d8b79f0bd59f5e6d1cda31e97f)\n[//]: # (START_SECTION 9b3e2cbbd9010213a9e2205408594031807d3eb5)\n### Inbound DID and Fail2Ban Update\n\n> Commit: [9b3e2cbbd9010213a9e2205408594031807d3eb5](https://github.com/dOpensource/dsiprouter/commit/9b3e2cbbd9010213a9e2205408594031807d3eb5)  \n> Date: Thu, 7 Feb 2019 22:31:55 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Resolves #100\n- Resolves #54\n- add support for secondary pbx inbound route\n- DID failover is supported by adding another rule\n- add fail2ban instructions to domain and pbx pages\n- small syntax fixes\n- update combobox and fix issues\n- update inboundroutes routing and DB model\n\n\n---\n\n[//]: # (END_SECTION 9b3e2cbbd9010213a9e2205408594031807d3eb5)\n[//]: # (START_SECTION 8111636d42d431e521ffbcc8511f61cacef3be00)\n### Update use-cases.rst\n\n> Commit: [8111636d42d431e521ffbcc8511f61cacef3be00](https://github.com/dOpensource/dsiprouter/commit/8111636d42d431e521ffbcc8511f61cacef3be00)  \n> Date: Thu, 7 Feb 2019 16:02:44 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8111636d42d431e521ffbcc8511f61cacef3be00)\n[//]: # (START_SECTION 9247eaefdfee193b8766945c6cd2377d48637c2a)\n### Update use-cases.rst\n\n> Commit: [9247eaefdfee193b8766945c6cd2377d48637c2a](https://github.com/dOpensource/dsiprouter/commit/9247eaefdfee193b8766945c6cd2377d48637c2a)  \n> Date: Thu, 7 Feb 2019 15:24:47 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9247eaefdfee193b8766945c6cd2377d48637c2a)\n[//]: # (START_SECTION cdbf7233f95ebf0034c664574e5b4afe45209567)\n### AMI Provisioning Fixes\n\n> Commit: [cdbf7233f95ebf0034c664574e5b4afe45209567](https://github.com/dOpensource/dsiprouter/commit/cdbf7233f95ebf0034c664574e5b4afe45209567)  \n> Date: Thu, 7 Feb 2019 14:30:28 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fix PATH on install\n- dynamic kam config update\n- add updatekamconfig cli option\n- fix for debian 8 debhelper issue\n- fix kamailio and rtpengine user creation\n- fix rtpengine default conf file location\n- fix firewalld centos ami issue\n- fix merge issues (install,installSipsak)\n- minor improvements to syslog handler\n\n\n---\n\n[//]: # (END_SECTION cdbf7233f95ebf0034c664574e5b4afe45209567)\n[//]: # (START_SECTION 7c7bd555e3dc8e056f90de9b2175959b3443f482)\n### Update command_line_options.rst\n\n> Commit: [7c7bd555e3dc8e056f90de9b2175959b3443f482](https://github.com/dOpensource/dsiprouter/commit/7c7bd555e3dc8e056f90de9b2175959b3443f482)  \n> Date: Thu, 7 Feb 2019 10:01:48 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7c7bd555e3dc8e056f90de9b2175959b3443f482)\n[//]: # (START_SECTION 993a7605687b42aa7c36987e121b70b3e6c6afd3)\n### Adds the ability to change the name of the server presented to clients\n\n> Commit: [993a7605687b42aa7c36987e121b70b3e6c6afd3](https://github.com/dOpensource/dsiprouter/commit/993a7605687b42aa7c36987e121b70b3e6c6afd3)  \n> Date: Wed, 6 Feb 2019 21:28:15 -0700  \n> Author: matmurdock (mat.murdock@gmail.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 993a7605687b42aa7c36987e121b70b3e6c6afd3)\n[//]: # (START_SECTION 83d90093a9f5e63379de1bcca145c51bf8618483)\n### Fixed firewall issues\n\n> Commit: [83d90093a9f5e63379de1bcca145c51bf8618483](https://github.com/dOpensource/dsiprouter/commit/83d90093a9f5e63379de1bcca145c51bf8618483)  \n> Date: Thu, 7 Feb 2019 01:18:41 +0000  \n> Author: root (root@ip-172-31-11-14.us-east-2.compute.internal)  \n> Committer: root (root@ip-172-31-11-14.us-east-2.compute.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 83d90093a9f5e63379de1bcca145c51bf8618483)\n[//]: # (START_SECTION 3534c6ea4bdd91ece5dbc71b85155bf07f9e4cdd)\n### Changed order that firewalld rules are being added.  This is workaround for cloud-init\n\n> Commit: [3534c6ea4bdd91ece5dbc71b85155bf07f9e4cdd](https://github.com/dOpensource/dsiprouter/commit/3534c6ea4bdd91ece5dbc71b85155bf07f9e4cdd)  \n> Date: Thu, 7 Feb 2019 00:31:15 +0000  \n> Author: root (root@ip-172-31-31-55.us-east-2.compute.internal)  \n> Committer: root (root@ip-172-31-31-55.us-east-2.compute.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3534c6ea4bdd91ece5dbc71b85155bf07f9e4cdd)\n[//]: # (START_SECTION 396c062ec0629ed16a30c714463422979ad83202)\n### Added fix to the centos 7 kamailio install so that firewall rules can be added\n\n> Commit: [396c062ec0629ed16a30c714463422979ad83202](https://github.com/dOpensource/dsiprouter/commit/396c062ec0629ed16a30c714463422979ad83202)  \n> Date: Wed, 6 Feb 2019 23:33:58 +0000  \n> Author: root (root@ip-172-31-38-36.us-east-2.compute.internal)  \n> Committer: root (root@ip-172-31-38-36.us-east-2.compute.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 396c062ec0629ed16a30c714463422979ad83202)\n[//]: # (START_SECTION f4008680a09bb4faeb340deaa9b81cdd09ec7216)\n### Inbound DID Mapping Sort By Name\n\n> Commit: [f4008680a09bb4faeb340deaa9b81cdd09ec7216](https://github.com/dOpensource/dsiprouter/commit/f4008680a09bb4faeb340deaa9b81cdd09ec7216)  \n> Date: Wed, 6 Feb 2019 17:36:14 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Resolves #76\n- sort pbx select list on pbx name\n- enable combobox for imported did's\n- fix autoselect on add / import did modal\n\n\n---\n\n[//]: # (END_SECTION f4008680a09bb4faeb340deaa9b81cdd09ec7216)\n[//]: # (START_SECTION 56b3c8974e36565744ffdb592fed64811bbae82d)\n### Remove Carrier From gwlist On Delete\n\n> Commit: [56b3c8974e36565744ffdb592fed64811bbae82d](https://github.com/dOpensource/dsiprouter/commit/56b3c8974e36565744ffdb592fed64811bbae82d)  \n> Date: Wed, 6 Feb 2019 15:17:34 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Resolves #7\n- carriers removed from all related dr_rules gwlists on delete\n- create alert and warn user that related rules will be updated\n\n\n---\n\n[//]: # (END_SECTION 56b3c8974e36565744ffdb592fed64811bbae82d)\n[//]: # (START_SECTION c0c3444708304739612bab676095883c823ef96b)\n### Fix Carrier Modal Actions\n\n> Commit: [c0c3444708304739612bab676095883c823ef96b](https://github.com/dOpensource/dsiprouter/commit/c0c3444708304739612bab676095883c823ef96b)  \n> Date: Tue, 5 Feb 2019 12:28:13 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Resolves #96\n- replace data-tables ver w/ standalone library\n- rename imports\n\n\n---\n\n[//]: # (END_SECTION c0c3444708304739612bab676095883c823ef96b)\n[//]: # (START_SECTION 755e1e7bd6bd44f358588a248e038826672944a9)\n### Update use-cases.rst\n\n> Commit: [755e1e7bd6bd44f358588a248e038826672944a9](https://github.com/dOpensource/dsiprouter/commit/755e1e7bd6bd44f358588a248e038826672944a9)  \n> Date: Wed, 6 Feb 2019 10:41:58 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 755e1e7bd6bd44f358588a248e038826672944a9)\n[//]: # (START_SECTION 5ac8de10cb3f1fef840d9d2acbe0ecce293712ee)\n### Fixed a regression that caused the password not to be set correct when installed on a non-AMI\n\n> Commit: [5ac8de10cb3f1fef840d9d2acbe0ecce293712ee](https://github.com/dOpensource/dsiprouter/commit/5ac8de10cb3f1fef840d9d2acbe0ecce293712ee)  \n> Date: Tue, 5 Feb 2019 19:30:53 +0000  \n> Author: root (root@dSIPRouterMackAMI.localdomain)  \n> Committer: root (root@dSIPRouterMackAMI.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5ac8de10cb3f1fef840d9d2acbe0ecce293712ee)\n[//]: # (START_SECTION 0c974de72660d7e8ebbcc3f7ce495199837bcd26)\n### Update use-cases.rst\n\n> Commit: [0c974de72660d7e8ebbcc3f7ce495199837bcd26](https://github.com/dOpensource/dsiprouter/commit/0c974de72660d7e8ebbcc3f7ce495199837bcd26)  \n> Date: Tue, 5 Feb 2019 10:23:59 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0c974de72660d7e8ebbcc3f7ce495199837bcd26)\n[//]: # (START_SECTION 87c55ef12898069dec92fe13ee704dfac649f33d)\n### Fixed testing scripts\n\n> Commit: [87c55ef12898069dec92fe13ee704dfac649f33d](https://github.com/dOpensource/dsiprouter/commit/87c55ef12898069dec92fe13ee704dfac649f33d)  \n> Date: Tue, 5 Feb 2019 06:49:27 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 87c55ef12898069dec92fe13ee704dfac649f33d)\n[//]: # (START_SECTION 6fe68a149f51a757953ca35bcf08591f8a349f67)\n### Added support for NOTIFY messages from PBX - which is used to update MWI\n\n> Commit: [6fe68a149f51a757953ca35bcf08591f8a349f67](https://github.com/dOpensource/dsiprouter/commit/6fe68a149f51a757953ca35bcf08591f8a349f67)  \n> Date: Mon, 4 Feb 2019 21:30:19 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6fe68a149f51a757953ca35bcf08591f8a349f67)\n[//]: # (START_SECTION cad9957616e80eb216a8269ad552c105da553861)\n### Update use-cases.rst\n\n> Commit: [cad9957616e80eb216a8269ad552c105da553861](https://github.com/dOpensource/dsiprouter/commit/cad9957616e80eb216a8269ad552c105da553861)  \n> Date: Mon, 4 Feb 2019 12:34:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cad9957616e80eb216a8269ad552c105da553861)\n[//]: # (START_SECTION 51ebf27bd815180fe84a7813b4f7160d63e09abd)\n### Update use-cases.rst\n\n> Commit: [51ebf27bd815180fe84a7813b4f7160d63e09abd](https://github.com/dOpensource/dsiprouter/commit/51ebf27bd815180fe84a7813b4f7160d63e09abd)  \n> Date: Mon, 4 Feb 2019 12:13:28 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 51ebf27bd815180fe84a7813b4f7160d63e09abd)\n[//]: # (START_SECTION 9ff4531b0227acd84946a6ebd4f40928036442b2)\n### Update use-cases.rst\n\n> Commit: [9ff4531b0227acd84946a6ebd4f40928036442b2](https://github.com/dOpensource/dsiprouter/commit/9ff4531b0227acd84946a6ebd4f40928036442b2)  \n> Date: Mon, 4 Feb 2019 12:09:52 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9ff4531b0227acd84946a6ebd4f40928036442b2)\n[//]: # (START_SECTION 045c47dc9118fb22a20579c908d071342d3be8ca)\n### Update use-cases.rst\n\n> Commit: [045c47dc9118fb22a20579c908d071342d3be8ca](https://github.com/dOpensource/dsiprouter/commit/045c47dc9118fb22a20579c908d071342d3be8ca)  \n> Date: Mon, 4 Feb 2019 12:01:54 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 045c47dc9118fb22a20579c908d071342d3be8ca)\n[//]: # (START_SECTION 1b24a410cd70e3c1a09130db33b66ecdc123524c)\n### Update use-cases.rst\n\n> Commit: [1b24a410cd70e3c1a09130db33b66ecdc123524c](https://github.com/dOpensource/dsiprouter/commit/1b24a410cd70e3c1a09130db33b66ecdc123524c)  \n> Date: Mon, 4 Feb 2019 11:44:21 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1b24a410cd70e3c1a09130db33b66ecdc123524c)\n[//]: # (START_SECTION 6312d09c44a26d0cd989ea283200bdf11bf985bd)\n### Update use-cases.rst\n\n> Commit: [6312d09c44a26d0cd989ea283200bdf11bf985bd](https://github.com/dOpensource/dsiprouter/commit/6312d09c44a26d0cd989ea283200bdf11bf985bd)  \n> Date: Mon, 4 Feb 2019 11:34:10 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6312d09c44a26d0cd989ea283200bdf11bf985bd)\n[//]: # (START_SECTION 936c72146ac85c3a490de4c3441084c4d403ae29)\n### Update use-cases.rst\n\n> Commit: [936c72146ac85c3a490de4c3441084c4d403ae29](https://github.com/dOpensource/dsiprouter/commit/936c72146ac85c3a490de4c3441084c4d403ae29)  \n> Date: Mon, 4 Feb 2019 11:31:59 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 936c72146ac85c3a490de4c3441084c4d403ae29)\n[//]: # (START_SECTION 3e8f145578cf290075c710ce46dcdcafabb88898)\n### Update use-cases.rst\n\n> Commit: [3e8f145578cf290075c710ce46dcdcafabb88898](https://github.com/dOpensource/dsiprouter/commit/3e8f145578cf290075c710ce46dcdcafabb88898)  \n> Date: Mon, 4 Feb 2019 11:29:01 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3e8f145578cf290075c710ce46dcdcafabb88898)\n[//]: # (START_SECTION e4618758b9f80847d459dd053b6c5b60a26bb580)\n### Update use-cases.rst\n\n> Commit: [e4618758b9f80847d459dd053b6c5b60a26bb580](https://github.com/dOpensource/dsiprouter/commit/e4618758b9f80847d459dd053b6c5b60a26bb580)  \n> Date: Mon, 4 Feb 2019 11:11:38 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e4618758b9f80847d459dd053b6c5b60a26bb580)\n[//]: # (START_SECTION 0842d2135ade287427a82842735f404d09b807dd)\n### Rename troubleshooting.rst.txt to troubleshooting.rst\n\n> Commit: [0842d2135ade287427a82842735f404d09b807dd](https://github.com/dOpensource/dsiprouter/commit/0842d2135ade287427a82842735f404d09b807dd)  \n> Date: Mon, 4 Feb 2019 10:27:07 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0842d2135ade287427a82842735f404d09b807dd)\n[//]: # (START_SECTION 73c0c257d966bae0c310dcd841fb37d997df9483)\n### Update troubleshooting.rst.txt\n\n> Commit: [73c0c257d966bae0c310dcd841fb37d997df9483](https://github.com/dOpensource/dsiprouter/commit/73c0c257d966bae0c310dcd841fb37d997df9483)  \n> Date: Mon, 4 Feb 2019 10:25:11 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 73c0c257d966bae0c310dcd841fb37d997df9483)\n[//]: # (START_SECTION dce6af8d9c49166c1619fc762874ef274fc36913)\n### Update troubleshooting.rst\n\n> Commit: [dce6af8d9c49166c1619fc762874ef274fc36913](https://github.com/dOpensource/dsiprouter/commit/dce6af8d9c49166c1619fc762874ef274fc36913)  \n> Date: Mon, 4 Feb 2019 09:45:14 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dce6af8d9c49166c1619fc762874ef274fc36913)\n[//]: # (START_SECTION 2442d0d467e47af0e62e1a55059a242b2789ff3c)\n### Rename troubleshooting.rst.txt to troubleshooting.rst\n\n> Commit: [2442d0d467e47af0e62e1a55059a242b2789ff3c](https://github.com/dOpensource/dsiprouter/commit/2442d0d467e47af0e62e1a55059a242b2789ff3c)  \n> Date: Mon, 4 Feb 2019 09:40:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2442d0d467e47af0e62e1a55059a242b2789ff3c)\n[//]: # (START_SECTION c45597efaa5fa9ad6a98deca8c1b9f4ba0c7b388)\n### Fixed the directory path that points to the rsyslog and logrotate settings\n\n> Commit: [c45597efaa5fa9ad6a98deca8c1b9f4ba0c7b388](https://github.com/dOpensource/dsiprouter/commit/c45597efaa5fa9ad6a98deca8c1b9f4ba0c7b388)  \n> Date: Mon, 4 Feb 2019 10:59:09 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c45597efaa5fa9ad6a98deca8c1b9f4ba0c7b388)\n[//]: # (START_SECTION f5e2d7155ff1f7d69f9d32fb02fc00c728ca0534)\n### Moved the logrotate and syslog to the resouces directory\n\n> Commit: [f5e2d7155ff1f7d69f9d32fb02fc00c728ca0534](https://github.com/dOpensource/dsiprouter/commit/f5e2d7155ff1f7d69f9d32fb02fc00c728ca0534)  \n> Date: Mon, 4 Feb 2019 10:05:36 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f5e2d7155ff1f7d69f9d32fb02fc00c728ca0534)\n[//]: # (START_SECTION 565bf63ac62792464caaeaf7fef7e12e6ee55424)\n### Unit test for testing Denial of Service (DoS) Attacks\n\n> Commit: [565bf63ac62792464caaeaf7fef7e12e6ee55424](https://github.com/dOpensource/dsiprouter/commit/565bf63ac62792464caaeaf7fef7e12e6ee55424)  \n> Date: Fri, 1 Feb 2019 11:37:20 +0000  \n> Author: root (root@dsiprouterMackMaster.localdomain)  \n> Committer: root (root@dsiprouterMackMaster.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 565bf63ac62792464caaeaf7fef7e12e6ee55424)\n[//]: # (START_SECTION e7a5f4f11da1b0219626e642c4183311b745ee3b)\n### Fixed the SQL script so that it works with the newer versions of MariaDB\n\n> Commit: [e7a5f4f11da1b0219626e642c4183311b745ee3b](https://github.com/dOpensource/dsiprouter/commit/e7a5f4f11da1b0219626e642c4183311b745ee3b)  \n> Date: Fri, 1 Feb 2019 11:31:56 +0000  \n> Author: root (root@dsiprouterMackMaster.localdomain)  \n> Committer: root (root@dsiprouterMackMaster.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e7a5f4f11da1b0219626e642c4183311b745ee3b)\n[//]: # (START_SECTION 8d5d0bb12e5cd91db0edf18448c5704b5639d4f8)\n### Fixed issue with enabling PIKE\n\n> Commit: [8d5d0bb12e5cd91db0edf18448c5704b5639d4f8](https://github.com/dOpensource/dsiprouter/commit/8d5d0bb12e5cd91db0edf18448c5704b5639d4f8)  \n> Date: Thu, 31 Jan 2019 17:39:16 +0000  \n> Author: root (root@dsiprouterMackMaster.localdomain)  \n> Committer: root (root@dsiprouterMackMaster.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8d5d0bb12e5cd91db0edf18448c5704b5639d4f8)\n[//]: # (START_SECTION 55f8ec2471c4df074ee237fe1ec5adbba32db24a)\n### Update README.md\n\n> Commit: [55f8ec2471c4df074ee237fe1ec5adbba32db24a](https://github.com/dOpensource/dsiprouter/commit/55f8ec2471c4df074ee237fe1ec5adbba32db24a)  \n> Date: Thu, 31 Jan 2019 12:29:28 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 55f8ec2471c4df074ee237fe1ec5adbba32db24a)\n[//]: # (START_SECTION 843b6e0db426bacbf21994b137d27fffdc13222d)\n### Update README.md\n\n> Commit: [843b6e0db426bacbf21994b137d27fffdc13222d](https://github.com/dOpensource/dsiprouter/commit/843b6e0db426bacbf21994b137d27fffdc13222d)  \n> Date: Thu, 31 Jan 2019 12:28:47 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 843b6e0db426bacbf21994b137d27fffdc13222d)\n[//]: # (START_SECTION f979796b0c2a29a5e77e6c72dd933cc922a3a610)\n### Moved the server_signature parameter\n\n> Commit: [f979796b0c2a29a5e77e6c72dd933cc922a3a610](https://github.com/dOpensource/dsiprouter/commit/f979796b0c2a29a5e77e6c72dd933cc922a3a610)  \n> Date: Thu, 31 Jan 2019 17:01:01 +0000  \n> Author: root (root@dsiprouterMackKamsec.localdomain)  \n> Committer: root (root@dsiprouterMackKamsec.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f979796b0c2a29a5e77e6c72dd933cc922a3a610)\n[//]: # (START_SECTION 0b0a6abd67a44d8f3a219e9794ca9fb5dd9c3679)\n### Added a record route before relaying to endpoints to ensure they route all traffic thru the proxy\n\n> Commit: [0b0a6abd67a44d8f3a219e9794ca9fb5dd9c3679](https://github.com/dOpensource/dsiprouter/commit/0b0a6abd67a44d8f3a219e9794ca9fb5dd9c3679)  \n> Date: Thu, 31 Jan 2019 10:36:18 +0000  \n> Author: root (root@dsiprouterMackMaster.localdomain)  \n> Committer: root (root@dsiprouterMackMaster.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0b0a6abd67a44d8f3a219e9794ca9fb5dd9c3679)\n[//]: # (START_SECTION 34e4ff3b7f27b17a0e7b6c0cdbb53bd68e012ea1)\n### Added commit [776f17bd9ba1cb7a623803a4bc3f54e6d5954565](https://github.com/dOpensource/dsiprouter/commit/776f17bd9ba1cb7a623803a4bc3f54e6d5954565) by MatMurdock into the template file\n\n> Commit: [34e4ff3b7f27b17a0e7b6c0cdbb53bd68e012ea1](https://github.com/dOpensource/dsiprouter/commit/34e4ff3b7f27b17a0e7b6c0cdbb53bd68e012ea1)  \n> Date: Thu, 31 Jan 2019 10:15:53 +0000  \n> Author: root (root@dsiprouterMackMaster.localdomain)  \n> Committer: root (root@dsiprouterMackMaster.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 34e4ff3b7f27b17a0e7b6c0cdbb53bd68e012ea1)\n[//]: # (START_SECTION eddbd60d7351d594c8f56e46a09f7e878424d9eb)\n### Fixed an issue with the initial startup of RTPEngine\n\n> Commit: [eddbd60d7351d594c8f56e46a09f7e878424d9eb](https://github.com/dOpensource/dsiprouter/commit/eddbd60d7351d594c8f56e46a09f7e878424d9eb)  \n> Date: Thu, 31 Jan 2019 09:54:58 +0000  \n> Author: root (root@dsiprouterMackMaster.localdomain)  \n> Committer: root (root@dsiprouterMackMaster.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eddbd60d7351d594c8f56e46a09f7e878424d9eb)\n[//]: # (START_SECTION 8336f450225b1088a02a40c62c6de74818681040)\n### Fixed an issue with dsiprouter.sh running commands in the wrong directory.\n\n> Commit: [8336f450225b1088a02a40c62c6de74818681040](https://github.com/dOpensource/dsiprouter/commit/8336f450225b1088a02a40c62c6de74818681040)  \n> Date: Thu, 31 Jan 2019 08:55:10 +0000  \n> Author: root (root@dsiprouterMackMaster.localdomain)  \n> Committer: root (root@dsiprouterMackMaster.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8336f450225b1088a02a40c62c6de74818681040)\n[//]: # (START_SECTION ed0782b28b6c89069bd919709e1cf57222bc734e)\n### Removed set -x\n\n> Commit: [ed0782b28b6c89069bd919709e1cf57222bc734e](https://github.com/dOpensource/dsiprouter/commit/ed0782b28b6c89069bd919709e1cf57222bc734e)  \n> Date: Thu, 31 Jan 2019 02:58:09 +0000  \n> Author: root (root@dsiprouterMackDocs.localdomain)  \n> Committer: root (root@dsiprouterMackDocs.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ed0782b28b6c89069bd919709e1cf57222bc734e)\n[//]: # (START_SECTION 2d510c904fe14aa029ee9ea96e98f3aadb9a27a9)\n### Remove the yaml file used for to host our website originally\n\n> Commit: [2d510c904fe14aa029ee9ea96e98f3aadb9a27a9](https://github.com/dOpensource/dsiprouter/commit/2d510c904fe14aa029ee9ea96e98f3aadb9a27a9)  \n> Date: Thu, 31 Jan 2019 02:56:26 +0000  \n> Author: root (root@dsiprouterMackDocs.localdomain)  \n> Committer: root (root@dsiprouterMackDocs.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2d510c904fe14aa029ee9ea96e98f3aadb9a27a9)\n[//]: # (START_SECTION 309d520d70d01ca5cb2911bb2b51889ee265f1dd)\n### Fixed a regression that caused sipsak to be installed each time dSIPRouter started\n\n> Commit: [309d520d70d01ca5cb2911bb2b51889ee265f1dd](https://github.com/dOpensource/dsiprouter/commit/309d520d70d01ca5cb2911bb2b51889ee265f1dd)  \n> Date: Thu, 31 Jan 2019 02:52:24 +0000  \n> Author: root (root@dsiprouterMackDocs.localdomain)  \n> Committer: root (root@dsiprouterMackDocs.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 309d520d70d01ca5cb2911bb2b51889ee265f1dd)\n[//]: # (START_SECTION 1ef917eb3b5c3674142c40c8d9c83a2542a097ee)\n### Started the development of a test plan for Carrier Registration\n\n> Commit: [1ef917eb3b5c3674142c40c8d9c83a2542a097ee](https://github.com/dOpensource/dsiprouter/commit/1ef917eb3b5c3674142c40c8d9c83a2542a097ee)  \n> Date: Wed, 30 Jan 2019 19:59:01 +0000  \n> Author: root (root@dsiprouterDroplet.localdomain)  \n> Committer: root (root@dsiprouterDroplet.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1ef917eb3b5c3674142c40c8d9c83a2542a097ee)\n[//]: # (START_SECTION 3d4490f29327d1eb634369b349edaece42972c6f)\n### AMI Startup Fixes and General Maintenance\n\n> Commit: [3d4490f29327d1eb634369b349edaece42972c6f](https://github.com/dOpensource/dsiprouter/commit/3d4490f29327d1eb634369b349edaece42972c6f)  \n> Date: Wed, 30 Jan 2019 05:07:37 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Resolves #103\n- change rtpengine install to be last\n- update usage options\n- update command line options\n- misc formatting improvements\n- fix centos ami kam repo issue\n- fix centos kamilio startup issue\n- fix rtpengine startup issue\n- fix debian ami sources issue\n- separate rtpengine source repo from project dir\n- fix rtpengine kernel packet forwarding issue\n- add location dependent redundancy checks in dsiprouter.sh\n- improve reliability of dynamic ip resolution\n- general cleanup in dsiprouter.sh\n- overhaul of arg / option parsing\n- improve usage readability\n- update usage options\n\n\n---\n\n[//]: # (END_SECTION 3d4490f29327d1eb634369b349edaece42972c6f)\n[//]: # (START_SECTION 625d0d43defef73588a349fe4c9ae4c4b5b513ef)\n### Delete unneeded files\n\n> Commit: [625d0d43defef73588a349fe4c9ae4c4b5b513ef](https://github.com/dOpensource/dsiprouter/commit/625d0d43defef73588a349fe4c9ae4c4b5b513ef)  \n> Date: Tue, 29 Jan 2019 23:19:04 +0000  \n> Author: root (root@dsiprouterDroplet.localdomain)  \n> Committer: root (root@dsiprouterDroplet.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 625d0d43defef73588a349fe4c9ae4c4b5b513ef)\n[//]: # (START_SECTION 371c4ea6c98df00752e0e43ad40c2017c94596b1)\n### - Added a basic Unit Testing Framework to allow us to test core dSIPRouter functionality - Fixed an issue with CDR's that will allow the SQL needed for CDR's to be ran during install - Added logic to install Sipsak for running Unit Testing and for users that want to troubleshoot SIP message without having a SIP client\n\n> Commit: [371c4ea6c98df00752e0e43ad40c2017c94596b1](https://github.com/dOpensource/dsiprouter/commit/371c4ea6c98df00752e0e43ad40c2017c94596b1)  \n> Date: Tue, 29 Jan 2019 22:31:59 +0000  \n> Author: root (root@dsiprouterDroplet.localdomain)  \n> Committer: root (root@dsiprouterDroplet.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 371c4ea6c98df00752e0e43ad40c2017c94596b1)\n[//]: # (START_SECTION de96347a51d1259827a7feb556d702629363225a)\n### Syslog Logging Fixes\n\n> Commit: [de96347a51d1259827a7feb556d702629363225a](https://github.com/dOpensource/dsiprouter/commit/de96347a51d1259827a7feb556d702629363225a)  \n> Date: Tue, 29 Jan 2019 10:44:44 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fixed syslog config files\n- seperate syslog configs in install process\n- redirect rtpengine daemon output to syslog\n- move syslog log handler to top of imports\n- support redirecting stdout / sterr to syslog\n- fix function naming to match\n- add signal handler func\n- add nohup signal handling to python app\n\n\n---\n\n[//]: # (END_SECTION de96347a51d1259827a7feb556d702629363225a)\n[//]: # (START_SECTION 3559b7e68f7f1ed0c0977ca4dde00c5ba84295a6)\n### Update Logging\n\n> Commit: [3559b7e68f7f1ed0c0977ca4dde00c5ba84295a6](https://github.com/dOpensource/dsiprouter/commit/3559b7e68f7f1ed0c0977ca4dde00c5ba84295a6)  \n> Date: Fri, 25 Jan 2019 17:13:50 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- move all logging to syslog\n- move all log rotation to logrotate\n- add syslog and logrotate as dependencies\n- update and create syslog configs for each service\n- add werkzurg and sqlalchemy log handlers from pull #36\n- add syslog support for dsiprouter app\n- add script header in comments\n- update app DEBUG variable dynamically\n\n\n---\n\n[//]: # (END_SECTION 3559b7e68f7f1ed0c0977ca4dde00c5ba84295a6)\n[//]: # (START_SECTION eb6d3af6e64c3bcb5e358f3f690aa1e2e68bce4a)\n### Added ability for 7 Digit numbers\n\n> Commit: [eb6d3af6e64c3bcb5e358f3f690aa1e2e68bce4a](https://github.com/dOpensource/dsiprouter/commit/eb6d3af6e64c3bcb5e358f3f690aa1e2e68bce4a)  \n> Date: Fri, 25 Jan 2019 14:58:25 -0700  \n> Author: Mat Murdock (mat.murdock@gmail.com)  \n> Committer: Mat Murdock (mat.murdock@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eb6d3af6e64c3bcb5e358f3f690aa1e2e68bce4a)\n[//]: # (START_SECTION 7943f39c2d5d2fe4b4b0e693ab16e1efcc15df14)\n### Create troubleshooting.rst.txt\n\n> Commit: [7943f39c2d5d2fe4b4b0e693ab16e1efcc15df14](https://github.com/dOpensource/dsiprouter/commit/7943f39c2d5d2fe4b4b0e693ab16e1efcc15df14)  \n> Date: Fri, 25 Jan 2019 16:12:07 -0500  \n> Author: Nicole (ncannon@goflyball.com)  \n> Committer: Nicole (ncannon@goflyball.com)  \n> Signed:   \n\n\n- -Created documentation for troubeshooting  dSIPRouter, Kamailio and rtpengine when turning logging on and off.\n- - Includes information:\n- 1 how to turn it on\n- 2. how do to turn it off\n- 3. location of the log files\n- 4. how do i configure it\n- 5. References\n\n\n---\n\n[//]: # (END_SECTION 7943f39c2d5d2fe4b4b0e693ab16e1efcc15df14)\n[//]: # (START_SECTION e408ca867b6a5af7cdd9804b1adc42a7fc0b428e)\n### Added logic to lookup the uac registration info based on the source ip coming from the carrier since I couldn't grab the realm - Fixed issue #98\n\n> Commit: [e408ca867b6a5af7cdd9804b1adc42a7fc0b428e](https://github.com/dOpensource/dsiprouter/commit/e408ca867b6a5af7cdd9804b1adc42a7fc0b428e)  \n> Date: Fri, 25 Jan 2019 00:46:48 +0000  \n> Author: root (root@dsiprouter.localdomain)  \n> Committer: root (root@dsiprouter.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e408ca867b6a5af7cdd9804b1adc42a7fc0b428e)\n[//]: # (START_SECTION c7d4923d98be9c4a4b9fed8d8e22d72c99a8a66d)\n### Update global_outbound_routes.rst\n\n> Commit: [c7d4923d98be9c4a4b9fed8d8e22d72c99a8a66d](https://github.com/dOpensource/dsiprouter/commit/c7d4923d98be9c4a4b9fed8d8e22d72c99a8a66d)  \n> Date: Tue, 22 Jan 2019 11:42:20 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c7d4923d98be9c4a4b9fed8d8e22d72c99a8a66d)\n[//]: # (START_SECTION 8a2eb618c88996ce96f2b9651945086a9911987b)\n### Added Pike and disbabled User Agent String\n\n> Commit: [8a2eb618c88996ce96f2b9651945086a9911987b](https://github.com/dOpensource/dsiprouter/commit/8a2eb618c88996ce96f2b9651945086a9911987b)  \n> Date: Fri, 18 Jan 2019 22:40:54 +0000  \n> Author: root (root@debian-s-1vcpu-1gb-tor1-01.localdomain)  \n> Committer: root (root@debian-s-1vcpu-1gb-tor1-01.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8a2eb618c88996ce96f2b9651945086a9911987b)\n[//]: # (START_SECTION 15d9cb64ccb64e28c7a8a31e8c70d7b11bb8ca45)\n### Added Pike and disbabled User Agent String\n\n> Commit: [15d9cb64ccb64e28c7a8a31e8c70d7b11bb8ca45](https://github.com/dOpensource/dsiprouter/commit/15d9cb64ccb64e28c7a8a31e8c70d7b11bb8ca45)  \n> Date: Fri, 18 Jan 2019 22:18:54 +0000  \n> Author: root (root@debian-s-1vcpu-1gb-tor1-01.localdomain)  \n> Committer: root (root@debian-s-1vcpu-1gb-tor1-01.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 15d9cb64ccb64e28c7a8a31e8c70d7b11bb8ca45)\n[//]: # (START_SECTION 06a2374ea824885faeb8423146f78977e585c921)\n### ChanSIP Documentation\n\n> Commit: [06a2374ea824885faeb8423146f78977e585c921](https://github.com/dOpensource/dsiprouter/commit/06a2374ea824885faeb8423146f78977e585c921)  \n> Date: Thu, 17 Jan 2019 13:33:50 -0500  \n> Author: Nicole (ncannon@goflyball.com)  \n> Committer: Nicole (ncannon@goflyball.com)  \n> Signed:   \n\n\n- added images for chan sip\n- added work flow for chan sip\n\n\n---\n\n[//]: # (END_SECTION 06a2374ea824885faeb8423146f78977e585c921)\n[//]: # (START_SECTION d752b7dadc3e93935c4473643ac459501855f69f)\n### Install Script Fixes\n\n> Commit: [d752b7dadc3e93935c4473643ac459501855f69f](https://github.com/dOpensource/dsiprouter/commit/d752b7dadc3e93935c4473643ac459501855f69f)  \n> Date: Mon, 14 Jan 2019 17:21:32 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Fix project path for absolute path resolution\n- Fix cron jobs for empty crontab use case\n\n\n---\n\n[//]: # (END_SECTION d752b7dadc3e93935c4473643ac459501855f69f)\n[//]: # (START_SECTION 32f6c5185b7017a86944ab21ed861f3cda67ead2)\n### Install Script Improvement\n\n> Commit: [32f6c5185b7017a86944ab21ed861f3cda67ead2](https://github.com/dOpensource/dsiprouter/commit/32f6c5185b7017a86944ab21ed861f3cda67ead2)  \n> Date: Mon, 14 Jan 2019 15:19:01 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- create cronAppend and cronRemove library functions\n- replace overwriting cron commands\n- change permissions on git hook\n\n\n---\n\n[//]: # (END_SECTION 32f6c5185b7017a86944ab21ed861f3cda67ead2)\n[//]: # (START_SECTION 763f35552506f642325d124def537b738dade694)\n### Merge with Master\n\n> Commit: [763f35552506f642325d124def537b738dade694](https://github.com/dOpensource/dsiprouter/commit/763f35552506f642325d124def537b738dade694)  \n> Date: Mon, 14 Jan 2019 14:29:25 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Merge with Master branch\n- update ami build to clone from feature branch\n\n\n---\n\n[//]: # (END_SECTION 763f35552506f642325d124def537b738dade694)\n[//]: # (START_SECTION e17357b54f0392ae5559328fd404a1c02ad3373f)\n### AMI updates\n\n> Commit: [e17357b54f0392ae5559328fd404a1c02ad3373f](https://github.com/dOpensource/dsiprouter/commit/e17357b54f0392ae5559328fd404a1c02ad3373f)  \n> Date: Thu, 10 Jan 2019 13:12:12 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- add getInstanceID to library script\n- allow independent execution of changelog hook\n- update ami bootstrap commands to be more robust\n- fix debian ami sys-maint user bug\n- fix PID check for startup process\n- fix python version check bug\n- added comments\n\n\n---\n\n[//]: # (END_SECTION e17357b54f0392ae5559328fd404a1c02ad3373f)\n[//]: # (START_SECTION 494fc460f3533bca4e81fbb3cac52f381f0169ce)\n### Update use-cases.rst\n\n> Commit: [494fc460f3533bca4e81fbb3cac52f381f0169ce](https://github.com/dOpensource/dsiprouter/commit/494fc460f3533bca4e81fbb3cac52f381f0169ce)  \n> Date: Wed, 9 Jan 2019 15:46:38 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 494fc460f3533bca4e81fbb3cac52f381f0169ce)\n[//]: # (START_SECTION 353887e5360d94ce4ff5a0a891814aa2f03c1be0)\n### Add Changelog\n\n> Commit: [353887e5360d94ce4ff5a0a891814aa2f03c1be0](https://github.com/dOpensource/dsiprouter/commit/353887e5360d94ce4ff5a0a891814aa2f03c1be0)  \n> Date: Wed, 9 Jan 2019 09:27:47 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Resolves #81\n- Add changelog markdown file\n- Add git hook for generating changelog\n\n\n---\n\n[//]: # (END_SECTION 353887e5360d94ce4ff5a0a891814aa2f03c1be0)\n[//]: # (START_SECTION f673d614f9955d79b613eb248288d317349c5777)\n### Update to Commit 2e7acf4\n\n> Commit: [f673d614f9955d79b613eb248288d317349c5777](https://github.com/dOpensource/dsiprouter/commit/f673d614f9955d79b613eb248288d317349c5777)  \n> Date: Mon, 7 Jan 2019 16:42:13 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- move AMI checks for debian before end of function\n- to ensure we do not return false positive to calling script\n\n\n---\n\n[//]: # (END_SECTION f673d614f9955d79b613eb248288d317349c5777)\n[//]: # (START_SECTION 2e7acf4fe904c02ec796c6e8aebe73aa4364c073)\n### AWS Image Debian Support\n\n> Commit: [2e7acf4fe904c02ec796c6e8aebe73aa4364c073](https://github.com/dOpensource/dsiprouter/commit/2e7acf4fe904c02ec796c6e8aebe73aa4364c073)  \n> Date: Mon, 7 Jan 2019 16:34:28 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- add support for debian AMI build\n- apply AWS AMI policies for debian build\n\n\n---\n\n[//]: # (END_SECTION 2e7acf4fe904c02ec796c6e8aebe73aa4364c073)\n[//]: # (START_SECTION b6f4f1481ed3a36c3d6ae02cbca9a2877ddf6702)\n### External IP BUG fix\n\n> Commit: [b6f4f1481ed3a36c3d6ae02cbca9a2877ddf6702](https://github.com/dOpensource/dsiprouter/commit/b6f4f1481ed3a36c3d6ae02cbca9a2877ddf6702)  \n> Date: Fri, 4 Jan 2019 15:35:12 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- catch errors on external IP resolution failure\n- add commandline option for setting external ip\n- change permisions on ami build script\n\n\n---\n\n[//]: # (END_SECTION b6f4f1481ed3a36c3d6ae02cbca9a2877ddf6702)\n[//]: # (START_SECTION e892735488702df24d6bc4d9b6847e5c13c57caa)\n### Update use-cases.rst\n\n> Commit: [e892735488702df24d6bc4d9b6847e5c13c57caa](https://github.com/dOpensource/dsiprouter/commit/e892735488702df24d6bc4d9b6847e5c13c57caa)  \n> Date: Thu, 3 Jan 2019 23:29:41 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e892735488702df24d6bc4d9b6847e5c13c57caa)\n[//]: # (START_SECTION c23f6f166ea3ee2e3a9c659b5e1763edc823420c)\n### Updates for AMI install\n\n> Commit: [c23f6f166ea3ee2e3a9c659b5e1763edc823420c](https://github.com/dOpensource/dsiprouter/commit/c23f6f166ea3ee2e3a9c659b5e1763edc823420c)  \n> Date: Wed, 2 Jan 2019 09:21:48 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- update debian-based install for unattended install\n\n\n---\n\n[//]: # (END_SECTION c23f6f166ea3ee2e3a9c659b5e1763edc823420c)\n[//]: # (START_SECTION 5c2c32cf69b65865957b495122ea3252a1b74715)\n### Update upgrade.rst\n\n> Commit: [5c2c32cf69b65865957b495122ea3252a1b74715](https://github.com/dOpensource/dsiprouter/commit/5c2c32cf69b65865957b495122ea3252a1b74715)  \n> Date: Sat, 29 Dec 2018 14:47:58 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5c2c32cf69b65865957b495122ea3252a1b74715)\n[//]: # (START_SECTION 8319ce82acb079246cb64d185d99835bf195a11c)\n### Fixed the install function so that dSIPRouter starts up after the install\n\n> Commit: [8319ce82acb079246cb64d185d99835bf195a11c](https://github.com/dOpensource/dsiprouter/commit/8319ce82acb079246cb64d185d99835bf195a11c)  \n> Date: Sat, 29 Dec 2018 19:13:47 +0000  \n> Author: root (mack@dopensource.com)  \n> Committer: root (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8319ce82acb079246cb64d185d99835bf195a11c)\n[//]: # (START_SECTION a7b5433d897630789f687d9504b1d58cfaadb6ba)\n### Update centos-install.rst\n\n> Commit: [a7b5433d897630789f687d9504b1d58cfaadb6ba](https://github.com/dOpensource/dsiprouter/commit/a7b5433d897630789f687d9504b1d58cfaadb6ba)  \n> Date: Fri, 28 Dec 2018 18:17:17 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a7b5433d897630789f687d9504b1d58cfaadb6ba)\n[//]: # (START_SECTION 68f9b1f66a6579f058026aac150edf7d260c1e1b)\n### Update centos-install.rst\n\n> Commit: [68f9b1f66a6579f058026aac150edf7d260c1e1b](https://github.com/dOpensource/dsiprouter/commit/68f9b1f66a6579f058026aac150edf7d260c1e1b)  \n> Date: Fri, 28 Dec 2018 18:16:51 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 68f9b1f66a6579f058026aac150edf7d260c1e1b)\n[//]: # (START_SECTION fa306ebbebfe90ed57cecbf5d170a5670efc2151)\n### Fixed an issue that stoped dSIPRouter from starting up after the install.  Also, started to decouple the dSIPRouter UI from the rest of the install - Docker here we come\n\n> Commit: [fa306ebbebfe90ed57cecbf5d170a5670efc2151](https://github.com/dOpensource/dsiprouter/commit/fa306ebbebfe90ed57cecbf5d170a5670efc2151)  \n> Date: Fri, 28 Dec 2018 23:14:33 +0000  \n> Author: root (mack@dsiprouter.org)  \n> Committer: root (mack@dsiprouter.org)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fa306ebbebfe90ed57cecbf5d170a5670efc2151)\n[//]: # (START_SECTION a77115c64ec17be4382284d7e7da7ceffd81b796)\n### Update centos-install.rst\n\n> Commit: [a77115c64ec17be4382284d7e7da7ceffd81b796](https://github.com/dOpensource/dsiprouter/commit/a77115c64ec17be4382284d7e7da7ceffd81b796)  \n> Date: Fri, 28 Dec 2018 16:44:29 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a77115c64ec17be4382284d7e7da7ceffd81b796)\n[//]: # (START_SECTION 7c7e1cf85df43725857ab626818bb4f464a5fbed)\n### Update centos-install.rst\n\n> Commit: [7c7e1cf85df43725857ab626818bb4f464a5fbed](https://github.com/dOpensource/dsiprouter/commit/7c7e1cf85df43725857ab626818bb4f464a5fbed)  \n> Date: Fri, 28 Dec 2018 16:26:47 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7c7e1cf85df43725857ab626818bb4f464a5fbed)\n[//]: # (START_SECTION ba0abf4fe1b23978a2479d2e47d38777e88fe8da)\n### Update centos-install.rst\n\n> Commit: [ba0abf4fe1b23978a2479d2e47d38777e88fe8da](https://github.com/dOpensource/dsiprouter/commit/ba0abf4fe1b23978a2479d2e47d38777e88fe8da)  \n> Date: Fri, 28 Dec 2018 09:29:15 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ba0abf4fe1b23978a2479d2e47d38777e88fe8da)\n[//]: # (START_SECTION 5c9045fce266d3460c68c8a91db8c0bae73928d4)\n### Update centos-install.rst\n\n> Commit: [5c9045fce266d3460c68c8a91db8c0bae73928d4](https://github.com/dOpensource/dsiprouter/commit/5c9045fce266d3460c68c8a91db8c0bae73928d4)  \n> Date: Fri, 28 Dec 2018 09:27:50 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5c9045fce266d3460c68c8a91db8c0bae73928d4)\n[//]: # (START_SECTION 213dadb37638d3907b081787817dd0bd29c7801d)\n### Update installing.rst\n\n> Commit: [213dadb37638d3907b081787817dd0bd29c7801d](https://github.com/dOpensource/dsiprouter/commit/213dadb37638d3907b081787817dd0bd29c7801d)  \n> Date: Fri, 28 Dec 2018 08:55:49 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 213dadb37638d3907b081787817dd0bd29c7801d)\n[//]: # (START_SECTION 5dac9a380d67d645ef6ee855bc8db7bb2fa240f4)\n### Update installing.rst\n\n> Commit: [5dac9a380d67d645ef6ee855bc8db7bb2fa240f4](https://github.com/dOpensource/dsiprouter/commit/5dac9a380d67d645ef6ee855bc8db7bb2fa240f4)  \n> Date: Fri, 28 Dec 2018 08:49:36 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5dac9a380d67d645ef6ee855bc8db7bb2fa240f4)\n[//]: # (START_SECTION 56aadb779fa63e3751a4fe096fbf0f6fe0b8c6ff)\n### Update centos-install.rst\n\n> Commit: [56aadb779fa63e3751a4fe096fbf0f6fe0b8c6ff](https://github.com/dOpensource/dsiprouter/commit/56aadb779fa63e3751a4fe096fbf0f6fe0b8c6ff)  \n> Date: Fri, 28 Dec 2018 08:48:39 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 56aadb779fa63e3751a4fe096fbf0f6fe0b8c6ff)\n[//]: # (START_SECTION 380777056cadabb8b2088bc504764f6f5a988e3a)\n### Update centos-install.rst\n\n> Commit: [380777056cadabb8b2088bc504764f6f5a988e3a](https://github.com/dOpensource/dsiprouter/commit/380777056cadabb8b2088bc504764f6f5a988e3a)  \n> Date: Fri, 28 Dec 2018 08:48:15 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 380777056cadabb8b2088bc504764f6f5a988e3a)\n[//]: # (START_SECTION f8e12884d2b5b749d7f004a76bff484938ae9949)\n### Create centos-install.rst\n\n> Commit: [f8e12884d2b5b749d7f004a76bff484938ae9949](https://github.com/dOpensource/dsiprouter/commit/f8e12884d2b5b749d7f004a76bff484938ae9949)  \n> Date: Fri, 28 Dec 2018 08:45:24 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f8e12884d2b5b749d7f004a76bff484938ae9949)\n[//]: # (START_SECTION ff9cfc9e20b5804ed54ffac937af1c1d923c4b52)\n### Update installing.rst\n\n> Commit: [ff9cfc9e20b5804ed54ffac937af1c1d923c4b52](https://github.com/dOpensource/dsiprouter/commit/ff9cfc9e20b5804ed54ffac937af1c1d923c4b52)  \n> Date: Fri, 28 Dec 2018 08:44:38 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ff9cfc9e20b5804ed54ffac937af1c1d923c4b52)\n[//]: # (START_SECTION 11f42d1ca38c9a24d8c86d1e6f3b4f5697b62a50)\n### Update debian_install.rst\n\n> Commit: [11f42d1ca38c9a24d8c86d1e6f3b4f5697b62a50](https://github.com/dOpensource/dsiprouter/commit/11f42d1ca38c9a24d8c86d1e6f3b4f5697b62a50)  \n> Date: Fri, 28 Dec 2018 08:43:17 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 11f42d1ca38c9a24d8c86d1e6f3b4f5697b62a50)\n[//]: # (START_SECTION 8cd24935a6a68bc5da3f95ed7b8098c639b286a8)\n### Update installing.rst\n\n> Commit: [8cd24935a6a68bc5da3f95ed7b8098c639b286a8](https://github.com/dOpensource/dsiprouter/commit/8cd24935a6a68bc5da3f95ed7b8098c639b286a8)  \n> Date: Fri, 28 Dec 2018 08:41:57 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8cd24935a6a68bc5da3f95ed7b8098c639b286a8)\n[//]: # (START_SECTION 44db64adf744fca1005aaad64cb679c8e46480b9)\n### Create debian_install.rst\n\n> Commit: [44db64adf744fca1005aaad64cb679c8e46480b9](https://github.com/dOpensource/dsiprouter/commit/44db64adf744fca1005aaad64cb679c8e46480b9)  \n> Date: Fri, 28 Dec 2018 08:35:38 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 44db64adf744fca1005aaad64cb679c8e46480b9)\n[//]: # (START_SECTION 07a29500e828892fd6f9f3bd32791daa93523c33)\n### Update installing.rst\n\n> Commit: [07a29500e828892fd6f9f3bd32791daa93523c33](https://github.com/dOpensource/dsiprouter/commit/07a29500e828892fd6f9f3bd32791daa93523c33)  \n> Date: Fri, 28 Dec 2018 08:34:58 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 07a29500e828892fd6f9f3bd32791daa93523c33)\n[//]: # (START_SECTION 00d60f84e5beb610ad2d414caa16071ee8318c20)\n### Fixed the CentOS 7 install so that MariaDB starts before Kamailio\n\n> Commit: [00d60f84e5beb610ad2d414caa16071ee8318c20](https://github.com/dOpensource/dsiprouter/commit/00d60f84e5beb610ad2d414caa16071ee8318c20)  \n> Date: Fri, 28 Dec 2018 13:31:54 +0000  \n> Author: root (mack@dopensource.com)  \n> Committer: root (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 00d60f84e5beb610ad2d414caa16071ee8318c20)\n[//]: # (START_SECTION 9e6165884c69419cc706edbe695ee44bf594201c)\n### Fixed RTPEngine\n\n> Commit: [9e6165884c69419cc706edbe695ee44bf594201c](https://github.com/dOpensource/dsiprouter/commit/9e6165884c69419cc706edbe695ee44bf594201c)  \n> Date: Fri, 28 Dec 2018 10:04:49 +0000  \n> Author: root (mack@dopensource.com)  \n> Committer: root (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9e6165884c69419cc706edbe695ee44bf594201c)\n[//]: # (START_SECTION db85442c55b983a53acf28b630c26156f5c78a7a)\n### Removed the yum update from the RTPEngine install section for CentOS - it was causing us to reboot before completing the install of RTPEngine\n\n> Commit: [db85442c55b983a53acf28b630c26156f5c78a7a](https://github.com/dOpensource/dsiprouter/commit/db85442c55b983a53acf28b630c26156f5c78a7a)  \n> Date: Fri, 28 Dec 2018 09:03:40 +0000  \n> Author: root (mack.hendricks@gmail.com)  \n> Committer: root (mack.hendricks@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION db85442c55b983a53acf28b630c26156f5c78a7a)\n[//]: # (START_SECTION a39c1523d84b91a9fb2153966c01f40545d263a9)\n### Fixed issues with installing on CentOS 7\n\n> Commit: [a39c1523d84b91a9fb2153966c01f40545d263a9](https://github.com/dOpensource/dsiprouter/commit/a39c1523d84b91a9fb2153966c01f40545d263a9)  \n> Date: Fri, 28 Dec 2018 08:30:51 +0000  \n> Author: root (mack@dopensource.com)  \n> Committer: root (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a39c1523d84b91a9fb2153966c01f40545d263a9)\n[//]: # (START_SECTION e4c32ffe5e1ae60996f08362524f0a31e156e580)\n### Fixed the hostname of the service that provides the external ip of the server\n\n> Commit: [e4c32ffe5e1ae60996f08362524f0a31e156e580](https://github.com/dOpensource/dsiprouter/commit/e4c32ffe5e1ae60996f08362524f0a31e156e580)  \n> Date: Thu, 27 Dec 2018 20:23:43 +0000  \n> Author: root (mack@dopensource.com)  \n> Committer: root (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e4c32ffe5e1ae60996f08362524f0a31e156e580)\n[//]: # (START_SECTION 53f6940f23637c92e9b3c7f2590489f1a04fee8b)\n### Fixed the hostname of the service that provides the external ip of the server\n\n> Commit: [53f6940f23637c92e9b3c7f2590489f1a04fee8b](https://github.com/dOpensource/dsiprouter/commit/53f6940f23637c92e9b3c7f2590489f1a04fee8b)  \n> Date: Thu, 27 Dec 2018 20:23:43 +0000  \n> Author: root (mack@dopensource.com)  \n> Committer: root (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 53f6940f23637c92e9b3c7f2590489f1a04fee8b)\n[//]: # (START_SECTION a31a5aab83265cae124030e31afb2d8161517a49)\n### AMI build updates\n\n> Commit: [a31a5aab83265cae124030e31afb2d8161517a49](https://github.com/dOpensource/dsiprouter/commit/a31a5aab83265cae124030e31afb2d8161517a49)  \n> Date: Fri, 21 Dec 2018 16:35:14 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- add ami build script for v0.52\n- fix firewalld not started bug\n- change rtpengine install to bootstrap on restart of AMI image\n- fix rc.local format bug from last commit\n- add descriptive comments\n- Signed-off-by: Tyler Moore <tmoore@goflyball.com>\n\n\n---\n\n[//]: # (END_SECTION a31a5aab83265cae124030e31afb2d8161517a49)\n[//]: # (START_SECTION 4e66d02f7be46804f4291e52a2913dca11398bb4)\n### AMI image pw reset fix\n\n> Commit: [4e66d02f7be46804f4291e52a2913dca11398bb4](https://github.com/dOpensource/dsiprouter/commit/4e66d02f7be46804f4291e52a2913dca11398bb4)  \n> Date: Fri, 21 Dec 2018 13:50:05 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fix AMI pw reset bug\n\n\n---\n\n[//]: # (END_SECTION 4e66d02f7be46804f4291e52a2913dca11398bb4)\n[//]: # (START_SECTION d9c9a4e85c250fdfc52889e313e88bab93361a67)\n### Fix AMI bootstrap file\n\n> Commit: [d9c9a4e85c250fdfc52889e313e88bab93361a67](https://github.com/dOpensource/dsiprouter/commit/d9c9a4e85c250fdfc52889e313e88bab93361a67)  \n> Date: Fri, 21 Dec 2018 13:32:35 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fixed bootstrap file test\n\n\n---\n\n[//]: # (END_SECTION d9c9a4e85c250fdfc52889e313e88bab93361a67)\n[//]: # (START_SECTION 3d427e156f02e5ae80971c5adedab736e4570218)\n### Updates for AMI image install\n\n> Commit: [3d427e156f02e5ae80971c5adedab736e4570218](https://github.com/dOpensource/dsiprouter/commit/3d427e156f02e5ae80971c5adedab736e4570218)  \n> Date: Fri, 21 Dec 2018 12:34:30 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fix bootstrap file not to interfere with other cmds\n- fix centos rtpengine install\n- make centos rtpengine failure stop install\n\n\n---\n\n[//]: # (END_SECTION 3d427e156f02e5ae80971c5adedab736e4570218)\n[//]: # (START_SECTION 2817457d03d14b9b8c919d14a4398ad11a9724f2)\n### Fixes to AMI image support\n\n> Commit: [2817457d03d14b9b8c919d14a4398ad11a9724f2](https://github.com/dOpensource/dsiprouter/commit/2817457d03d14b9b8c919d14a4398ad11a9724f2)  \n> Date: Fri, 21 Dec 2018 11:50:49 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- fix logical check for bootstrap file\n- add cmdExists function to dsip_lib\n\n\n---\n\n[//]: # (END_SECTION 2817457d03d14b9b8c919d14a4398ad11a9724f2)\n[//]: # (START_SECTION cf8d21c1423bee2d235b7d2997fb08b13cb2576c)\n### Updated restart message for AMI instances.\n\n> Commit: [cf8d21c1423bee2d235b7d2997fb08b13cb2576c](https://github.com/dOpensource/dsiprouter/commit/cf8d21c1423bee2d235b7d2997fb08b13cb2576c)  \n> Date: Fri, 21 Dec 2018 11:34:04 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cf8d21c1423bee2d235b7d2997fb08b13cb2576c)\n[//]: # (START_SECTION 461216e738dfa7790d86d0bf5fa32a94f22f6b30)\n### Add support for AMI images\n\n> Commit: [461216e738dfa7790d86d0bf5fa32a94f22f6b30](https://github.com/dOpensource/dsiprouter/commit/461216e738dfa7790d86d0bf5fa32a94f22f6b30)  \n> Date: Fri, 21 Dec 2018 11:21:36 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- add support for installing on AMI images\n- fixed small typos in install script\n- fixed error message when restarting process\n- fixed centos kernel headers install issue\n\n\n---\n\n[//]: # (END_SECTION 461216e738dfa7790d86d0bf5fa32a94f22f6b30)\n[//]: # (START_SECTION 6fb37dafdc35a91ac066c8b82598db9a565c5c6a)\n### Add files via upload\n\n> Commit: [6fb37dafdc35a91ac066c8b82598db9a565c5c6a](https://github.com/dOpensource/dsiprouter/commit/6fb37dafdc35a91ac066c8b82598db9a565c5c6a)  \n> Date: Wed, 19 Dec 2018 15:03:28 -0600  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6fb37dafdc35a91ac066c8b82598db9a565c5c6a)\n[//]: # (START_SECTION dcda7bacf833e3388f96f369bc8e1a713cfff943)\n### Update use-cases.rst\n\n> Commit: [dcda7bacf833e3388f96f369bc8e1a713cfff943](https://github.com/dOpensource/dsiprouter/commit/dcda7bacf833e3388f96f369bc8e1a713cfff943)  \n> Date: Wed, 19 Dec 2018 10:18:24 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@debian-post51.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dcda7bacf833e3388f96f369bc8e1a713cfff943)\n[//]: # (START_SECTION 6b7441318d53089d8178c5fd92d47b625836c38c)\n### Update use-cases.rst\n\n> Commit: [6b7441318d53089d8178c5fd92d47b625836c38c](https://github.com/dOpensource/dsiprouter/commit/6b7441318d53089d8178c5fd92d47b625836c38c)  \n> Date: Wed, 19 Dec 2018 10:12:58 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@debian-post51.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6b7441318d53089d8178c5fd92d47b625836c38c)\n[//]: # (START_SECTION ed1d68e393d336f3860968921d17a19c080f82ef)\n### Update use-cases.rst\n\n> Commit: [ed1d68e393d336f3860968921d17a19c080f82ef](https://github.com/dOpensource/dsiprouter/commit/ed1d68e393d336f3860968921d17a19c080f82ef)  \n> Date: Wed, 19 Dec 2018 10:11:58 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@debian-post51.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ed1d68e393d336f3860968921d17a19c080f82ef)\n[//]: # (START_SECTION 5165fdbdf164623a319c97df2d3a1dda239bc350)\n### Update use-cases.rst\n\n> Commit: [5165fdbdf164623a319c97df2d3a1dda239bc350](https://github.com/dOpensource/dsiprouter/commit/5165fdbdf164623a319c97df2d3a1dda239bc350)  \n> Date: Wed, 19 Dec 2018 10:10:04 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@debian-post51.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5165fdbdf164623a319c97df2d3a1dda239bc350)\n[//]: # (START_SECTION edcd5b42498f1658c88938b38d66ef23b3e997b0)\n### Update use-cases.rst\n\n> Commit: [edcd5b42498f1658c88938b38d66ef23b3e997b0](https://github.com/dOpensource/dsiprouter/commit/edcd5b42498f1658c88938b38d66ef23b3e997b0)  \n> Date: Wed, 19 Dec 2018 10:07:45 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@debian-post51.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION edcd5b42498f1658c88938b38d66ef23b3e997b0)\n[//]: # (START_SECTION a8f51d584f6de535a545173d2e714c34c9f057d5)\n### Update use-cases.rst\n\n> Commit: [a8f51d584f6de535a545173d2e714c34c9f057d5](https://github.com/dOpensource/dsiprouter/commit/a8f51d584f6de535a545173d2e714c34c9f057d5)  \n> Date: Wed, 19 Dec 2018 10:05:32 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@debian-post51.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a8f51d584f6de535a545173d2e714c34c9f057d5)\n[//]: # (START_SECTION 921ad20d8c0baebf6ad19398fca666347e17305b)\n### Update use-cases.rst\n\n> Commit: [921ad20d8c0baebf6ad19398fca666347e17305b](https://github.com/dOpensource/dsiprouter/commit/921ad20d8c0baebf6ad19398fca666347e17305b)  \n> Date: Wed, 19 Dec 2018 09:59:21 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@debian-post51.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 921ad20d8c0baebf6ad19398fca666347e17305b)\n[//]: # (START_SECTION 44fe70810398f30422f01c23f0b4d5119e5ac269)\n### Update use-cases.rst\n\n> Commit: [44fe70810398f30422f01c23f0b4d5119e5ac269](https://github.com/dOpensource/dsiprouter/commit/44fe70810398f30422f01c23f0b4d5119e5ac269)  \n> Date: Wed, 19 Dec 2018 09:57:55 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@debian-post51.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 44fe70810398f30422f01c23f0b4d5119e5ac269)\n[//]: # (START_SECTION 04d3d7dbe9aa5b49e4e6edcabaa73de93a99007b)\n### Update use-cases.rst\n\n> Commit: [04d3d7dbe9aa5b49e4e6edcabaa73de93a99007b](https://github.com/dOpensource/dsiprouter/commit/04d3d7dbe9aa5b49e4e6edcabaa73de93a99007b)  \n> Date: Wed, 19 Dec 2018 09:57:05 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@debian-post51.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 04d3d7dbe9aa5b49e4e6edcabaa73de93a99007b)\n[//]: # (START_SECTION eed48aadbcb3a0099d6339b16d0ca256efd5b2eb)\n### Update use-cases.rst\n\n> Commit: [eed48aadbcb3a0099d6339b16d0ca256efd5b2eb](https://github.com/dOpensource/dsiprouter/commit/eed48aadbcb3a0099d6339b16d0ca256efd5b2eb)  \n> Date: Wed, 19 Dec 2018 06:59:11 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@debian-post51.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eed48aadbcb3a0099d6339b16d0ca256efd5b2eb)\n[//]: # (START_SECTION 77f487b70306be50873167a363b7cbc455ac7a1e)\n### Update use-cases.rst\n\n> Commit: [77f487b70306be50873167a363b7cbc455ac7a1e](https://github.com/dOpensource/dsiprouter/commit/77f487b70306be50873167a363b7cbc455ac7a1e)  \n> Date: Wed, 19 Dec 2018 06:57:51 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: root (root@debian-post51.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 77f487b70306be50873167a363b7cbc455ac7a1e)\n[//]: # (START_SECTION bdc2997ed709155a166218a613f78877bc8adf99)\n### Fixed the BYE issue #56 for FusionPBX as well\n\n> Commit: [bdc2997ed709155a166218a613f78877bc8adf99](https://github.com/dOpensource/dsiprouter/commit/bdc2997ed709155a166218a613f78877bc8adf99)  \n> Date: Wed, 19 Dec 2018 19:05:34 +0000  \n> Author: root (root@debian-post51.localdomain)  \n> Committer: root (root@debian-post51.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bdc2997ed709155a166218a613f78877bc8adf99)\n[//]: # (START_SECTION e5f2cd20edad11523f89f70fb5a9a0bbe976e220)\n### Fixed issue #56\n\n> Commit: [e5f2cd20edad11523f89f70fb5a9a0bbe976e220](https://github.com/dOpensource/dsiprouter/commit/e5f2cd20edad11523f89f70fb5a9a0bbe976e220)  \n> Date: Wed, 19 Dec 2018 17:40:05 +0000  \n> Author: root (root@demo-dsiprouter.localdomain)  \n> Committer: root (root@demo-dsiprouter.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e5f2cd20edad11523f89f70fb5a9a0bbe976e220)\n[//]: # (START_SECTION c8f7d9b4118bae93328cd9937a42cf078e12c989)\n### Update use-cases.rst\n\n> Commit: [c8f7d9b4118bae93328cd9937a42cf078e12c989](https://github.com/dOpensource/dsiprouter/commit/c8f7d9b4118bae93328cd9937a42cf078e12c989)  \n> Date: Wed, 19 Dec 2018 10:29:40 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c8f7d9b4118bae93328cd9937a42cf078e12c989)\n[//]: # (START_SECTION 3255032cd4fa5aaa8339ae3307fcc40e2c1e5526)\n### Update use-cases.rst\n\n> Commit: [3255032cd4fa5aaa8339ae3307fcc40e2c1e5526](https://github.com/dOpensource/dsiprouter/commit/3255032cd4fa5aaa8339ae3307fcc40e2c1e5526)  \n> Date: Wed, 19 Dec 2018 10:20:09 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3255032cd4fa5aaa8339ae3307fcc40e2c1e5526)\n[//]: # (START_SECTION fcb9686346d364b4d16eb7bebaebef1c9bbee05b)\n### Update use-cases.rst\n\n> Commit: [fcb9686346d364b4d16eb7bebaebef1c9bbee05b](https://github.com/dOpensource/dsiprouter/commit/fcb9686346d364b4d16eb7bebaebef1c9bbee05b)  \n> Date: Wed, 19 Dec 2018 10:18:24 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fcb9686346d364b4d16eb7bebaebef1c9bbee05b)\n[//]: # (START_SECTION 453da82b53c59342919230bb97ac103e7d19f5ca)\n### Update use-cases.rst\n\n> Commit: [453da82b53c59342919230bb97ac103e7d19f5ca](https://github.com/dOpensource/dsiprouter/commit/453da82b53c59342919230bb97ac103e7d19f5ca)  \n> Date: Wed, 19 Dec 2018 10:12:58 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 453da82b53c59342919230bb97ac103e7d19f5ca)\n[//]: # (START_SECTION 5df0b5ada91bead8b0c1ce43133ce31bb988721d)\n### Update use-cases.rst\n\n> Commit: [5df0b5ada91bead8b0c1ce43133ce31bb988721d](https://github.com/dOpensource/dsiprouter/commit/5df0b5ada91bead8b0c1ce43133ce31bb988721d)  \n> Date: Wed, 19 Dec 2018 10:11:58 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5df0b5ada91bead8b0c1ce43133ce31bb988721d)\n[//]: # (START_SECTION 0c92eda77ba173040b0a35da9232869f02e23b31)\n### Update use-cases.rst\n\n> Commit: [0c92eda77ba173040b0a35da9232869f02e23b31](https://github.com/dOpensource/dsiprouter/commit/0c92eda77ba173040b0a35da9232869f02e23b31)  \n> Date: Wed, 19 Dec 2018 10:10:04 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0c92eda77ba173040b0a35da9232869f02e23b31)\n[//]: # (START_SECTION c7f3890d56ca343518535df5254586262c26ca14)\n### Update use-cases.rst\n\n> Commit: [c7f3890d56ca343518535df5254586262c26ca14](https://github.com/dOpensource/dsiprouter/commit/c7f3890d56ca343518535df5254586262c26ca14)  \n> Date: Wed, 19 Dec 2018 10:07:45 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c7f3890d56ca343518535df5254586262c26ca14)\n[//]: # (START_SECTION f3622aab4b6f207b1ce84f7997dc3650f3f50a4e)\n### Update use-cases.rst\n\n> Commit: [f3622aab4b6f207b1ce84f7997dc3650f3f50a4e](https://github.com/dOpensource/dsiprouter/commit/f3622aab4b6f207b1ce84f7997dc3650f3f50a4e)  \n> Date: Wed, 19 Dec 2018 10:05:32 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f3622aab4b6f207b1ce84f7997dc3650f3f50a4e)\n[//]: # (START_SECTION ee463965c59f9fd8f31c162345be3784dd08ae92)\n### Update use-cases.rst\n\n> Commit: [ee463965c59f9fd8f31c162345be3784dd08ae92](https://github.com/dOpensource/dsiprouter/commit/ee463965c59f9fd8f31c162345be3784dd08ae92)  \n> Date: Wed, 19 Dec 2018 09:59:21 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ee463965c59f9fd8f31c162345be3784dd08ae92)\n[//]: # (START_SECTION 0616d64521ad05f27ec02c43467f4246d20439d3)\n### Update use-cases.rst\n\n> Commit: [0616d64521ad05f27ec02c43467f4246d20439d3](https://github.com/dOpensource/dsiprouter/commit/0616d64521ad05f27ec02c43467f4246d20439d3)  \n> Date: Wed, 19 Dec 2018 09:57:55 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0616d64521ad05f27ec02c43467f4246d20439d3)\n[//]: # (START_SECTION 97fe92ebc2bba8e18fdf11834703cd25a9889e55)\n### Update use-cases.rst\n\n> Commit: [97fe92ebc2bba8e18fdf11834703cd25a9889e55](https://github.com/dOpensource/dsiprouter/commit/97fe92ebc2bba8e18fdf11834703cd25a9889e55)  \n> Date: Wed, 19 Dec 2018 09:57:05 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 97fe92ebc2bba8e18fdf11834703cd25a9889e55)\n[//]: # (START_SECTION 8cc732379d61f12545214351227b96d33e81d6c8)\n### Add files via upload\n\n> Commit: [8cc732379d61f12545214351227b96d33e81d6c8](https://github.com/dOpensource/dsiprouter/commit/8cc732379d61f12545214351227b96d33e81d6c8)  \n> Date: Wed, 19 Dec 2018 08:56:39 -0600  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8cc732379d61f12545214351227b96d33e81d6c8)\n[//]: # (START_SECTION 0817c9ad7c76c8dc9f6c04a5b64230ceac788eb8)\n### Update use-cases.rst\n\n> Commit: [0817c9ad7c76c8dc9f6c04a5b64230ceac788eb8](https://github.com/dOpensource/dsiprouter/commit/0817c9ad7c76c8dc9f6c04a5b64230ceac788eb8)  \n> Date: Wed, 19 Dec 2018 06:59:11 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0817c9ad7c76c8dc9f6c04a5b64230ceac788eb8)\n[//]: # (START_SECTION daab25528053c5430942d3734593046b09c0735a)\n### Update use-cases.rst\n\n> Commit: [daab25528053c5430942d3734593046b09c0735a](https://github.com/dOpensource/dsiprouter/commit/daab25528053c5430942d3734593046b09c0735a)  \n> Date: Wed, 19 Dec 2018 06:57:51 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION daab25528053c5430942d3734593046b09c0735a)\n[//]: # (START_SECTION 6a30501a6e9356518a6d47951b7d201ae7462c00)\n### Added files for documenting FreePBX - Pass Thru\n\n> Commit: [6a30501a6e9356518a6d47951b7d201ae7462c00](https://github.com/dOpensource/dsiprouter/commit/6a30501a6e9356518a6d47951b7d201ae7462c00)  \n> Date: Wed, 19 Dec 2018 05:53:52 -0600  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6a30501a6e9356518a6d47951b7d201ae7462c00)\n[//]: # (START_SECTION fa8222a169bfa56c56addb8cfb8e2782a0b291c1)\n### Update index.rst\n\n> Commit: [fa8222a169bfa56c56addb8cfb8e2782a0b291c1](https://github.com/dOpensource/dsiprouter/commit/fa8222a169bfa56c56addb8cfb8e2782a0b291c1)  \n> Date: Tue, 18 Dec 2018 05:43:29 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fa8222a169bfa56c56addb8cfb8e2782a0b291c1)\n[//]: # (START_SECTION 609550397bb6bde78991a8eea0701a9380113af6)\n### Update upgrade.rst\n\n> Commit: [609550397bb6bde78991a8eea0701a9380113af6](https://github.com/dOpensource/dsiprouter/commit/609550397bb6bde78991a8eea0701a9380113af6)  \n> Date: Tue, 18 Dec 2018 05:28:24 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 609550397bb6bde78991a8eea0701a9380113af6)\n[//]: # (START_SECTION 3d47aeae457dcaf92ed21b63d134359ed50e1af3)\n### Update index.rst\n\n> Commit: [3d47aeae457dcaf92ed21b63d134359ed50e1af3](https://github.com/dOpensource/dsiprouter/commit/3d47aeae457dcaf92ed21b63d134359ed50e1af3)  \n> Date: Mon, 17 Dec 2018 10:22:00 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3d47aeae457dcaf92ed21b63d134359ed50e1af3)\n[//]: # (START_SECTION f3543191335117632af7ee612dc7b1f5e65f92ba)\n### Update index.rst\n\n> Commit: [f3543191335117632af7ee612dc7b1f5e65f92ba](https://github.com/dOpensource/dsiprouter/commit/f3543191335117632af7ee612dc7b1f5e65f92ba)  \n> Date: Mon, 17 Dec 2018 10:21:30 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f3543191335117632af7ee612dc7b1f5e65f92ba)\n[//]: # (START_SECTION 9dfa4e37ad094fb0c2076e0d03f547700c9a2250)\n### Fixed domain support\n\n> Commit: [9dfa4e37ad094fb0c2076e0d03f547700c9a2250](https://github.com/dOpensource/dsiprouter/commit/9dfa4e37ad094fb0c2076e0d03f547700c9a2250)  \n> Date: Mon, 17 Dec 2018 12:02:33 +0000  \n> Author: root (root@debian-dsip-test.localdomain)  \n> Committer: root (root@debian-dsip-test.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9dfa4e37ad094fb0c2076e0d03f547700c9a2250)\n[//]: # (START_SECTION c6ca8da2268aeb7471a4a7e7ecb0a91b6c1403f7)\n### Update index.rst\n\n> Commit: [c6ca8da2268aeb7471a4a7e7ecb0a91b6c1403f7](https://github.com/dOpensource/dsiprouter/commit/c6ca8da2268aeb7471a4a7e7ecb0a91b6c1403f7)  \n> Date: Sat, 15 Dec 2018 04:37:31 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c6ca8da2268aeb7471a4a7e7ecb0a91b6c1403f7)\n[//]: # (START_SECTION a2ea8cd20379fda7bde5886f59498235de34ad2d)\n### Update upgrade.rst\n\n> Commit: [a2ea8cd20379fda7bde5886f59498235de34ad2d](https://github.com/dOpensource/dsiprouter/commit/a2ea8cd20379fda7bde5886f59498235de34ad2d)  \n> Date: Fri, 14 Dec 2018 15:46:54 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a2ea8cd20379fda7bde5886f59498235de34ad2d)\n[//]: # (START_SECTION cbd26d3ca965a86c1ca286e0291ea0b7cd6faf31)\n### Update upgrade.rst\n\n> Commit: [cbd26d3ca965a86c1ca286e0291ea0b7cd6faf31](https://github.com/dOpensource/dsiprouter/commit/cbd26d3ca965a86c1ca286e0291ea0b7cd6faf31)  \n> Date: Fri, 14 Dec 2018 15:41:52 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cbd26d3ca965a86c1ca286e0291ea0b7cd6faf31)\n[//]: # (START_SECTION ce8adf1b5273038f21dfc5dd8b7b74249e395a95)\n### Update upgrade.rst\n\n> Commit: [ce8adf1b5273038f21dfc5dd8b7b74249e395a95](https://github.com/dOpensource/dsiprouter/commit/ce8adf1b5273038f21dfc5dd8b7b74249e395a95)  \n> Date: Fri, 14 Dec 2018 15:35:55 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ce8adf1b5273038f21dfc5dd8b7b74249e395a95)\n[//]: # (START_SECTION 3dc44779f8e0875e678ea03c20e17c9d445024ad)\n### Update upgrade.rst\n\n> Commit: [3dc44779f8e0875e678ea03c20e17c9d445024ad](https://github.com/dOpensource/dsiprouter/commit/3dc44779f8e0875e678ea03c20e17c9d445024ad)  \n> Date: Fri, 14 Dec 2018 15:15:13 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3dc44779f8e0875e678ea03c20e17c9d445024ad)\n[//]: # (START_SECTION 6237b951c3b52feae6841d91aa8e10a25f583921)\n### Update upgrade.rst\n\n> Commit: [6237b951c3b52feae6841d91aa8e10a25f583921](https://github.com/dOpensource/dsiprouter/commit/6237b951c3b52feae6841d91aa8e10a25f583921)  \n> Date: Fri, 14 Dec 2018 15:13:54 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6237b951c3b52feae6841d91aa8e10a25f583921)\n[//]: # (START_SECTION b8346484331f3344c4b5c1f9caa75cdc542861e2)\n### Add files via upload\n\n> Commit: [b8346484331f3344c4b5c1f9caa75cdc542861e2](https://github.com/dOpensource/dsiprouter/commit/b8346484331f3344c4b5c1f9caa75cdc542861e2)  \n> Date: Fri, 14 Dec 2018 15:12:23 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b8346484331f3344c4b5c1f9caa75cdc542861e2)\n[//]: # (START_SECTION 03adc3156bd16c5fcdbb83619bc0770524930361)\n### Update upgrade.rst\n\n> Commit: [03adc3156bd16c5fcdbb83619bc0770524930361](https://github.com/dOpensource/dsiprouter/commit/03adc3156bd16c5fcdbb83619bc0770524930361)  \n> Date: Fri, 14 Dec 2018 15:11:50 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 03adc3156bd16c5fcdbb83619bc0770524930361)\n[//]: # (START_SECTION 3cb41569351838ab46ff9f533d06d9b4ae3d89fc)\n### Update upgrade.rst\n\n> Commit: [3cb41569351838ab46ff9f533d06d9b4ae3d89fc](https://github.com/dOpensource/dsiprouter/commit/3cb41569351838ab46ff9f533d06d9b4ae3d89fc)  \n> Date: Fri, 14 Dec 2018 15:01:42 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3cb41569351838ab46ff9f533d06d9b4ae3d89fc)\n[//]: # (START_SECTION f01c855e592c9851b9c55d7db90b5969fd426868)\n### Update upgrade.rst\n\n> Commit: [f01c855e592c9851b9c55d7db90b5969fd426868](https://github.com/dOpensource/dsiprouter/commit/f01c855e592c9851b9c55d7db90b5969fd426868)  \n> Date: Fri, 14 Dec 2018 14:59:06 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f01c855e592c9851b9c55d7db90b5969fd426868)\n[//]: # (START_SECTION 57395c0f477f7c865266322d3556c7583a5e72a6)\n### Update upgrade.rst\n\n> Commit: [57395c0f477f7c865266322d3556c7583a5e72a6](https://github.com/dOpensource/dsiprouter/commit/57395c0f477f7c865266322d3556c7583a5e72a6)  \n> Date: Fri, 14 Dec 2018 14:51:20 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 57395c0f477f7c865266322d3556c7583a5e72a6)\n[//]: # (START_SECTION 8aa1830a3e65eb9e093a055d59110c69094d6242)\n### Update upgrade.rst\n\n> Commit: [8aa1830a3e65eb9e093a055d59110c69094d6242](https://github.com/dOpensource/dsiprouter/commit/8aa1830a3e65eb9e093a055d59110c69094d6242)  \n> Date: Fri, 14 Dec 2018 14:49:01 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8aa1830a3e65eb9e093a055d59110c69094d6242)\n[//]: # (START_SECTION 9f19dcce147e3b82ba5d8a3fea49332c13503366)\n### Update upgrade.rst\n\n> Commit: [9f19dcce147e3b82ba5d8a3fea49332c13503366](https://github.com/dOpensource/dsiprouter/commit/9f19dcce147e3b82ba5d8a3fea49332c13503366)  \n> Date: Fri, 14 Dec 2018 14:21:53 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9f19dcce147e3b82ba5d8a3fea49332c13503366)\n[//]: # (START_SECTION 746ec4c40b56b3f5cf26d83e5c1bc11a4f21359b)\n### Update index.rst\n\n> Commit: [746ec4c40b56b3f5cf26d83e5c1bc11a4f21359b](https://github.com/dOpensource/dsiprouter/commit/746ec4c40b56b3f5cf26d83e5c1bc11a4f21359b)  \n> Date: Fri, 14 Dec 2018 14:18:57 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 746ec4c40b56b3f5cf26d83e5c1bc11a4f21359b)\n[//]: # (START_SECTION 3c1c4292af8ec1c90c5b070aa9048bd430f1844b)\n### Create upgrade.rst\n\n> Commit: [3c1c4292af8ec1c90c5b070aa9048bd430f1844b](https://github.com/dOpensource/dsiprouter/commit/3c1c4292af8ec1c90c5b070aa9048bd430f1844b)  \n> Date: Fri, 14 Dec 2018 14:14:38 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3c1c4292af8ec1c90c5b070aa9048bd430f1844b)\n[//]: # (START_SECTION a59a27bda0a5ba38bc4221b9e7855319e7ab36c1)\n### Update index.rst\n\n> Commit: [a59a27bda0a5ba38bc4221b9e7855319e7ab36c1](https://github.com/dOpensource/dsiprouter/commit/a59a27bda0a5ba38bc4221b9e7855319e7ab36c1)  \n> Date: Fri, 14 Dec 2018 14:12:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a59a27bda0a5ba38bc4221b9e7855319e7ab36c1)\n[//]: # (START_SECTION 09bf9da137d19cbb57e15cdece55bff490238364)\n### Update README.md\n\n> Commit: [09bf9da137d19cbb57e15cdece55bff490238364](https://github.com/dOpensource/dsiprouter/commit/09bf9da137d19cbb57e15cdece55bff490238364)  \n> Date: Fri, 14 Dec 2018 13:40:17 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 09bf9da137d19cbb57e15cdece55bff490238364)\n[//]: # (START_SECTION f9afd41bcdc50468b2b12333789f3ecaa598443c)\n### Update README.md\n\n> Commit: [f9afd41bcdc50468b2b12333789f3ecaa598443c](https://github.com/dOpensource/dsiprouter/commit/f9afd41bcdc50468b2b12333789f3ecaa598443c)  \n> Date: Fri, 14 Dec 2018 13:39:57 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f9afd41bcdc50468b2b12333789f3ecaa598443c)\n[//]: # (START_SECTION 0835acd461d8403200f1dcbdc6840146651d3dc5)\n### Update index.rst\n\n> Commit: [0835acd461d8403200f1dcbdc6840146651d3dc5](https://github.com/dOpensource/dsiprouter/commit/0835acd461d8403200f1dcbdc6840146651d3dc5)  \n> Date: Fri, 14 Dec 2018 13:13:52 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0835acd461d8403200f1dcbdc6840146651d3dc5)\n[//]: # (START_SECTION 0bc8459a29b18b69b630ff293e1b5cbdfe240353)\n### Update resources.rst\n\n> Commit: [0bc8459a29b18b69b630ff293e1b5cbdfe240353](https://github.com/dOpensource/dsiprouter/commit/0bc8459a29b18b69b630ff293e1b5cbdfe240353)  \n> Date: Fri, 14 Dec 2018 13:11:29 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0bc8459a29b18b69b630ff293e1b5cbdfe240353)\n[//]: # (START_SECTION 0d81ecee5c630442421dd4bef7825de9841e189e)\n### Update configuring.rst\n\n> Commit: [0d81ecee5c630442421dd4bef7825de9841e189e](https://github.com/dOpensource/dsiprouter/commit/0d81ecee5c630442421dd4bef7825de9841e189e)  \n> Date: Fri, 14 Dec 2018 13:08:13 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0d81ecee5c630442421dd4bef7825de9841e189e)\n[//]: # (START_SECTION 392844aa937cc2abb498a179debd22f8b48b8072)\n### Update resources.rst\n\n> Commit: [392844aa937cc2abb498a179debd22f8b48b8072](https://github.com/dOpensource/dsiprouter/commit/392844aa937cc2abb498a179debd22f8b48b8072)  \n> Date: Fri, 14 Dec 2018 13:06:36 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 392844aa937cc2abb498a179debd22f8b48b8072)\n[//]: # (START_SECTION 92f73f2a4e887e1a18c4fbbf58e6e29f0672990e)\n### Update configuring.rst\n\n> Commit: [92f73f2a4e887e1a18c4fbbf58e6e29f0672990e](https://github.com/dOpensource/dsiprouter/commit/92f73f2a4e887e1a18c4fbbf58e6e29f0672990e)  \n> Date: Fri, 14 Dec 2018 13:04:33 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 92f73f2a4e887e1a18c4fbbf58e6e29f0672990e)\n[//]: # (START_SECTION 9d43e76dd04598dff4e2d90dc086ce1126c75a8f)\n### Update index.rst\n\n> Commit: [9d43e76dd04598dff4e2d90dc086ce1126c75a8f](https://github.com/dOpensource/dsiprouter/commit/9d43e76dd04598dff4e2d90dc086ce1126c75a8f)  \n> Date: Fri, 14 Dec 2018 12:55:38 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9d43e76dd04598dff4e2d90dc086ce1126c75a8f)\n[//]: # (START_SECTION 94d678fd6d46196d9c35a0eef6ba84ae9b794b5b)\n### Fixed the Global Outbound Route issue that prevented routes from being saved\n\n> Commit: [94d678fd6d46196d9c35a0eef6ba84ae9b794b5b](https://github.com/dOpensource/dsiprouter/commit/94d678fd6d46196d9c35a0eef6ba84ae9b794b5b)  \n> Date: Fri, 14 Dec 2018 17:50:50 +0000  \n> Author: root (root@debian-dsip-test.localdomain)  \n> Committer: root (root@debian-dsip-test.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 94d678fd6d46196d9c35a0eef6ba84ae9b794b5b)\n[//]: # (START_SECTION 1c49f296e9184f7e2bcbfada9de19d2dca8d315f)\n### Update configuring.rst\n\n> Commit: [1c49f296e9184f7e2bcbfada9de19d2dca8d315f](https://github.com/dOpensource/dsiprouter/commit/1c49f296e9184f7e2bcbfada9de19d2dca8d315f)  \n> Date: Fri, 14 Dec 2018 12:50:28 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1c49f296e9184f7e2bcbfada9de19d2dca8d315f)\n[//]: # (START_SECTION 0434d161c35d7fd70eb1a28a9376fdd55e8af2ea)\n### Update index.rst\n\n> Commit: [0434d161c35d7fd70eb1a28a9376fdd55e8af2ea](https://github.com/dOpensource/dsiprouter/commit/0434d161c35d7fd70eb1a28a9376fdd55e8af2ea)  \n> Date: Fri, 14 Dec 2018 12:48:59 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0434d161c35d7fd70eb1a28a9376fdd55e8af2ea)\n[//]: # (START_SECTION 4922aad478a5f06bdacec005c741dd4fef76a13f)\n### Update configuring.rst\n\n> Commit: [4922aad478a5f06bdacec005c741dd4fef76a13f](https://github.com/dOpensource/dsiprouter/commit/4922aad478a5f06bdacec005c741dd4fef76a13f)  \n> Date: Fri, 14 Dec 2018 12:02:35 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4922aad478a5f06bdacec005c741dd4fef76a13f)\n[//]: # (START_SECTION a490a980257e7fbd63b67accfbecfaeab2ee6150)\n### Update index.rst\n\n> Commit: [a490a980257e7fbd63b67accfbecfaeab2ee6150](https://github.com/dOpensource/dsiprouter/commit/a490a980257e7fbd63b67accfbecfaeab2ee6150)  \n> Date: Fri, 14 Dec 2018 11:58:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a490a980257e7fbd63b67accfbecfaeab2ee6150)\n[//]: # (START_SECTION c1200780b220e4b1793e392ef033325e1feb371c)\n### Update use-cases.rst\n\n> Commit: [c1200780b220e4b1793e392ef033325e1feb371c](https://github.com/dOpensource/dsiprouter/commit/c1200780b220e4b1793e392ef033325e1feb371c)  \n> Date: Fri, 14 Dec 2018 11:29:57 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c1200780b220e4b1793e392ef033325e1feb371c)\n[//]: # (START_SECTION a725efdb0090b911f25c5d59c2c80fc2fc4e71f5)\n### Update use-cases.rst\n\n> Commit: [a725efdb0090b911f25c5d59c2c80fc2fc4e71f5](https://github.com/dOpensource/dsiprouter/commit/a725efdb0090b911f25c5d59c2c80fc2fc4e71f5)  \n> Date: Fri, 14 Dec 2018 11:25:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a725efdb0090b911f25c5d59c2c80fc2fc4e71f5)\n[//]: # (START_SECTION 09aa41c30a0f6f4f22f4f10680ddb540acfbe892)\n### Update use-cases.rst\n\n> Commit: [09aa41c30a0f6f4f22f4f10680ddb540acfbe892](https://github.com/dOpensource/dsiprouter/commit/09aa41c30a0f6f4f22f4f10680ddb540acfbe892)  \n> Date: Fri, 14 Dec 2018 11:25:06 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 09aa41c30a0f6f4f22f4f10680ddb540acfbe892)\n[//]: # (START_SECTION f4f8ab64127207693ac48d3ff6cf3ddfdfa41aea)\n### Update use-cases.rst\n\n> Commit: [f4f8ab64127207693ac48d3ff6cf3ddfdfa41aea](https://github.com/dOpensource/dsiprouter/commit/f4f8ab64127207693ac48d3ff6cf3ddfdfa41aea)  \n> Date: Fri, 14 Dec 2018 11:22:44 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f4f8ab64127207693ac48d3ff6cf3ddfdfa41aea)\n[//]: # (START_SECTION 8f68f34dfaffc9a8f17f12472a5e5fb8581e5219)\n### Add files via upload\n\n> Commit: [8f68f34dfaffc9a8f17f12472a5e5fb8581e5219](https://github.com/dOpensource/dsiprouter/commit/8f68f34dfaffc9a8f17f12472a5e5fb8581e5219)  \n> Date: Fri, 14 Dec 2018 11:21:11 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8f68f34dfaffc9a8f17f12472a5e5fb8581e5219)\n[//]: # (START_SECTION 96a104220f748e43a43efd15249e1ee2fe932144)\n### Update use-cases.rst\n\n> Commit: [96a104220f748e43a43efd15249e1ee2fe932144](https://github.com/dOpensource/dsiprouter/commit/96a104220f748e43a43efd15249e1ee2fe932144)  \n> Date: Fri, 14 Dec 2018 10:34:25 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 96a104220f748e43a43efd15249e1ee2fe932144)\n[//]: # (START_SECTION cda111f1c8a28c8abdc93ce2843b2c5193173b89)\n### Update use-cases.rst\n\n> Commit: [cda111f1c8a28c8abdc93ce2843b2c5193173b89](https://github.com/dOpensource/dsiprouter/commit/cda111f1c8a28c8abdc93ce2843b2c5193173b89)  \n> Date: Thu, 13 Dec 2018 21:34:56 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cda111f1c8a28c8abdc93ce2843b2c5193173b89)\n[//]: # (START_SECTION 10d61b696779138984cd6393a3de536d90c4102e)\n### Update use-cases.rst\n\n> Commit: [10d61b696779138984cd6393a3de536d90c4102e](https://github.com/dOpensource/dsiprouter/commit/10d61b696779138984cd6393a3de536d90c4102e)  \n> Date: Thu, 13 Dec 2018 21:33:21 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 10d61b696779138984cd6393a3de536d90c4102e)\n[//]: # (START_SECTION 60cebcc2a64b4b9bd3e80cbdeffc0b8ffa6ddd24)\n### Update use-cases.rst\n\n> Commit: [60cebcc2a64b4b9bd3e80cbdeffc0b8ffa6ddd24](https://github.com/dOpensource/dsiprouter/commit/60cebcc2a64b4b9bd3e80cbdeffc0b8ffa6ddd24)  \n> Date: Thu, 13 Dec 2018 21:11:59 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 60cebcc2a64b4b9bd3e80cbdeffc0b8ffa6ddd24)\n[//]: # (START_SECTION 0b6851cee7a0aca9664ec2f09091514374cdd819)\n### Update use-cases.rst\n\n> Commit: [0b6851cee7a0aca9664ec2f09091514374cdd819](https://github.com/dOpensource/dsiprouter/commit/0b6851cee7a0aca9664ec2f09091514374cdd819)  \n> Date: Thu, 13 Dec 2018 21:10:25 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0b6851cee7a0aca9664ec2f09091514374cdd819)\n[//]: # (START_SECTION 2b7aedc107c5b7e8cf17d71e96ee760f6d0b38f3)\n### Update use-cases.rst\n\n> Commit: [2b7aedc107c5b7e8cf17d71e96ee760f6d0b38f3](https://github.com/dOpensource/dsiprouter/commit/2b7aedc107c5b7e8cf17d71e96ee760f6d0b38f3)  \n> Date: Thu, 13 Dec 2018 21:05:25 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2b7aedc107c5b7e8cf17d71e96ee760f6d0b38f3)\n[//]: # (START_SECTION 663bc588faa0d3c7773adc1d841bee6379b9d46f)\n### Add files via upload\n\n> Commit: [663bc588faa0d3c7773adc1d841bee6379b9d46f](https://github.com/dOpensource/dsiprouter/commit/663bc588faa0d3c7773adc1d841bee6379b9d46f)  \n> Date: Thu, 13 Dec 2018 21:04:21 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 663bc588faa0d3c7773adc1d841bee6379b9d46f)\n[//]: # (START_SECTION 60f17c1538c2db3020056e6ef23c76456f738b90)\n### Update use-cases.rst\n\n> Commit: [60f17c1538c2db3020056e6ef23c76456f738b90](https://github.com/dOpensource/dsiprouter/commit/60f17c1538c2db3020056e6ef23c76456f738b90)  \n> Date: Thu, 13 Dec 2018 21:03:33 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 60f17c1538c2db3020056e6ef23c76456f738b90)\n[//]: # (START_SECTION 55e5e7ea66be6e2f2b0a2f98e6ea78312ba3dc1c)\n### Add files via upload\n\n> Commit: [55e5e7ea66be6e2f2b0a2f98e6ea78312ba3dc1c](https://github.com/dOpensource/dsiprouter/commit/55e5e7ea66be6e2f2b0a2f98e6ea78312ba3dc1c)  \n> Date: Thu, 13 Dec 2018 20:51:06 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 55e5e7ea66be6e2f2b0a2f98e6ea78312ba3dc1c)\n[//]: # (START_SECTION 922f88419d1403bc1e08e30b49070594081ce187)\n### Update use-cases.rst\n\n> Commit: [922f88419d1403bc1e08e30b49070594081ce187](https://github.com/dOpensource/dsiprouter/commit/922f88419d1403bc1e08e30b49070594081ce187)  \n> Date: Thu, 13 Dec 2018 20:50:38 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 922f88419d1403bc1e08e30b49070594081ce187)\n[//]: # (START_SECTION 08da76f6d09eb31073cc12c71bca1f645a66cec3)\n### Update use-cases.rst\n\n> Commit: [08da76f6d09eb31073cc12c71bca1f645a66cec3](https://github.com/dOpensource/dsiprouter/commit/08da76f6d09eb31073cc12c71bca1f645a66cec3)  \n> Date: Thu, 13 Dec 2018 20:45:03 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 08da76f6d09eb31073cc12c71bca1f645a66cec3)\n[//]: # (START_SECTION a56861ee45b5ff1b1d90fff3f905785ba6484a9e)\n### Update use-cases.rst\n\n> Commit: [a56861ee45b5ff1b1d90fff3f905785ba6484a9e](https://github.com/dOpensource/dsiprouter/commit/a56861ee45b5ff1b1d90fff3f905785ba6484a9e)  \n> Date: Thu, 13 Dec 2018 20:43:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a56861ee45b5ff1b1d90fff3f905785ba6484a9e)\n[//]: # (START_SECTION 2a5e280ea37fdb94fcc120d03369b5ede10e128d)\n### Update use-cases.rst\n\n> Commit: [2a5e280ea37fdb94fcc120d03369b5ede10e128d](https://github.com/dOpensource/dsiprouter/commit/2a5e280ea37fdb94fcc120d03369b5ede10e128d)  \n> Date: Thu, 13 Dec 2018 20:41:44 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2a5e280ea37fdb94fcc120d03369b5ede10e128d)\n[//]: # (START_SECTION d1aa338f6885a69c3e695e3cb3efe83d62caaa09)\n### Update use-cases.rst\n\n> Commit: [d1aa338f6885a69c3e695e3cb3efe83d62caaa09](https://github.com/dOpensource/dsiprouter/commit/d1aa338f6885a69c3e695e3cb3efe83d62caaa09)  \n> Date: Thu, 13 Dec 2018 20:38:29 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d1aa338f6885a69c3e695e3cb3efe83d62caaa09)\n[//]: # (START_SECTION 6ba9817c4b49b3d3d5624d8f28026b4ff4a2cddd)\n### Update use-cases.rst\n\n> Commit: [6ba9817c4b49b3d3d5624d8f28026b4ff4a2cddd](https://github.com/dOpensource/dsiprouter/commit/6ba9817c4b49b3d3d5624d8f28026b4ff4a2cddd)  \n> Date: Thu, 13 Dec 2018 20:36:35 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6ba9817c4b49b3d3d5624d8f28026b4ff4a2cddd)\n[//]: # (START_SECTION 39f04ba4cee1077a03d713be74b12e1d72040060)\n### Update use-cases.rst\n\n> Commit: [39f04ba4cee1077a03d713be74b12e1d72040060](https://github.com/dOpensource/dsiprouter/commit/39f04ba4cee1077a03d713be74b12e1d72040060)  \n> Date: Thu, 13 Dec 2018 20:32:25 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 39f04ba4cee1077a03d713be74b12e1d72040060)\n[//]: # (START_SECTION e215925241dd1e34b67b1cc5f948096769ac2ada)\n### Update use-cases.rst\n\n> Commit: [e215925241dd1e34b67b1cc5f948096769ac2ada](https://github.com/dOpensource/dsiprouter/commit/e215925241dd1e34b67b1cc5f948096769ac2ada)  \n> Date: Thu, 13 Dec 2018 20:19:36 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e215925241dd1e34b67b1cc5f948096769ac2ada)\n[//]: # (START_SECTION 3486102b7ce136fe13d4fcaeba2e8af2b9a15e63)\n### Update use-cases.rst\n\n> Commit: [3486102b7ce136fe13d4fcaeba2e8af2b9a15e63](https://github.com/dOpensource/dsiprouter/commit/3486102b7ce136fe13d4fcaeba2e8af2b9a15e63)  \n> Date: Thu, 13 Dec 2018 20:17:17 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3486102b7ce136fe13d4fcaeba2e8af2b9a15e63)\n[//]: # (START_SECTION 115df31ed442fdc46f586ad953306fa5ae3d375e)\n### Add files via upload\n\n> Commit: [115df31ed442fdc46f586ad953306fa5ae3d375e](https://github.com/dOpensource/dsiprouter/commit/115df31ed442fdc46f586ad953306fa5ae3d375e)  \n> Date: Thu, 13 Dec 2018 20:15:20 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 115df31ed442fdc46f586ad953306fa5ae3d375e)\n[//]: # (START_SECTION 060b00fc8acc7fc52e3b6d944e5b306170393850)\n### Update use-cases.rst\n\n> Commit: [060b00fc8acc7fc52e3b6d944e5b306170393850](https://github.com/dOpensource/dsiprouter/commit/060b00fc8acc7fc52e3b6d944e5b306170393850)  \n> Date: Thu, 13 Dec 2018 20:11:24 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 060b00fc8acc7fc52e3b6d944e5b306170393850)\n[//]: # (START_SECTION 5069e7ddda117ad784c8124d6d29081ab8ad53a4)\n### Update pbxs_and_endpoints.rst\n\n> Commit: [5069e7ddda117ad784c8124d6d29081ab8ad53a4](https://github.com/dOpensource/dsiprouter/commit/5069e7ddda117ad784c8124d6d29081ab8ad53a4)  \n> Date: Thu, 13 Dec 2018 13:06:12 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5069e7ddda117ad784c8124d6d29081ab8ad53a4)\n[//]: # (START_SECTION 6598db3e26ae44c4ac858e846963c4a24c209dfa)\n### Update pbxs_and_endpoints.rst\n\n> Commit: [6598db3e26ae44c4ac858e846963c4a24c209dfa](https://github.com/dOpensource/dsiprouter/commit/6598db3e26ae44c4ac858e846963c4a24c209dfa)  \n> Date: Thu, 13 Dec 2018 13:03:34 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6598db3e26ae44c4ac858e846963c4a24c209dfa)\n[//]: # (START_SECTION b39e2701253246c1ddbb41a7c4ace9bdbf88c587)\n### Update pbxs_and_endpoints.rst\n\n> Commit: [b39e2701253246c1ddbb41a7c4ace9bdbf88c587](https://github.com/dOpensource/dsiprouter/commit/b39e2701253246c1ddbb41a7c4ace9bdbf88c587)  \n> Date: Thu, 13 Dec 2018 13:00:39 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b39e2701253246c1ddbb41a7c4ace9bdbf88c587)\n[//]: # (START_SECTION cf978ec560f604e6d9e2ac00c94af68ab8321620)\n### Update use-cases.rst\n\n> Commit: [cf978ec560f604e6d9e2ac00c94af68ab8321620](https://github.com/dOpensource/dsiprouter/commit/cf978ec560f604e6d9e2ac00c94af68ab8321620)  \n> Date: Thu, 13 Dec 2018 11:17:20 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cf978ec560f604e6d9e2ac00c94af68ab8321620)\n[//]: # (START_SECTION 8973ac89c3fbb08f72f46fd6785bcdd3f4cac8dd)\n### Update use-cases.rst\n\n> Commit: [8973ac89c3fbb08f72f46fd6785bcdd3f4cac8dd](https://github.com/dOpensource/dsiprouter/commit/8973ac89c3fbb08f72f46fd6785bcdd3f4cac8dd)  \n> Date: Thu, 13 Dec 2018 10:43:48 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8973ac89c3fbb08f72f46fd6785bcdd3f4cac8dd)\n[//]: # (START_SECTION b30a139ad0c87fb0f02c90f29481f40f12ca808b)\n### Update use-cases.rst\n\n> Commit: [b30a139ad0c87fb0f02c90f29481f40f12ca808b](https://github.com/dOpensource/dsiprouter/commit/b30a139ad0c87fb0f02c90f29481f40f12ca808b)  \n> Date: Thu, 13 Dec 2018 10:16:23 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b30a139ad0c87fb0f02c90f29481f40f12ca808b)\n[//]: # (START_SECTION db3bc54cf0fee96e609fd94e8714f02a064a91b7)\n### Update use-cases.rst\n\n> Commit: [db3bc54cf0fee96e609fd94e8714f02a064a91b7](https://github.com/dOpensource/dsiprouter/commit/db3bc54cf0fee96e609fd94e8714f02a064a91b7)  \n> Date: Thu, 13 Dec 2018 10:12:12 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION db3bc54cf0fee96e609fd94e8714f02a064a91b7)\n[//]: # (START_SECTION f3353970df8f5514290769b898a9368d369b7f4a)\n### Update use-cases.rst\n\n> Commit: [f3353970df8f5514290769b898a9368d369b7f4a](https://github.com/dOpensource/dsiprouter/commit/f3353970df8f5514290769b898a9368d369b7f4a)  \n> Date: Thu, 13 Dec 2018 10:08:30 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f3353970df8f5514290769b898a9368d369b7f4a)\n[//]: # (START_SECTION af41bfe6cb1e93304b43a2b806ce9de97d0a3677)\n### Update use-cases.rst\n\n> Commit: [af41bfe6cb1e93304b43a2b806ce9de97d0a3677](https://github.com/dOpensource/dsiprouter/commit/af41bfe6cb1e93304b43a2b806ce9de97d0a3677)  \n> Date: Thu, 13 Dec 2018 10:06:35 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION af41bfe6cb1e93304b43a2b806ce9de97d0a3677)\n[//]: # (START_SECTION 1fbcbd0f6fbf67d8f84918621833e1d6e63de439)\n### Update use-cases.rst\n\n> Commit: [1fbcbd0f6fbf67d8f84918621833e1d6e63de439](https://github.com/dOpensource/dsiprouter/commit/1fbcbd0f6fbf67d8f84918621833e1d6e63de439)  \n> Date: Thu, 13 Dec 2018 10:02:24 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1fbcbd0f6fbf67d8f84918621833e1d6e63de439)\n[//]: # (START_SECTION 20933ae40f783684e90147b154933d86028867f5)\n### Update use-cases.rst\n\n> Commit: [20933ae40f783684e90147b154933d86028867f5](https://github.com/dOpensource/dsiprouter/commit/20933ae40f783684e90147b154933d86028867f5)  \n> Date: Thu, 13 Dec 2018 10:01:24 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 20933ae40f783684e90147b154933d86028867f5)\n[//]: # (START_SECTION 9441c946a1b67c621e7647c0bf93c3ecbfc2ad38)\n### Update use-cases.rst\n\n> Commit: [9441c946a1b67c621e7647c0bf93c3ecbfc2ad38](https://github.com/dOpensource/dsiprouter/commit/9441c946a1b67c621e7647c0bf93c3ecbfc2ad38)  \n> Date: Thu, 13 Dec 2018 09:58:57 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9441c946a1b67c621e7647c0bf93c3ecbfc2ad38)\n[//]: # (START_SECTION 3ff193d807dfd3b4511a567bcaa5db5ccf717d39)\n### Update use-cases.rst\n\n> Commit: [3ff193d807dfd3b4511a567bcaa5db5ccf717d39](https://github.com/dOpensource/dsiprouter/commit/3ff193d807dfd3b4511a567bcaa5db5ccf717d39)  \n> Date: Thu, 13 Dec 2018 09:57:38 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3ff193d807dfd3b4511a567bcaa5db5ccf717d39)\n[//]: # (START_SECTION ff658fa38a9f9e038357decb831e35181656b0a7)\n### Update use-cases.rst\n\n> Commit: [ff658fa38a9f9e038357decb831e35181656b0a7](https://github.com/dOpensource/dsiprouter/commit/ff658fa38a9f9e038357decb831e35181656b0a7)  \n> Date: Thu, 13 Dec 2018 09:55:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ff658fa38a9f9e038357decb831e35181656b0a7)\n[//]: # (START_SECTION bc5465e7b036b3f8b15200103f8006ba54207617)\n### Update use-cases.rst\n\n> Commit: [bc5465e7b036b3f8b15200103f8006ba54207617](https://github.com/dOpensource/dsiprouter/commit/bc5465e7b036b3f8b15200103f8006ba54207617)  \n> Date: Thu, 13 Dec 2018 09:54:21 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bc5465e7b036b3f8b15200103f8006ba54207617)\n[//]: # (START_SECTION 5928ca15b6f45e8fafd468d09000c4da765b3267)\n### Fixed Javascript error\n\n> Commit: [5928ca15b6f45e8fafd468d09000c4da765b3267](https://github.com/dOpensource/dsiprouter/commit/5928ca15b6f45e8fafd468d09000c4da765b3267)  \n> Date: Thu, 13 Dec 2018 14:22:16 +0000  \n> Author: root (root@debian-dsip-test.localdomain)  \n> Committer: root (root@debian-dsip-test.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5928ca15b6f45e8fafd468d09000c4da765b3267)\n[//]: # (START_SECTION 65afd1f6e7515175ba2ecd3b6d86e34c39a3a143)\n### Rename Resources.rst to resources.rst\n\n> Commit: [65afd1f6e7515175ba2ecd3b6d86e34c39a3a143](https://github.com/dOpensource/dsiprouter/commit/65afd1f6e7515175ba2ecd3b6d86e34c39a3a143)  \n> Date: Wed, 12 Dec 2018 15:20:34 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 65afd1f6e7515175ba2ecd3b6d86e34c39a3a143)\n[//]: # (START_SECTION 7d1c22dc0e55a181f232f58d52ac0adca49f856c)\n### Update Resources.rst\n\n> Commit: [7d1c22dc0e55a181f232f58d52ac0adca49f856c](https://github.com/dOpensource/dsiprouter/commit/7d1c22dc0e55a181f232f58d52ac0adca49f856c)  \n> Date: Wed, 12 Dec 2018 15:19:21 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7d1c22dc0e55a181f232f58d52ac0adca49f856c)\n[//]: # (START_SECTION 12b76eb311777c6b1671e6f475b40205d7b804b2)\n### Update Resources.rst\n\n> Commit: [12b76eb311777c6b1671e6f475b40205d7b804b2](https://github.com/dOpensource/dsiprouter/commit/12b76eb311777c6b1671e6f475b40205d7b804b2)  \n> Date: Wed, 12 Dec 2018 13:58:47 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 12b76eb311777c6b1671e6f475b40205d7b804b2)\n[//]: # (START_SECTION a721002c199cc70bc0c18557cba3e8c537d6744b)\n### Update configuring.rst\n\n> Commit: [a721002c199cc70bc0c18557cba3e8c537d6744b](https://github.com/dOpensource/dsiprouter/commit/a721002c199cc70bc0c18557cba3e8c537d6744b)  \n> Date: Wed, 12 Dec 2018 13:58:08 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a721002c199cc70bc0c18557cba3e8c537d6744b)\n[//]: # (START_SECTION 2642451f9b3d61c5bc1f9a247f38cc695e043ff4)\n### Update Resources.rst\n\n> Commit: [2642451f9b3d61c5bc1f9a247f38cc695e043ff4](https://github.com/dOpensource/dsiprouter/commit/2642451f9b3d61c5bc1f9a247f38cc695e043ff4)  \n> Date: Wed, 12 Dec 2018 13:53:12 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2642451f9b3d61c5bc1f9a247f38cc695e043ff4)\n[//]: # (START_SECTION 3717ad63180779f1f3119ce38b8b29c0b637c827)\n### Create Resources.rst\n\n> Commit: [3717ad63180779f1f3119ce38b8b29c0b637c827](https://github.com/dOpensource/dsiprouter/commit/3717ad63180779f1f3119ce38b8b29c0b637c827)  \n> Date: Wed, 12 Dec 2018 13:46:16 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3717ad63180779f1f3119ce38b8b29c0b637c827)\n[//]: # (START_SECTION cf20c150ef0acb3c2c61b4a8fbe6015c8a924ea9)\n### Add files via upload\n\n> Commit: [cf20c150ef0acb3c2c61b4a8fbe6015c8a924ea9](https://github.com/dOpensource/dsiprouter/commit/cf20c150ef0acb3c2c61b4a8fbe6015c8a924ea9)  \n> Date: Wed, 12 Dec 2018 13:24:23 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cf20c150ef0acb3c2c61b4a8fbe6015c8a924ea9)\n[//]: # (START_SECTION 584eb58f48b8ce5365a92c75407db692001878db)\n### Update pbxs_and_endpoints.rst\n\n> Commit: [584eb58f48b8ce5365a92c75407db692001878db](https://github.com/dOpensource/dsiprouter/commit/584eb58f48b8ce5365a92c75407db692001878db)  \n> Date: Wed, 12 Dec 2018 13:17:54 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 584eb58f48b8ce5365a92c75407db692001878db)\n[//]: # (START_SECTION 7555cb78caca688287a315ac3ae44e92e2717488)\n### Fixed issues with FusionPBX Sync and the ability to delete PBX's\n\n> Commit: [7555cb78caca688287a315ac3ae44e92e2717488](https://github.com/dOpensource/dsiprouter/commit/7555cb78caca688287a315ac3ae44e92e2717488)  \n> Date: Tue, 11 Dec 2018 22:13:47 +0000  \n> Author: root (root@debian-dsip-test.localdomain)  \n> Committer: root (root@debian-dsip-test.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7555cb78caca688287a315ac3ae44e92e2717488)\n[//]: # (START_SECTION 0f214bed7a7cb4fce58819d54234dc67538f897a)\n### Update use-cases.rst\n\n> Commit: [0f214bed7a7cb4fce58819d54234dc67538f897a](https://github.com/dOpensource/dsiprouter/commit/0f214bed7a7cb4fce58819d54234dc67538f897a)  \n> Date: Tue, 11 Dec 2018 12:16:18 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0f214bed7a7cb4fce58819d54234dc67538f897a)\n[//]: # (START_SECTION 1c7454dac153ab89baec68da1e74cdff26a5e476)\n### Add files via upload\n\n> Commit: [1c7454dac153ab89baec68da1e74cdff26a5e476](https://github.com/dOpensource/dsiprouter/commit/1c7454dac153ab89baec68da1e74cdff26a5e476)  \n> Date: Tue, 11 Dec 2018 12:15:32 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1c7454dac153ab89baec68da1e74cdff26a5e476)\n[//]: # (START_SECTION 59181b7efd513109142736c54594559c20b99759)\n### Update use-cases.rst\n\n> Commit: [59181b7efd513109142736c54594559c20b99759](https://github.com/dOpensource/dsiprouter/commit/59181b7efd513109142736c54594559c20b99759)  \n> Date: Tue, 11 Dec 2018 12:13:30 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 59181b7efd513109142736c54594559c20b99759)\n[//]: # (START_SECTION 7132baa5bd7686de6190e63dcd538aa0392db08a)\n### Update use-cases.rst\n\n> Commit: [7132baa5bd7686de6190e63dcd538aa0392db08a](https://github.com/dOpensource/dsiprouter/commit/7132baa5bd7686de6190e63dcd538aa0392db08a)  \n> Date: Tue, 11 Dec 2018 12:11:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7132baa5bd7686de6190e63dcd538aa0392db08a)\n[//]: # (START_SECTION e2070fe65954daff51a8bc6839b499b3eef6bbe4)\n### Update use-cases.rst\n\n> Commit: [e2070fe65954daff51a8bc6839b499b3eef6bbe4](https://github.com/dOpensource/dsiprouter/commit/e2070fe65954daff51a8bc6839b499b3eef6bbe4)  \n> Date: Tue, 11 Dec 2018 12:09:22 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e2070fe65954daff51a8bc6839b499b3eef6bbe4)\n[//]: # (START_SECTION ac1a77631ec67159ddffc6009e23606b1803d9f1)\n### Add files via upload\n\n> Commit: [ac1a77631ec67159ddffc6009e23606b1803d9f1](https://github.com/dOpensource/dsiprouter/commit/ac1a77631ec67159ddffc6009e23606b1803d9f1)  \n> Date: Tue, 11 Dec 2018 12:05:50 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ac1a77631ec67159ddffc6009e23606b1803d9f1)\n[//]: # (START_SECTION 75b3dfb19728dbe76654eb4f633bae7a2dcafe85)\n### Update use-cases.rst\n\n> Commit: [75b3dfb19728dbe76654eb4f633bae7a2dcafe85](https://github.com/dOpensource/dsiprouter/commit/75b3dfb19728dbe76654eb4f633bae7a2dcafe85)  \n> Date: Tue, 11 Dec 2018 09:34:03 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 75b3dfb19728dbe76654eb4f633bae7a2dcafe85)\n[//]: # (START_SECTION 7fa9cee31dd7c3cad756282f7ff64ea8d120c2fa)\n### Update use-cases.rst\n\n> Commit: [7fa9cee31dd7c3cad756282f7ff64ea8d120c2fa](https://github.com/dOpensource/dsiprouter/commit/7fa9cee31dd7c3cad756282f7ff64ea8d120c2fa)  \n> Date: Mon, 10 Dec 2018 15:30:42 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7fa9cee31dd7c3cad756282f7ff64ea8d120c2fa)\n[//]: # (START_SECTION 1e65dda2670080e312ad1553169b1d3ea4976632)\n### Fixed the creation of static routes\n\n> Commit: [1e65dda2670080e312ad1553169b1d3ea4976632](https://github.com/dOpensource/dsiprouter/commit/1e65dda2670080e312ad1553169b1d3ea4976632)  \n> Date: Sun, 9 Dec 2018 13:08:25 +0000  \n> Author: root (root@debian-dsip-test.localdomain)  \n> Committer: root (root@debian-dsip-test.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1e65dda2670080e312ad1553169b1d3ea4976632)\n[//]: # (START_SECTION 9a87f3a42bb3f2456840d4d98a9dd51d2e3418f4)\n### Simplfied the Multidomain support\n\n> Commit: [9a87f3a42bb3f2456840d4d98a9dd51d2e3418f4](https://github.com/dOpensource/dsiprouter/commit/9a87f3a42bb3f2456840d4d98a9dd51d2e3418f4)  \n> Date: Sat, 8 Dec 2018 19:56:09 +0000  \n> Author: root (root@debian-dsip-test.localdomain)  \n> Committer: root (root@debian-dsip-test.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9a87f3a42bb3f2456840d4d98a9dd51d2e3418f4)\n[//]: # (START_SECTION 1e7644e62946bf52d247663ff2514226fde46138)\n### Update installing.rst\n\n> Commit: [1e7644e62946bf52d247663ff2514226fde46138](https://github.com/dOpensource/dsiprouter/commit/1e7644e62946bf52d247663ff2514226fde46138)  \n> Date: Sat, 8 Dec 2018 12:24:48 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1e7644e62946bf52d247663ff2514226fde46138)\n[//]: # (START_SECTION 9a6f944d187ce553b84ee511d1ea936e1770d6e9)\n### Update installing.rst\n\n> Commit: [9a6f944d187ce553b84ee511d1ea936e1770d6e9](https://github.com/dOpensource/dsiprouter/commit/9a6f944d187ce553b84ee511d1ea936e1770d6e9)  \n> Date: Sat, 8 Dec 2018 12:21:50 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9a6f944d187ce553b84ee511d1ea936e1770d6e9)\n[//]: # (START_SECTION aadc0547f85cc28ac730b88b809d8b6b58d99f13)\n### Changes to fix the GUI\n\n> Commit: [aadc0547f85cc28ac730b88b809d8b6b58d99f13](https://github.com/dOpensource/dsiprouter/commit/aadc0547f85cc28ac730b88b809d8b6b58d99f13)  \n> Date: Sat, 8 Dec 2018 16:58:59 +0000  \n> Author: root (root@debian-v51.localdomain)  \n> Committer: root (root@debian-v51.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION aadc0547f85cc28ac730b88b809d8b6b58d99f13)\n[//]: # (START_SECTION 3e99df7b4ccc1fd40a5739257186ea24ac292c4e)\n### Fixed an issue Javascript error that was preventing Fusion Support toggle button from working\n\n> Commit: [3e99df7b4ccc1fd40a5739257186ea24ac292c4e](https://github.com/dOpensource/dsiprouter/commit/3e99df7b4ccc1fd40a5739257186ea24ac292c4e)  \n> Date: Sat, 8 Dec 2018 15:49:54 +0000  \n> Author: root (root@debian-v51.localdomain)  \n> Committer: root (root@debian-v51.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3e99df7b4ccc1fd40a5739257186ea24ac292c4e)\n[//]: # (START_SECTION 916b4adc6c3af117a9a7992b872d6b95047adc63)\n### Fixed an issue with datatables that was causing a JS error\n\n> Commit: [916b4adc6c3af117a9a7992b872d6b95047adc63](https://github.com/dOpensource/dsiprouter/commit/916b4adc6c3af117a9a7992b872d6b95047adc63)  \n> Date: Sat, 8 Dec 2018 14:46:09 +0000  \n> Author: root (root@debian-v51.localdomain)  \n> Committer: root (root@debian-v51.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 916b4adc6c3af117a9a7992b872d6b95047adc63)\n[//]: # (START_SECTION cf311b3f8941a68b155e4190c2f00a5fbd5f53fa)\n### Update command_line_options.rst\n\n> Commit: [cf311b3f8941a68b155e4190c2f00a5fbd5f53fa](https://github.com/dOpensource/dsiprouter/commit/cf311b3f8941a68b155e4190c2f00a5fbd5f53fa)  \n> Date: Fri, 7 Dec 2018 22:54:30 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cf311b3f8941a68b155e4190c2f00a5fbd5f53fa)\n[//]: # (START_SECTION ed16d248270e4aa1a5cab2af65b009c685ee1c65)\n### Update command_line_options.rst\n\n> Commit: [ed16d248270e4aa1a5cab2af65b009c685ee1c65](https://github.com/dOpensource/dsiprouter/commit/ed16d248270e4aa1a5cab2af65b009c685ee1c65)  \n> Date: Fri, 7 Dec 2018 22:44:50 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ed16d248270e4aa1a5cab2af65b009c685ee1c65)\n[//]: # (START_SECTION 050b95e0f8ad833d5233a6691bea89b6fc6168dd)\n### Update command_line_options.rst\n\n> Commit: [050b95e0f8ad833d5233a6691bea89b6fc6168dd](https://github.com/dOpensource/dsiprouter/commit/050b95e0f8ad833d5233a6691bea89b6fc6168dd)  \n> Date: Fri, 7 Dec 2018 22:43:09 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 050b95e0f8ad833d5233a6691bea89b6fc6168dd)\n[//]: # (START_SECTION 638fcacb4433104acaf7d59b0c29a34a9f063681)\n### Update command_line_options.rst\n\n> Commit: [638fcacb4433104acaf7d59b0c29a34a9f063681](https://github.com/dOpensource/dsiprouter/commit/638fcacb4433104acaf7d59b0c29a34a9f063681)  \n> Date: Fri, 7 Dec 2018 22:37:40 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 638fcacb4433104acaf7d59b0c29a34a9f063681)\n[//]: # (START_SECTION 79e0aa07c41d4f4c94d80184af13f9f8fbbd89a2)\n### Update install_option\n\n> Commit: [79e0aa07c41d4f4c94d80184af13f9f8fbbd89a2](https://github.com/dOpensource/dsiprouter/commit/79e0aa07c41d4f4c94d80184af13f9f8fbbd89a2)  \n> Date: Fri, 7 Dec 2018 22:33:38 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 79e0aa07c41d4f4c94d80184af13f9f8fbbd89a2)\n[//]: # (START_SECTION 5fd7d73cb7ab0f751e5ecddce95d0f85bcb30bb0)\n### Update command_line_options.rst\n\n> Commit: [5fd7d73cb7ab0f751e5ecddce95d0f85bcb30bb0](https://github.com/dOpensource/dsiprouter/commit/5fd7d73cb7ab0f751e5ecddce95d0f85bcb30bb0)  \n> Date: Fri, 7 Dec 2018 22:30:15 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5fd7d73cb7ab0f751e5ecddce95d0f85bcb30bb0)\n[//]: # (START_SECTION a95423bc1dfb8763f4d702e9e15995e2634a44dc)\n### Create install_option\n\n> Commit: [a95423bc1dfb8763f4d702e9e15995e2634a44dc](https://github.com/dOpensource/dsiprouter/commit/a95423bc1dfb8763f4d702e9e15995e2634a44dc)  \n> Date: Fri, 7 Dec 2018 22:29:00 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a95423bc1dfb8763f4d702e9e15995e2634a44dc)\n[//]: # (START_SECTION 33477e607dfcbe3d005b919de124af3ae1b7701c)\n### Update command_line_options.rst\n\n> Commit: [33477e607dfcbe3d005b919de124af3ae1b7701c](https://github.com/dOpensource/dsiprouter/commit/33477e607dfcbe3d005b919de124af3ae1b7701c)  \n> Date: Fri, 7 Dec 2018 19:30:37 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 33477e607dfcbe3d005b919de124af3ae1b7701c)\n[//]: # (START_SECTION a3cb688818101f47ef7adc65e0a5aa189c89cf3d)\n### Update command_line_options.rst\n\n> Commit: [a3cb688818101f47ef7adc65e0a5aa189c89cf3d](https://github.com/dOpensource/dsiprouter/commit/a3cb688818101f47ef7adc65e0a5aa189c89cf3d)  \n> Date: Fri, 7 Dec 2018 19:25:26 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a3cb688818101f47ef7adc65e0a5aa189c89cf3d)\n[//]: # (START_SECTION 85494ea42dba92740cad9829b0231e303a5db0d9)\n### Update command_line_options.rst\n\n> Commit: [85494ea42dba92740cad9829b0231e303a5db0d9](https://github.com/dOpensource/dsiprouter/commit/85494ea42dba92740cad9829b0231e303a5db0d9)  \n> Date: Fri, 7 Dec 2018 17:52:42 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 85494ea42dba92740cad9829b0231e303a5db0d9)\n[//]: # (START_SECTION 89da2706398db81653a1a5b6509e6c0ec990cc6d)\n### Update command_line_options.rst\n\n> Commit: [89da2706398db81653a1a5b6509e6c0ec990cc6d](https://github.com/dOpensource/dsiprouter/commit/89da2706398db81653a1a5b6509e6c0ec990cc6d)  \n> Date: Fri, 7 Dec 2018 15:37:01 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 89da2706398db81653a1a5b6509e6c0ec990cc6d)\n[//]: # (START_SECTION c72fda263ead57ae45ccf2eb28ab1ce92962bced)\n### Update command_line_options.rst\n\n> Commit: [c72fda263ead57ae45ccf2eb28ab1ce92962bced](https://github.com/dOpensource/dsiprouter/commit/c72fda263ead57ae45ccf2eb28ab1ce92962bced)  \n> Date: Fri, 7 Dec 2018 15:35:23 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c72fda263ead57ae45ccf2eb28ab1ce92962bced)\n[//]: # (START_SECTION 765375ae09d8e3cc08b8c389ec856f75f1f7ba4d)\n### Update command_line_options.rst\n\n> Commit: [765375ae09d8e3cc08b8c389ec856f75f1f7ba4d](https://github.com/dOpensource/dsiprouter/commit/765375ae09d8e3cc08b8c389ec856f75f1f7ba4d)  \n> Date: Fri, 7 Dec 2018 14:48:18 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 765375ae09d8e3cc08b8c389ec856f75f1f7ba4d)\n[//]: # (START_SECTION 61c5967288d6a33d938d769c02d25f68d9ba4926)\n### Update command_line_options.rst\n\n> Commit: [61c5967288d6a33d938d769c02d25f68d9ba4926](https://github.com/dOpensource/dsiprouter/commit/61c5967288d6a33d938d769c02d25f68d9ba4926)  \n> Date: Fri, 7 Dec 2018 14:40:38 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 61c5967288d6a33d938d769c02d25f68d9ba4926)\n[//]: # (START_SECTION ca7f16cd52c720561b40ccdd6beec26f88cbbe5e)\n### Update command_line_options.rst\n\n> Commit: [ca7f16cd52c720561b40ccdd6beec26f88cbbe5e](https://github.com/dOpensource/dsiprouter/commit/ca7f16cd52c720561b40ccdd6beec26f88cbbe5e)  \n> Date: Fri, 7 Dec 2018 14:37:29 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ca7f16cd52c720561b40ccdd6beec26f88cbbe5e)\n[//]: # (START_SECTION f23b864a3cb3d5526a99a2d75ca3a69ac795bd3d)\n### Update use-cases.rst\n\n> Commit: [f23b864a3cb3d5526a99a2d75ca3a69ac795bd3d](https://github.com/dOpensource/dsiprouter/commit/f23b864a3cb3d5526a99a2d75ca3a69ac795bd3d)  \n> Date: Fri, 7 Dec 2018 10:54:18 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f23b864a3cb3d5526a99a2d75ca3a69ac795bd3d)\n[//]: # (START_SECTION 76f8d5f07325ef4942885103453f986ea3782f14)\n### Update use-cases.rst\n\n> Commit: [76f8d5f07325ef4942885103453f986ea3782f14](https://github.com/dOpensource/dsiprouter/commit/76f8d5f07325ef4942885103453f986ea3782f14)  \n> Date: Fri, 7 Dec 2018 10:53:05 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 76f8d5f07325ef4942885103453f986ea3782f14)\n[//]: # (START_SECTION ce8acb42553ccf7eb76360b5f65ce778bd6d099f)\n### Update use-cases.rst\n\n> Commit: [ce8acb42553ccf7eb76360b5f65ce778bd6d099f](https://github.com/dOpensource/dsiprouter/commit/ce8acb42553ccf7eb76360b5f65ce778bd6d099f)  \n> Date: Fri, 7 Dec 2018 10:51:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ce8acb42553ccf7eb76360b5f65ce778bd6d099f)\n[//]: # (START_SECTION fe9ffc06953ccbe5c65ec134efd7070dbf6310f9)\n### Update use-cases.rst\n\n> Commit: [fe9ffc06953ccbe5c65ec134efd7070dbf6310f9](https://github.com/dOpensource/dsiprouter/commit/fe9ffc06953ccbe5c65ec134efd7070dbf6310f9)  \n> Date: Fri, 7 Dec 2018 10:48:59 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fe9ffc06953ccbe5c65ec134efd7070dbf6310f9)\n[//]: # (START_SECTION 62c0b4fae5e8b9cc366a7de5d3b349d0fc263065)\n### Update use-cases.rst\n\n> Commit: [62c0b4fae5e8b9cc366a7de5d3b349d0fc263065](https://github.com/dOpensource/dsiprouter/commit/62c0b4fae5e8b9cc366a7de5d3b349d0fc263065)  \n> Date: Fri, 7 Dec 2018 10:48:14 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 62c0b4fae5e8b9cc366a7de5d3b349d0fc263065)\n[//]: # (START_SECTION 9c1b112fbb2b12dfca47b63646b77af1eaecea8e)\n### Delete list_of_domains1.PNG\n\n> Commit: [9c1b112fbb2b12dfca47b63646b77af1eaecea8e](https://github.com/dOpensource/dsiprouter/commit/9c1b112fbb2b12dfca47b63646b77af1eaecea8e)  \n> Date: Fri, 7 Dec 2018 10:46:05 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9c1b112fbb2b12dfca47b63646b77af1eaecea8e)\n[//]: # (START_SECTION ac1ebe0c86d4bb1b5d21db0eb29e1313defa8f41)\n### Update use-cases.rst\n\n> Commit: [ac1ebe0c86d4bb1b5d21db0eb29e1313defa8f41](https://github.com/dOpensource/dsiprouter/commit/ac1ebe0c86d4bb1b5d21db0eb29e1313defa8f41)  \n> Date: Fri, 7 Dec 2018 10:43:28 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ac1ebe0c86d4bb1b5d21db0eb29e1313defa8f41)\n[//]: # (START_SECTION 8a7f7ba79856092d3d1202eb41ed585cd294cc52)\n### Update use-cases.rst\n\n> Commit: [8a7f7ba79856092d3d1202eb41ed585cd294cc52](https://github.com/dOpensource/dsiprouter/commit/8a7f7ba79856092d3d1202eb41ed585cd294cc52)  \n> Date: Fri, 7 Dec 2018 10:41:40 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8a7f7ba79856092d3d1202eb41ed585cd294cc52)\n[//]: # (START_SECTION aea234264abda28edee90db41d92dd6df1dfcc6b)\n### Update use-cases.rst\n\n> Commit: [aea234264abda28edee90db41d92dd6df1dfcc6b](https://github.com/dOpensource/dsiprouter/commit/aea234264abda28edee90db41d92dd6df1dfcc6b)  \n> Date: Fri, 7 Dec 2018 10:38:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION aea234264abda28edee90db41d92dd6df1dfcc6b)\n[//]: # (START_SECTION 148e79032178755f8fbd7dd6dd15840ae02e96fa)\n### Update command_line_options.rst\n\n> Commit: [148e79032178755f8fbd7dd6dd15840ae02e96fa](https://github.com/dOpensource/dsiprouter/commit/148e79032178755f8fbd7dd6dd15840ae02e96fa)  \n> Date: Fri, 7 Dec 2018 09:11:28 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 148e79032178755f8fbd7dd6dd15840ae02e96fa)\n[//]: # (START_SECTION 9019969a7522958aab1a4bbc30140a5f1f5df983)\n### Update command_line_options.rst\n\n> Commit: [9019969a7522958aab1a4bbc30140a5f1f5df983](https://github.com/dOpensource/dsiprouter/commit/9019969a7522958aab1a4bbc30140a5f1f5df983)  \n> Date: Fri, 7 Dec 2018 09:09:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9019969a7522958aab1a4bbc30140a5f1f5df983)\n[//]: # (START_SECTION 6ceb6692fc981af823fe43e16e59a6fa1410a398)\n### Update command_line_options.rst\n\n> Commit: [6ceb6692fc981af823fe43e16e59a6fa1410a398](https://github.com/dOpensource/dsiprouter/commit/6ceb6692fc981af823fe43e16e59a6fa1410a398)  \n> Date: Fri, 7 Dec 2018 09:09:05 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6ceb6692fc981af823fe43e16e59a6fa1410a398)\n[//]: # (START_SECTION f15008333f58a883ef8e5caa27ef99bca6de1611)\n### Update command_line_options.rst\n\n> Commit: [f15008333f58a883ef8e5caa27ef99bca6de1611](https://github.com/dOpensource/dsiprouter/commit/f15008333f58a883ef8e5caa27ef99bca6de1611)  \n> Date: Fri, 7 Dec 2018 09:01:39 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f15008333f58a883ef8e5caa27ef99bca6de1611)\n[//]: # (START_SECTION ec07d7c56028a9076dd834a835a76ebcaf570d94)\n### Update command_line_options.rst\n\n> Commit: [ec07d7c56028a9076dd834a835a76ebcaf570d94](https://github.com/dOpensource/dsiprouter/commit/ec07d7c56028a9076dd834a835a76ebcaf570d94)  \n> Date: Fri, 7 Dec 2018 09:00:04 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ec07d7c56028a9076dd834a835a76ebcaf570d94)\n[//]: # (START_SECTION d97d0cbaa0ca851eec37585bea0e5357e4d6544e)\n### Update command_line_options.rst\n\n> Commit: [d97d0cbaa0ca851eec37585bea0e5357e4d6544e](https://github.com/dOpensource/dsiprouter/commit/d97d0cbaa0ca851eec37585bea0e5357e4d6544e)  \n> Date: Fri, 7 Dec 2018 08:59:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d97d0cbaa0ca851eec37585bea0e5357e4d6544e)\n[//]: # (START_SECTION 289886ee032f60c41fee15f3b5bba6e671e327e9)\n### Update use-cases.rst\n\n> Commit: [289886ee032f60c41fee15f3b5bba6e671e327e9](https://github.com/dOpensource/dsiprouter/commit/289886ee032f60c41fee15f3b5bba6e671e327e9)  \n> Date: Thu, 6 Dec 2018 15:17:37 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 289886ee032f60c41fee15f3b5bba6e671e327e9)\n[//]: # (START_SECTION f5cc37a37fc4b71a37dacdb993ce07b0e0c2ef79)\n### Update use-cases.rst\n\n> Commit: [f5cc37a37fc4b71a37dacdb993ce07b0e0c2ef79](https://github.com/dOpensource/dsiprouter/commit/f5cc37a37fc4b71a37dacdb993ce07b0e0c2ef79)  \n> Date: Thu, 6 Dec 2018 15:17:01 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f5cc37a37fc4b71a37dacdb993ce07b0e0c2ef79)\n[//]: # (START_SECTION d6fb1c05de68b63a38282b87d220cb5736d07fcb)\n### Update use-cases.rst\n\n> Commit: [d6fb1c05de68b63a38282b87d220cb5736d07fcb](https://github.com/dOpensource/dsiprouter/commit/d6fb1c05de68b63a38282b87d220cb5736d07fcb)  \n> Date: Thu, 6 Dec 2018 15:14:31 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d6fb1c05de68b63a38282b87d220cb5736d07fcb)\n[//]: # (START_SECTION bfc9a572d9ad25e1755002f8d03b31edf7b5fc9b)\n### Update use-cases.rst\n\n> Commit: [bfc9a572d9ad25e1755002f8d03b31edf7b5fc9b](https://github.com/dOpensource/dsiprouter/commit/bfc9a572d9ad25e1755002f8d03b31edf7b5fc9b)  \n> Date: Thu, 6 Dec 2018 15:12:14 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bfc9a572d9ad25e1755002f8d03b31edf7b5fc9b)\n[//]: # (START_SECTION 6b70420a51f716b3ab5842db978d551dfef88434)\n### Update use-cases.rst\n\n> Commit: [6b70420a51f716b3ab5842db978d551dfef88434](https://github.com/dOpensource/dsiprouter/commit/6b70420a51f716b3ab5842db978d551dfef88434)  \n> Date: Thu, 6 Dec 2018 15:11:24 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6b70420a51f716b3ab5842db978d551dfef88434)\n[//]: # (START_SECTION 393344f5571828dc6e58888245f3718035bdcae3)\n### Update use-cases.rst\n\n> Commit: [393344f5571828dc6e58888245f3718035bdcae3](https://github.com/dOpensource/dsiprouter/commit/393344f5571828dc6e58888245f3718035bdcae3)  \n> Date: Thu, 6 Dec 2018 15:10:11 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 393344f5571828dc6e58888245f3718035bdcae3)\n[//]: # (START_SECTION d5ecbcd2b61f92b5cc10a863f6e95ed8b0b96898)\n### Update use-cases.rst\n\n> Commit: [d5ecbcd2b61f92b5cc10a863f6e95ed8b0b96898](https://github.com/dOpensource/dsiprouter/commit/d5ecbcd2b61f92b5cc10a863f6e95ed8b0b96898)  \n> Date: Thu, 6 Dec 2018 15:08:51 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d5ecbcd2b61f92b5cc10a863f6e95ed8b0b96898)\n[//]: # (START_SECTION ff2a454b25c17ebbd4428ea41992bfdd71096fba)\n### Update use-cases.rst\n\n> Commit: [ff2a454b25c17ebbd4428ea41992bfdd71096fba](https://github.com/dOpensource/dsiprouter/commit/ff2a454b25c17ebbd4428ea41992bfdd71096fba)  \n> Date: Thu, 6 Dec 2018 15:06:36 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ff2a454b25c17ebbd4428ea41992bfdd71096fba)\n[//]: # (START_SECTION 98046aaaea3c7c203fe80e1c8d2f61618ba7b63e)\n### Update command_line_options.rst\n\n> Commit: [98046aaaea3c7c203fe80e1c8d2f61618ba7b63e](https://github.com/dOpensource/dsiprouter/commit/98046aaaea3c7c203fe80e1c8d2f61618ba7b63e)  \n> Date: Thu, 6 Dec 2018 15:05:52 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 98046aaaea3c7c203fe80e1c8d2f61618ba7b63e)\n[//]: # (START_SECTION 667b9288e5ee41e2e7cfcf2d4fb7a428503c549f)\n### Update command_line_options.rst\n\n> Commit: [667b9288e5ee41e2e7cfcf2d4fb7a428503c549f](https://github.com/dOpensource/dsiprouter/commit/667b9288e5ee41e2e7cfcf2d4fb7a428503c549f)  \n> Date: Thu, 6 Dec 2018 15:05:14 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 667b9288e5ee41e2e7cfcf2d4fb7a428503c549f)\n[//]: # (START_SECTION cd06d7a2a94f1abbf466516694c2cfe2149bf56d)\n### Update command_line_options.rst\n\n> Commit: [cd06d7a2a94f1abbf466516694c2cfe2149bf56d](https://github.com/dOpensource/dsiprouter/commit/cd06d7a2a94f1abbf466516694c2cfe2149bf56d)  \n> Date: Thu, 6 Dec 2018 15:04:57 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cd06d7a2a94f1abbf466516694c2cfe2149bf56d)\n[//]: # (START_SECTION b7178b68ba8542d149578874d7fbd5290786ffb4)\n### Update command_line_options.rst\n\n> Commit: [b7178b68ba8542d149578874d7fbd5290786ffb4](https://github.com/dOpensource/dsiprouter/commit/b7178b68ba8542d149578874d7fbd5290786ffb4)  \n> Date: Thu, 6 Dec 2018 15:03:09 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b7178b68ba8542d149578874d7fbd5290786ffb4)\n[//]: # (START_SECTION fd2328046e3788c440fa48192a63dbbbc1c79264)\n### Update use-cases.rst\n\n> Commit: [fd2328046e3788c440fa48192a63dbbbc1c79264](https://github.com/dOpensource/dsiprouter/commit/fd2328046e3788c440fa48192a63dbbbc1c79264)  \n> Date: Thu, 6 Dec 2018 15:01:39 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fd2328046e3788c440fa48192a63dbbbc1c79264)\n[//]: # (START_SECTION dd099c2ff2f187389bd5ed69a5a6e462490c2c86)\n### Update use-cases.rst\n\n> Commit: [dd099c2ff2f187389bd5ed69a5a6e462490c2c86](https://github.com/dOpensource/dsiprouter/commit/dd099c2ff2f187389bd5ed69a5a6e462490c2c86)  \n> Date: Thu, 6 Dec 2018 14:56:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dd099c2ff2f187389bd5ed69a5a6e462490c2c86)\n[//]: # (START_SECTION 28f686349a9d05b79cf399468027ad5936a20bc1)\n### Update use-cases.rst\n\n> Commit: [28f686349a9d05b79cf399468027ad5936a20bc1](https://github.com/dOpensource/dsiprouter/commit/28f686349a9d05b79cf399468027ad5936a20bc1)  \n> Date: Thu, 6 Dec 2018 14:55:27 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 28f686349a9d05b79cf399468027ad5936a20bc1)\n[//]: # (START_SECTION 9e4033dba0d8728cc8de62cd453027b01af2020e)\n### Add files via upload\n\n> Commit: [9e4033dba0d8728cc8de62cd453027b01af2020e](https://github.com/dOpensource/dsiprouter/commit/9e4033dba0d8728cc8de62cd453027b01af2020e)  \n> Date: Thu, 6 Dec 2018 14:54:29 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9e4033dba0d8728cc8de62cd453027b01af2020e)\n[//]: # (START_SECTION 16a8b5a62341f818a591d178760850fc8bd283a0)\n### Delete zoiper_example.PNG\n\n> Commit: [16a8b5a62341f818a591d178760850fc8bd283a0](https://github.com/dOpensource/dsiprouter/commit/16a8b5a62341f818a591d178760850fc8bd283a0)  \n> Date: Thu, 6 Dec 2018 14:54:11 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 16a8b5a62341f818a591d178760850fc8bd283a0)\n[//]: # (START_SECTION 588cb3c20dfa3ec6ed041cfce7aa391e37b75f16)\n### Update use-cases.rst\n\n> Commit: [588cb3c20dfa3ec6ed041cfce7aa391e37b75f16](https://github.com/dOpensource/dsiprouter/commit/588cb3c20dfa3ec6ed041cfce7aa391e37b75f16)  \n> Date: Thu, 6 Dec 2018 14:53:29 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 588cb3c20dfa3ec6ed041cfce7aa391e37b75f16)\n[//]: # (START_SECTION 5d1fbdfde5a0296973a89f90794e14985e89386a)\n### Update use-cases.rst\n\n> Commit: [5d1fbdfde5a0296973a89f90794e14985e89386a](https://github.com/dOpensource/dsiprouter/commit/5d1fbdfde5a0296973a89f90794e14985e89386a)  \n> Date: Thu, 6 Dec 2018 14:49:34 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5d1fbdfde5a0296973a89f90794e14985e89386a)\n[//]: # (START_SECTION 344be156abed478f0065bef75c3be54679183204)\n### Update use-cases.rst\n\n> Commit: [344be156abed478f0065bef75c3be54679183204](https://github.com/dOpensource/dsiprouter/commit/344be156abed478f0065bef75c3be54679183204)  \n> Date: Thu, 6 Dec 2018 14:47:45 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 344be156abed478f0065bef75c3be54679183204)\n[//]: # (START_SECTION 8a878d60cfed669c02afb1eda3d0f6cc8c9d4948)\n### Update use-cases.rst\n\n> Commit: [8a878d60cfed669c02afb1eda3d0f6cc8c9d4948](https://github.com/dOpensource/dsiprouter/commit/8a878d60cfed669c02afb1eda3d0f6cc8c9d4948)  \n> Date: Thu, 6 Dec 2018 14:46:00 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8a878d60cfed669c02afb1eda3d0f6cc8c9d4948)\n[//]: # (START_SECTION 5aab7b87b93d0326fb5e15630ce01aee03c30741)\n### Add files via upload\n\n> Commit: [5aab7b87b93d0326fb5e15630ce01aee03c30741](https://github.com/dOpensource/dsiprouter/commit/5aab7b87b93d0326fb5e15630ce01aee03c30741)  \n> Date: Thu, 6 Dec 2018 14:44:02 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5aab7b87b93d0326fb5e15630ce01aee03c30741)\n[//]: # (START_SECTION 42c6aea2f3ca18499804d48de4bcb70023f37a72)\n### Update use-cases.rst\n\n> Commit: [42c6aea2f3ca18499804d48de4bcb70023f37a72](https://github.com/dOpensource/dsiprouter/commit/42c6aea2f3ca18499804d48de4bcb70023f37a72)  \n> Date: Thu, 6 Dec 2018 14:26:24 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 42c6aea2f3ca18499804d48de4bcb70023f37a72)\n[//]: # (START_SECTION 74223b3c883d1f7aa53469454f0a1c5e3e5681f5)\n### Update use-cases.rst\n\n> Commit: [74223b3c883d1f7aa53469454f0a1c5e3e5681f5](https://github.com/dOpensource/dsiprouter/commit/74223b3c883d1f7aa53469454f0a1c5e3e5681f5)  \n> Date: Thu, 6 Dec 2018 14:12:40 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 74223b3c883d1f7aa53469454f0a1c5e3e5681f5)\n[//]: # (START_SECTION 47f956b156d70d77dd477dc8e92aa99b4858d4dc)\n### Update use-cases.rst\n\n> Commit: [47f956b156d70d77dd477dc8e92aa99b4858d4dc](https://github.com/dOpensource/dsiprouter/commit/47f956b156d70d77dd477dc8e92aa99b4858d4dc)  \n> Date: Thu, 6 Dec 2018 14:06:20 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 47f956b156d70d77dd477dc8e92aa99b4858d4dc)\n[//]: # (START_SECTION bb15224b49be4422bac517b6e18f6aa410ee2571)\n### Add files via upload\n\n> Commit: [bb15224b49be4422bac517b6e18f6aa410ee2571](https://github.com/dOpensource/dsiprouter/commit/bb15224b49be4422bac517b6e18f6aa410ee2571)  \n> Date: Thu, 6 Dec 2018 14:05:36 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bb15224b49be4422bac517b6e18f6aa410ee2571)\n[//]: # (START_SECTION 5d0b75705955ed383f676fe3bb4dc966cecd8583)\n### Update use-cases.rst\n\n> Commit: [5d0b75705955ed383f676fe3bb4dc966cecd8583](https://github.com/dOpensource/dsiprouter/commit/5d0b75705955ed383f676fe3bb4dc966cecd8583)  \n> Date: Thu, 6 Dec 2018 14:05:12 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5d0b75705955ed383f676fe3bb4dc966cecd8583)\n[//]: # (START_SECTION f29f670b6a9d4454d42679475c05d5933084344c)\n### Update command_line_options.rst\n\n> Commit: [f29f670b6a9d4454d42679475c05d5933084344c](https://github.com/dOpensource/dsiprouter/commit/f29f670b6a9d4454d42679475c05d5933084344c)  \n> Date: Thu, 6 Dec 2018 13:12:02 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f29f670b6a9d4454d42679475c05d5933084344c)\n[//]: # (START_SECTION 0aeb7ee3f572865c6ed1d00cb7d94830ee435fef)\n### Update command_line_options.rst\n\n> Commit: [0aeb7ee3f572865c6ed1d00cb7d94830ee435fef](https://github.com/dOpensource/dsiprouter/commit/0aeb7ee3f572865c6ed1d00cb7d94830ee435fef)  \n> Date: Thu, 6 Dec 2018 13:11:35 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0aeb7ee3f572865c6ed1d00cb7d94830ee435fef)\n[//]: # (START_SECTION 2daab6b71f884cccd363b6b05a19293cb3fee2b2)\n### Update command_line_options.rst\n\n> Commit: [2daab6b71f884cccd363b6b05a19293cb3fee2b2](https://github.com/dOpensource/dsiprouter/commit/2daab6b71f884cccd363b6b05a19293cb3fee2b2)  \n> Date: Thu, 6 Dec 2018 13:11:00 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2daab6b71f884cccd363b6b05a19293cb3fee2b2)\n[//]: # (START_SECTION 602ad423fccd8eb6d7f4ebd297fbc68cd2b08d01)\n### Update command_line_options.rst\n\n> Commit: [602ad423fccd8eb6d7f4ebd297fbc68cd2b08d01](https://github.com/dOpensource/dsiprouter/commit/602ad423fccd8eb6d7f4ebd297fbc68cd2b08d01)  \n> Date: Thu, 6 Dec 2018 13:07:22 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 602ad423fccd8eb6d7f4ebd297fbc68cd2b08d01)\n[//]: # (START_SECTION 983eee72f935a7ce70d28a7b6d35e6c263fc2fdf)\n### Update command_line_options.rst\n\n> Commit: [983eee72f935a7ce70d28a7b6d35e6c263fc2fdf](https://github.com/dOpensource/dsiprouter/commit/983eee72f935a7ce70d28a7b6d35e6c263fc2fdf)  \n> Date: Thu, 6 Dec 2018 13:06:59 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 983eee72f935a7ce70d28a7b6d35e6c263fc2fdf)\n[//]: # (START_SECTION 085797c33219df01c930dbe27c29cb11398f03d3)\n### Update command_line_options.rst\n\n> Commit: [085797c33219df01c930dbe27c29cb11398f03d3](https://github.com/dOpensource/dsiprouter/commit/085797c33219df01c930dbe27c29cb11398f03d3)  \n> Date: Thu, 6 Dec 2018 13:06:20 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 085797c33219df01c930dbe27c29cb11398f03d3)\n[//]: # (START_SECTION ef332f0ee789177c64c847302f1725cc42d558ec)\n### Update command_line_options.rst\n\n> Commit: [ef332f0ee789177c64c847302f1725cc42d558ec](https://github.com/dOpensource/dsiprouter/commit/ef332f0ee789177c64c847302f1725cc42d558ec)  \n> Date: Thu, 6 Dec 2018 13:05:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ef332f0ee789177c64c847302f1725cc42d558ec)\n[//]: # (START_SECTION 0695cd31a90572587772fbc3f45dac8d678c8c4e)\n### Update command_line_options.rst\n\n> Commit: [0695cd31a90572587772fbc3f45dac8d678c8c4e](https://github.com/dOpensource/dsiprouter/commit/0695cd31a90572587772fbc3f45dac8d678c8c4e)  \n> Date: Thu, 6 Dec 2018 13:05:17 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0695cd31a90572587772fbc3f45dac8d678c8c4e)\n[//]: # (START_SECTION 33815785d90440d319fb6efb7866118f310fd419)\n### Update command_line_options.rst\n\n> Commit: [33815785d90440d319fb6efb7866118f310fd419](https://github.com/dOpensource/dsiprouter/commit/33815785d90440d319fb6efb7866118f310fd419)  \n> Date: Thu, 6 Dec 2018 13:04:52 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 33815785d90440d319fb6efb7866118f310fd419)\n[//]: # (START_SECTION 21a43c9900c59302cbce48fd2b0d8bf34469cc7c)\n### Update use-cases.rst\n\n> Commit: [21a43c9900c59302cbce48fd2b0d8bf34469cc7c](https://github.com/dOpensource/dsiprouter/commit/21a43c9900c59302cbce48fd2b0d8bf34469cc7c)  \n> Date: Thu, 6 Dec 2018 12:57:50 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 21a43c9900c59302cbce48fd2b0d8bf34469cc7c)\n[//]: # (START_SECTION bc7d7326829a388463a5a7e41c99c7801269d2ad)\n### Update use-cases.rst\n\n> Commit: [bc7d7326829a388463a5a7e41c99c7801269d2ad](https://github.com/dOpensource/dsiprouter/commit/bc7d7326829a388463a5a7e41c99c7801269d2ad)  \n> Date: Thu, 6 Dec 2018 12:55:54 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bc7d7326829a388463a5a7e41c99c7801269d2ad)\n[//]: # (START_SECTION cce8539090b295ca6ae86ca7a0900960354798c9)\n### Add files via upload\n\n> Commit: [cce8539090b295ca6ae86ca7a0900960354798c9](https://github.com/dOpensource/dsiprouter/commit/cce8539090b295ca6ae86ca7a0900960354798c9)  \n> Date: Thu, 6 Dec 2018 12:54:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cce8539090b295ca6ae86ca7a0900960354798c9)\n[//]: # (START_SECTION 9c0fd79d14bdf43cbc69f752624d30e586e9564e)\n### Delete 11d_dialplan2.PNG\n\n> Commit: [9c0fd79d14bdf43cbc69f752624d30e586e9564e](https://github.com/dOpensource/dsiprouter/commit/9c0fd79d14bdf43cbc69f752624d30e586e9564e)  \n> Date: Thu, 6 Dec 2018 12:53:59 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9c0fd79d14bdf43cbc69f752624d30e586e9564e)\n[//]: # (START_SECTION f501053662ee2c79336012b5cf3eeb86539bbadd)\n### Delete dialplan_11.PNG\n\n> Commit: [f501053662ee2c79336012b5cf3eeb86539bbadd](https://github.com/dOpensource/dsiprouter/commit/f501053662ee2c79336012b5cf3eeb86539bbadd)  \n> Date: Thu, 6 Dec 2018 12:53:43 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f501053662ee2c79336012b5cf3eeb86539bbadd)\n[//]: # (START_SECTION b879fa2bdc4a4388efc07e8c9e3f4004abe15e98)\n### Update use-cases.rst\n\n> Commit: [b879fa2bdc4a4388efc07e8c9e3f4004abe15e98](https://github.com/dOpensource/dsiprouter/commit/b879fa2bdc4a4388efc07e8c9e3f4004abe15e98)  \n> Date: Thu, 6 Dec 2018 12:52:51 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b879fa2bdc4a4388efc07e8c9e3f4004abe15e98)\n[//]: # (START_SECTION 79318e3ef70daf15dad5b620857efa857b2066ed)\n### Update use-cases.rst\n\n> Commit: [79318e3ef70daf15dad5b620857efa857b2066ed](https://github.com/dOpensource/dsiprouter/commit/79318e3ef70daf15dad5b620857efa857b2066ed)  \n> Date: Thu, 6 Dec 2018 12:51:16 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 79318e3ef70daf15dad5b620857efa857b2066ed)\n[//]: # (START_SECTION f8e012438b1cb25abbcc2ff90caf8ebc2ce14ada)\n### Update use-cases.rst\n\n> Commit: [f8e012438b1cb25abbcc2ff90caf8ebc2ce14ada](https://github.com/dOpensource/dsiprouter/commit/f8e012438b1cb25abbcc2ff90caf8ebc2ce14ada)  \n> Date: Thu, 6 Dec 2018 12:50:15 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f8e012438b1cb25abbcc2ff90caf8ebc2ce14ada)\n[//]: # (START_SECTION 2ec4f03b7976b65d99845fbc0950379a9f6b5a59)\n### Update use-cases.rst\n\n> Commit: [2ec4f03b7976b65d99845fbc0950379a9f6b5a59](https://github.com/dOpensource/dsiprouter/commit/2ec4f03b7976b65d99845fbc0950379a9f6b5a59)  \n> Date: Thu, 6 Dec 2018 12:49:04 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2ec4f03b7976b65d99845fbc0950379a9f6b5a59)\n[//]: # (START_SECTION 13a73ea3062ca6e8977b6257dc0a0e80e9ff0d2f)\n### Update use-cases.rst\n\n> Commit: [13a73ea3062ca6e8977b6257dc0a0e80e9ff0d2f](https://github.com/dOpensource/dsiprouter/commit/13a73ea3062ca6e8977b6257dc0a0e80e9ff0d2f)  \n> Date: Thu, 6 Dec 2018 12:45:22 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 13a73ea3062ca6e8977b6257dc0a0e80e9ff0d2f)\n[//]: # (START_SECTION dbb03ebac0d3391ecb09345d7cf45ddc509104ff)\n### Add files via upload\n\n> Commit: [dbb03ebac0d3391ecb09345d7cf45ddc509104ff](https://github.com/dOpensource/dsiprouter/commit/dbb03ebac0d3391ecb09345d7cf45ddc509104ff)  \n> Date: Thu, 6 Dec 2018 12:43:08 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dbb03ebac0d3391ecb09345d7cf45ddc509104ff)\n[//]: # (START_SECTION 7535484f98c0098f8358cbdb3d7a82834272f566)\n### Update use-cases.rst\n\n> Commit: [7535484f98c0098f8358cbdb3d7a82834272f566](https://github.com/dOpensource/dsiprouter/commit/7535484f98c0098f8358cbdb3d7a82834272f566)  \n> Date: Thu, 6 Dec 2018 12:42:38 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7535484f98c0098f8358cbdb3d7a82834272f566)\n[//]: # (START_SECTION ae3d0c71561ce754a77e4b958de94cb494c17ba7)\n### Update command_line_options.rst\n\n> Commit: [ae3d0c71561ce754a77e4b958de94cb494c17ba7](https://github.com/dOpensource/dsiprouter/commit/ae3d0c71561ce754a77e4b958de94cb494c17ba7)  \n> Date: Thu, 6 Dec 2018 12:36:03 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ae3d0c71561ce754a77e4b958de94cb494c17ba7)\n[//]: # (START_SECTION 6475fee6ea6534f2e5641a56510bb36256951327)\n### Update command_line_options.rst\n\n> Commit: [6475fee6ea6534f2e5641a56510bb36256951327](https://github.com/dOpensource/dsiprouter/commit/6475fee6ea6534f2e5641a56510bb36256951327)  \n> Date: Thu, 6 Dec 2018 12:34:19 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6475fee6ea6534f2e5641a56510bb36256951327)\n[//]: # (START_SECTION 89cbb67300162eea463f9ac40cf533eed2caa6aa)\n### Update command_line_options.rst\n\n> Commit: [89cbb67300162eea463f9ac40cf533eed2caa6aa](https://github.com/dOpensource/dsiprouter/commit/89cbb67300162eea463f9ac40cf533eed2caa6aa)  \n> Date: Thu, 6 Dec 2018 12:30:35 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 89cbb67300162eea463f9ac40cf533eed2caa6aa)\n[//]: # (START_SECTION ca0d3756116e607ea96d3165151f590dbf605d4a)\n### Update command_line_options.rst\n\n> Commit: [ca0d3756116e607ea96d3165151f590dbf605d4a](https://github.com/dOpensource/dsiprouter/commit/ca0d3756116e607ea96d3165151f590dbf605d4a)  \n> Date: Thu, 6 Dec 2018 12:26:36 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ca0d3756116e607ea96d3165151f590dbf605d4a)\n[//]: # (START_SECTION c81cc5897f336db0c8889c06af72db2b3bca30e7)\n### Update command_line_options.rst\n\n> Commit: [c81cc5897f336db0c8889c06af72db2b3bca30e7](https://github.com/dOpensource/dsiprouter/commit/c81cc5897f336db0c8889c06af72db2b3bca30e7)  \n> Date: Thu, 6 Dec 2018 12:20:32 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c81cc5897f336db0c8889c06af72db2b3bca30e7)\n[//]: # (START_SECTION 1b983f297226e77b2800630ad2335da42ba976f3)\n### Update command_line_options.rst\n\n> Commit: [1b983f297226e77b2800630ad2335da42ba976f3](https://github.com/dOpensource/dsiprouter/commit/1b983f297226e77b2800630ad2335da42ba976f3)  \n> Date: Thu, 6 Dec 2018 12:20:01 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1b983f297226e77b2800630ad2335da42ba976f3)\n[//]: # (START_SECTION 3596c39c740ef83457b75261b774a48894c4420a)\n### Update command_line_options.rst\n\n> Commit: [3596c39c740ef83457b75261b774a48894c4420a](https://github.com/dOpensource/dsiprouter/commit/3596c39c740ef83457b75261b774a48894c4420a)  \n> Date: Thu, 6 Dec 2018 12:19:36 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3596c39c740ef83457b75261b774a48894c4420a)\n[//]: # (START_SECTION 22d83a156b253fc2f203a2e08d0538a7241675cc)\n### Update command_line_options.rst\n\n> Commit: [22d83a156b253fc2f203a2e08d0538a7241675cc](https://github.com/dOpensource/dsiprouter/commit/22d83a156b253fc2f203a2e08d0538a7241675cc)  \n> Date: Thu, 6 Dec 2018 11:56:31 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 22d83a156b253fc2f203a2e08d0538a7241675cc)\n[//]: # (START_SECTION 41d9b947ac9c7c7a2bf53d488b84c31ff436953f)\n### Update command_line_options.rst\n\n> Commit: [41d9b947ac9c7c7a2bf53d488b84c31ff436953f](https://github.com/dOpensource/dsiprouter/commit/41d9b947ac9c7c7a2bf53d488b84c31ff436953f)  \n> Date: Thu, 6 Dec 2018 11:19:07 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 41d9b947ac9c7c7a2bf53d488b84c31ff436953f)\n[//]: # (START_SECTION 79621819717360f647e693c07704ba1d7c30b16c)\n### Update command_line_options.rst\n\n> Commit: [79621819717360f647e693c07704ba1d7c30b16c](https://github.com/dOpensource/dsiprouter/commit/79621819717360f647e693c07704ba1d7c30b16c)  \n> Date: Thu, 6 Dec 2018 11:17:47 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 79621819717360f647e693c07704ba1d7c30b16c)\n[//]: # (START_SECTION e7d0e8562eb97199081bb7b9f84c68e218d0b557)\n### Update command_line_options.rst\n\n> Commit: [e7d0e8562eb97199081bb7b9f84c68e218d0b557](https://github.com/dOpensource/dsiprouter/commit/e7d0e8562eb97199081bb7b9f84c68e218d0b557)  \n> Date: Thu, 6 Dec 2018 11:17:30 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e7d0e8562eb97199081bb7b9f84c68e218d0b557)\n[//]: # (START_SECTION b5ebe5ff4f5d9a1e2f5158ee99e1d2bd08f0582f)\n### Update command_line_options.rst\n\n> Commit: [b5ebe5ff4f5d9a1e2f5158ee99e1d2bd08f0582f](https://github.com/dOpensource/dsiprouter/commit/b5ebe5ff4f5d9a1e2f5158ee99e1d2bd08f0582f)  \n> Date: Thu, 6 Dec 2018 11:16:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b5ebe5ff4f5d9a1e2f5158ee99e1d2bd08f0582f)\n[//]: # (START_SECTION b02dbcb157172a3c6ccd5eadfb2a78477557799a)\n### Update command_line_options.rst\n\n> Commit: [b02dbcb157172a3c6ccd5eadfb2a78477557799a](https://github.com/dOpensource/dsiprouter/commit/b02dbcb157172a3c6ccd5eadfb2a78477557799a)  \n> Date: Thu, 6 Dec 2018 11:15:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b02dbcb157172a3c6ccd5eadfb2a78477557799a)\n[//]: # (START_SECTION 6098b9f4d04820329d615ec01f8332748132a6de)\n### Update command_line_options.rst\n\n> Commit: [6098b9f4d04820329d615ec01f8332748132a6de](https://github.com/dOpensource/dsiprouter/commit/6098b9f4d04820329d615ec01f8332748132a6de)  \n> Date: Thu, 6 Dec 2018 11:13:19 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6098b9f4d04820329d615ec01f8332748132a6de)\n[//]: # (START_SECTION dab249698fd794f5c70475ec326165f2733107a6)\n### Update command_line_options.rst\n\n> Commit: [dab249698fd794f5c70475ec326165f2733107a6](https://github.com/dOpensource/dsiprouter/commit/dab249698fd794f5c70475ec326165f2733107a6)  \n> Date: Thu, 6 Dec 2018 11:11:55 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dab249698fd794f5c70475ec326165f2733107a6)\n[//]: # (START_SECTION 7e0faf08240b1026784dab219fc93fbfc88ddab9)\n### Update command_line_options.rst\n\n> Commit: [7e0faf08240b1026784dab219fc93fbfc88ddab9](https://github.com/dOpensource/dsiprouter/commit/7e0faf08240b1026784dab219fc93fbfc88ddab9)  \n> Date: Thu, 6 Dec 2018 11:11:33 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7e0faf08240b1026784dab219fc93fbfc88ddab9)\n[//]: # (START_SECTION 5caadc2dd4f99cdb4055f94e82247ac7b4644c82)\n### Update command_line_options.rst\n\n> Commit: [5caadc2dd4f99cdb4055f94e82247ac7b4644c82](https://github.com/dOpensource/dsiprouter/commit/5caadc2dd4f99cdb4055f94e82247ac7b4644c82)  \n> Date: Thu, 6 Dec 2018 11:11:10 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5caadc2dd4f99cdb4055f94e82247ac7b4644c82)\n[//]: # (START_SECTION eeae319e6fba19122867592373a6cf625ead92a0)\n### Update command_line_options.rst\n\n> Commit: [eeae319e6fba19122867592373a6cf625ead92a0](https://github.com/dOpensource/dsiprouter/commit/eeae319e6fba19122867592373a6cf625ead92a0)  \n> Date: Thu, 6 Dec 2018 11:09:47 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eeae319e6fba19122867592373a6cf625ead92a0)\n[//]: # (START_SECTION 151d86f75adc877b7d11cc35540cc848fce6d4a2)\n### Update command_line_options.rst\n\n> Commit: [151d86f75adc877b7d11cc35540cc848fce6d4a2](https://github.com/dOpensource/dsiprouter/commit/151d86f75adc877b7d11cc35540cc848fce6d4a2)  \n> Date: Thu, 6 Dec 2018 11:08:27 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 151d86f75adc877b7d11cc35540cc848fce6d4a2)\n[//]: # (START_SECTION 54b9e6b4096e25aa496d37571a8528afc7f6d586)\n### Update command_line_options.rst\n\n> Commit: [54b9e6b4096e25aa496d37571a8528afc7f6d586](https://github.com/dOpensource/dsiprouter/commit/54b9e6b4096e25aa496d37571a8528afc7f6d586)  \n> Date: Thu, 6 Dec 2018 11:07:40 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 54b9e6b4096e25aa496d37571a8528afc7f6d586)\n[//]: # (START_SECTION c3d0d364edf4d383ff904e23008821396e3cbf28)\n### Update command_line_options.rst\n\n> Commit: [c3d0d364edf4d383ff904e23008821396e3cbf28](https://github.com/dOpensource/dsiprouter/commit/c3d0d364edf4d383ff904e23008821396e3cbf28)  \n> Date: Thu, 6 Dec 2018 11:06:32 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c3d0d364edf4d383ff904e23008821396e3cbf28)\n[//]: # (START_SECTION 202ecb65234c571f146226d3c8421f7ec53011b1)\n### Update command_line_options.rst\n\n> Commit: [202ecb65234c571f146226d3c8421f7ec53011b1](https://github.com/dOpensource/dsiprouter/commit/202ecb65234c571f146226d3c8421f7ec53011b1)  \n> Date: Thu, 6 Dec 2018 11:05:29 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 202ecb65234c571f146226d3c8421f7ec53011b1)\n[//]: # (START_SECTION 8f4e543faab2ba15f0609321876286d3d98149b5)\n### Update command_line_options.rst\n\n> Commit: [8f4e543faab2ba15f0609321876286d3d98149b5](https://github.com/dOpensource/dsiprouter/commit/8f4e543faab2ba15f0609321876286d3d98149b5)  \n> Date: Thu, 6 Dec 2018 11:04:18 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8f4e543faab2ba15f0609321876286d3d98149b5)\n[//]: # (START_SECTION b7e8dff20e515cb91bcc486e3573714d5f53e68b)\n### Update command_line_options.rst\n\n> Commit: [b7e8dff20e515cb91bcc486e3573714d5f53e68b](https://github.com/dOpensource/dsiprouter/commit/b7e8dff20e515cb91bcc486e3573714d5f53e68b)  \n> Date: Thu, 6 Dec 2018 11:02:48 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b7e8dff20e515cb91bcc486e3573714d5f53e68b)\n[//]: # (START_SECTION d5258fa6c34c0b4d238f6f9c484495b07e42bc70)\n### Update command_line_options.rst\n\n> Commit: [d5258fa6c34c0b4d238f6f9c484495b07e42bc70](https://github.com/dOpensource/dsiprouter/commit/d5258fa6c34c0b4d238f6f9c484495b07e42bc70)  \n> Date: Thu, 6 Dec 2018 11:00:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d5258fa6c34c0b4d238f6f9c484495b07e42bc70)\n[//]: # (START_SECTION d1f5701490e421feacd1f2f83526c343524a126e)\n### Update command_line_options.rst\n\n> Commit: [d1f5701490e421feacd1f2f83526c343524a126e](https://github.com/dOpensource/dsiprouter/commit/d1f5701490e421feacd1f2f83526c343524a126e)  \n> Date: Thu, 6 Dec 2018 10:56:32 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d1f5701490e421feacd1f2f83526c343524a126e)\n[//]: # (START_SECTION 16ba0130857f157b8ca2d25ca970628b2d6e81b6)\n### Update use-cases.rst\n\n> Commit: [16ba0130857f157b8ca2d25ca970628b2d6e81b6](https://github.com/dOpensource/dsiprouter/commit/16ba0130857f157b8ca2d25ca970628b2d6e81b6)  \n> Date: Thu, 6 Dec 2018 10:29:48 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 16ba0130857f157b8ca2d25ca970628b2d6e81b6)\n[//]: # (START_SECTION b876bb46ad10a9e6b835dbf8017e36716fe566e9)\n### Update use-cases.rst\n\n> Commit: [b876bb46ad10a9e6b835dbf8017e36716fe566e9](https://github.com/dOpensource/dsiprouter/commit/b876bb46ad10a9e6b835dbf8017e36716fe566e9)  \n> Date: Thu, 6 Dec 2018 10:28:57 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b876bb46ad10a9e6b835dbf8017e36716fe566e9)\n[//]: # (START_SECTION b286809dea53c2a2ea82ffec535bd4519f5c9e8a)\n### Add files via upload\n\n> Commit: [b286809dea53c2a2ea82ffec535bd4519f5c9e8a](https://github.com/dOpensource/dsiprouter/commit/b286809dea53c2a2ea82ffec535bd4519f5c9e8a)  \n> Date: Thu, 6 Dec 2018 10:27:01 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b286809dea53c2a2ea82ffec535bd4519f5c9e8a)\n[//]: # (START_SECTION 61f9af2883329b93f18c7878535f9f97a6b9461c)\n### Update use-cases.rst\n\n> Commit: [61f9af2883329b93f18c7878535f9f97a6b9461c](https://github.com/dOpensource/dsiprouter/commit/61f9af2883329b93f18c7878535f9f97a6b9461c)  \n> Date: Thu, 6 Dec 2018 10:22:11 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 61f9af2883329b93f18c7878535f9f97a6b9461c)\n[//]: # (START_SECTION 003a349e1cd36f53ee8c76d0aa7f451b14d09fff)\n### Add files via upload\n\n> Commit: [003a349e1cd36f53ee8c76d0aa7f451b14d09fff](https://github.com/dOpensource/dsiprouter/commit/003a349e1cd36f53ee8c76d0aa7f451b14d09fff)  \n> Date: Thu, 6 Dec 2018 10:21:04 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 003a349e1cd36f53ee8c76d0aa7f451b14d09fff)\n[//]: # (START_SECTION 725ceeb3a0a11c0395f58b8524db7a5dc482f3b1)\n### Delete 11d_dialplan2.PNG\n\n> Commit: [725ceeb3a0a11c0395f58b8524db7a5dc482f3b1](https://github.com/dOpensource/dsiprouter/commit/725ceeb3a0a11c0395f58b8524db7a5dc482f3b1)  \n> Date: Thu, 6 Dec 2018 10:19:55 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 725ceeb3a0a11c0395f58b8524db7a5dc482f3b1)\n[//]: # (START_SECTION b36d14fb3160a67bef18ef1e98f81822d9f97ebe)\n### Update command_line_options.rst\n\n> Commit: [b36d14fb3160a67bef18ef1e98f81822d9f97ebe](https://github.com/dOpensource/dsiprouter/commit/b36d14fb3160a67bef18ef1e98f81822d9f97ebe)  \n> Date: Thu, 6 Dec 2018 09:42:39 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b36d14fb3160a67bef18ef1e98f81822d9f97ebe)\n[//]: # (START_SECTION 04439585a81b86bc2b6efa84ed95c22ed6b4aa2b)\n### Fixed an issue with sync'ing with FusionPBX servers\n\n> Commit: [04439585a81b86bc2b6efa84ed95c22ed6b4aa2b](https://github.com/dOpensource/dsiprouter/commit/04439585a81b86bc2b6efa84ed95c22ed6b4aa2b)  \n> Date: Wed, 5 Dec 2018 21:40:25 +0000  \n> Author: root (root@debian-v51.localdomain)  \n> Committer: root (root@debian-v51.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 04439585a81b86bc2b6efa84ed95c22ed6b4aa2b)\n[//]: # (START_SECTION 817cdea62dfacc1a2b26d76a5585473bdbdf80b2)\n### Update command_line_options.rst\n\n> Commit: [817cdea62dfacc1a2b26d76a5585473bdbdf80b2](https://github.com/dOpensource/dsiprouter/commit/817cdea62dfacc1a2b26d76a5585473bdbdf80b2)  \n> Date: Wed, 5 Dec 2018 16:10:09 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 817cdea62dfacc1a2b26d76a5585473bdbdf80b2)\n[//]: # (START_SECTION a67b89861bdcd4e46669fddaf9c9e7c4447a7c75)\n### Update command_line_options.rst\n\n> Commit: [a67b89861bdcd4e46669fddaf9c9e7c4447a7c75](https://github.com/dOpensource/dsiprouter/commit/a67b89861bdcd4e46669fddaf9c9e7c4447a7c75)  \n> Date: Wed, 5 Dec 2018 16:09:29 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a67b89861bdcd4e46669fddaf9c9e7c4447a7c75)\n[//]: # (START_SECTION 2fe18f670331c8858287495c2440c218a480eaf4)\n### Update command_line_options.rst\n\n> Commit: [2fe18f670331c8858287495c2440c218a480eaf4](https://github.com/dOpensource/dsiprouter/commit/2fe18f670331c8858287495c2440c218a480eaf4)  \n> Date: Wed, 5 Dec 2018 16:08:39 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2fe18f670331c8858287495c2440c218a480eaf4)\n[//]: # (START_SECTION 70fe86b3691b34c50faf1aa9d78f0d7bd29826f4)\n### Update command_line_options.rst\n\n> Commit: [70fe86b3691b34c50faf1aa9d78f0d7bd29826f4](https://github.com/dOpensource/dsiprouter/commit/70fe86b3691b34c50faf1aa9d78f0d7bd29826f4)  \n> Date: Wed, 5 Dec 2018 16:08:00 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 70fe86b3691b34c50faf1aa9d78f0d7bd29826f4)\n[//]: # (START_SECTION 176580d083e975678c1cb0204af07ec1f4d1e377)\n### Update command_line_options.rst\n\n> Commit: [176580d083e975678c1cb0204af07ec1f4d1e377](https://github.com/dOpensource/dsiprouter/commit/176580d083e975678c1cb0204af07ec1f4d1e377)  \n> Date: Wed, 5 Dec 2018 16:07:36 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 176580d083e975678c1cb0204af07ec1f4d1e377)\n[//]: # (START_SECTION 2031fbe4e584fd9ff239912990afb64bd8465206)\n### Update command_line_options.rst\n\n> Commit: [2031fbe4e584fd9ff239912990afb64bd8465206](https://github.com/dOpensource/dsiprouter/commit/2031fbe4e584fd9ff239912990afb64bd8465206)  \n> Date: Wed, 5 Dec 2018 16:06:53 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2031fbe4e584fd9ff239912990afb64bd8465206)\n[//]: # (START_SECTION 60e2fdab00f8f20d4204e4822909fbf15f2b80a9)\n### Update command_line_options.rst\n\n> Commit: [60e2fdab00f8f20d4204e4822909fbf15f2b80a9](https://github.com/dOpensource/dsiprouter/commit/60e2fdab00f8f20d4204e4822909fbf15f2b80a9)  \n> Date: Wed, 5 Dec 2018 16:06:24 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 60e2fdab00f8f20d4204e4822909fbf15f2b80a9)\n[//]: # (START_SECTION ace41e37436366b3b48f28150875d1da8683f971)\n### Update command_line_options.rst\n\n> Commit: [ace41e37436366b3b48f28150875d1da8683f971](https://github.com/dOpensource/dsiprouter/commit/ace41e37436366b3b48f28150875d1da8683f971)  \n> Date: Wed, 5 Dec 2018 16:05:26 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ace41e37436366b3b48f28150875d1da8683f971)\n[//]: # (START_SECTION c190e513d8762ca6cbd34203d4e7f34409718350)\n### Update command_line_options.rst\n\n> Commit: [c190e513d8762ca6cbd34203d4e7f34409718350](https://github.com/dOpensource/dsiprouter/commit/c190e513d8762ca6cbd34203d4e7f34409718350)  \n> Date: Wed, 5 Dec 2018 16:05:06 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c190e513d8762ca6cbd34203d4e7f34409718350)\n[//]: # (START_SECTION dbe86a1d276ffe751749d78b7644bf53d727b235)\n### Update command_line_options.rst\n\n> Commit: [dbe86a1d276ffe751749d78b7644bf53d727b235](https://github.com/dOpensource/dsiprouter/commit/dbe86a1d276ffe751749d78b7644bf53d727b235)  \n> Date: Wed, 5 Dec 2018 16:04:25 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dbe86a1d276ffe751749d78b7644bf53d727b235)\n[//]: # (START_SECTION 92f54bd32c64650744622d29e7639e8495298709)\n### Update command_line_options.rst\n\n> Commit: [92f54bd32c64650744622d29e7639e8495298709](https://github.com/dOpensource/dsiprouter/commit/92f54bd32c64650744622d29e7639e8495298709)  \n> Date: Wed, 5 Dec 2018 16:03:19 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 92f54bd32c64650744622d29e7639e8495298709)\n[//]: # (START_SECTION 974cede9e228fb9f90ea1f02b56762ca3e343a6a)\n### Update command_line_options.rst\n\n> Commit: [974cede9e228fb9f90ea1f02b56762ca3e343a6a](https://github.com/dOpensource/dsiprouter/commit/974cede9e228fb9f90ea1f02b56762ca3e343a6a)  \n> Date: Wed, 5 Dec 2018 16:02:52 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 974cede9e228fb9f90ea1f02b56762ca3e343a6a)\n[//]: # (START_SECTION 1c5f9ef05f2ac1df72a2c2790e0ce4a85196f899)\n### Update command_line_options.rst\n\n> Commit: [1c5f9ef05f2ac1df72a2c2790e0ce4a85196f899](https://github.com/dOpensource/dsiprouter/commit/1c5f9ef05f2ac1df72a2c2790e0ce4a85196f899)  \n> Date: Wed, 5 Dec 2018 16:02:25 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1c5f9ef05f2ac1df72a2c2790e0ce4a85196f899)\n[//]: # (START_SECTION ad56241386f50a1853c92bd6bc9de657d460e228)\n### Update command_line_options.rst\n\n> Commit: [ad56241386f50a1853c92bd6bc9de657d460e228](https://github.com/dOpensource/dsiprouter/commit/ad56241386f50a1853c92bd6bc9de657d460e228)  \n> Date: Wed, 5 Dec 2018 16:01:57 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ad56241386f50a1853c92bd6bc9de657d460e228)\n[//]: # (START_SECTION ea5e2e2a8a5282c92355f12c5c12210eeb7993d6)\n### Update command_line_options.rst\n\n> Commit: [ea5e2e2a8a5282c92355f12c5c12210eeb7993d6](https://github.com/dOpensource/dsiprouter/commit/ea5e2e2a8a5282c92355f12c5c12210eeb7993d6)  \n> Date: Wed, 5 Dec 2018 16:01:44 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ea5e2e2a8a5282c92355f12c5c12210eeb7993d6)\n[//]: # (START_SECTION 14c551b7293d696bfdcafd1c662291f094a1bfaf)\n### Update command_line_options.rst\n\n> Commit: [14c551b7293d696bfdcafd1c662291f094a1bfaf](https://github.com/dOpensource/dsiprouter/commit/14c551b7293d696bfdcafd1c662291f094a1bfaf)  \n> Date: Wed, 5 Dec 2018 16:01:26 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 14c551b7293d696bfdcafd1c662291f094a1bfaf)\n[//]: # (START_SECTION 25fc05cbf29e3dd7978c5c3ff98c2af837fe6393)\n### Update command_line_options.rst\n\n> Commit: [25fc05cbf29e3dd7978c5c3ff98c2af837fe6393](https://github.com/dOpensource/dsiprouter/commit/25fc05cbf29e3dd7978c5c3ff98c2af837fe6393)  \n> Date: Wed, 5 Dec 2018 16:00:48 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 25fc05cbf29e3dd7978c5c3ff98c2af837fe6393)\n[//]: # (START_SECTION 44dd4bdf4e26c1dd93494f1aff23a31f1f7a9e79)\n### Update command_line_options.rst\n\n> Commit: [44dd4bdf4e26c1dd93494f1aff23a31f1f7a9e79](https://github.com/dOpensource/dsiprouter/commit/44dd4bdf4e26c1dd93494f1aff23a31f1f7a9e79)  \n> Date: Wed, 5 Dec 2018 15:59:44 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 44dd4bdf4e26c1dd93494f1aff23a31f1f7a9e79)\n[//]: # (START_SECTION 09821b0afdefd29f0a2d8f1d16d07bfbf4993d65)\n### Update command_line_options.rst\n\n> Commit: [09821b0afdefd29f0a2d8f1d16d07bfbf4993d65](https://github.com/dOpensource/dsiprouter/commit/09821b0afdefd29f0a2d8f1d16d07bfbf4993d65)  \n> Date: Wed, 5 Dec 2018 15:50:47 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 09821b0afdefd29f0a2d8f1d16d07bfbf4993d65)\n[//]: # (START_SECTION b190587fe5a11c9bd30338dca34d8ffc5a3cb9a5)\n### Update command_line_options.rst\n\n> Commit: [b190587fe5a11c9bd30338dca34d8ffc5a3cb9a5](https://github.com/dOpensource/dsiprouter/commit/b190587fe5a11c9bd30338dca34d8ffc5a3cb9a5)  \n> Date: Wed, 5 Dec 2018 15:49:50 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b190587fe5a11c9bd30338dca34d8ffc5a3cb9a5)\n[//]: # (START_SECTION 5b3223187d5d3c66ba4ac176a28d9d445af3a05d)\n### Update command_line_options.rst\n\n> Commit: [5b3223187d5d3c66ba4ac176a28d9d445af3a05d](https://github.com/dOpensource/dsiprouter/commit/5b3223187d5d3c66ba4ac176a28d9d445af3a05d)  \n> Date: Wed, 5 Dec 2018 15:49:15 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5b3223187d5d3c66ba4ac176a28d9d445af3a05d)\n[//]: # (START_SECTION 30711d7e9e349183490fb405f8309d463dfe75c5)\n### Update command_line_options.rst\n\n> Commit: [30711d7e9e349183490fb405f8309d463dfe75c5](https://github.com/dOpensource/dsiprouter/commit/30711d7e9e349183490fb405f8309d463dfe75c5)  \n> Date: Wed, 5 Dec 2018 15:48:13 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 30711d7e9e349183490fb405f8309d463dfe75c5)\n[//]: # (START_SECTION 466d3cbed9a7de0143d2c409e39e477c2c437665)\n### Update command_line_options.rst\n\n> Commit: [466d3cbed9a7de0143d2c409e39e477c2c437665](https://github.com/dOpensource/dsiprouter/commit/466d3cbed9a7de0143d2c409e39e477c2c437665)  \n> Date: Wed, 5 Dec 2018 15:46:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 466d3cbed9a7de0143d2c409e39e477c2c437665)\n[//]: # (START_SECTION 87ffe7fe3b93ee0a27d4d22c89869325573a7386)\n### Update command_line_options.rst\n\n> Commit: [87ffe7fe3b93ee0a27d4d22c89869325573a7386](https://github.com/dOpensource/dsiprouter/commit/87ffe7fe3b93ee0a27d4d22c89869325573a7386)  \n> Date: Wed, 5 Dec 2018 15:45:26 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 87ffe7fe3b93ee0a27d4d22c89869325573a7386)\n[//]: # (START_SECTION a825793487ed70e3969d94660ddf17905edd3d78)\n### Update command_line_options.rst\n\n> Commit: [a825793487ed70e3969d94660ddf17905edd3d78](https://github.com/dOpensource/dsiprouter/commit/a825793487ed70e3969d94660ddf17905edd3d78)  \n> Date: Wed, 5 Dec 2018 15:44:33 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a825793487ed70e3969d94660ddf17905edd3d78)\n[//]: # (START_SECTION 127fb5fea981bda64d6d17c298789f1187fcecfd)\n### Update command_line_options.rst\n\n> Commit: [127fb5fea981bda64d6d17c298789f1187fcecfd](https://github.com/dOpensource/dsiprouter/commit/127fb5fea981bda64d6d17c298789f1187fcecfd)  \n> Date: Wed, 5 Dec 2018 15:40:52 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 127fb5fea981bda64d6d17c298789f1187fcecfd)\n[//]: # (START_SECTION 63814bd3e27245dcfc5d4ba11f765c6ecd727838)\n### Update command_line_options.rst\n\n> Commit: [63814bd3e27245dcfc5d4ba11f765c6ecd727838](https://github.com/dOpensource/dsiprouter/commit/63814bd3e27245dcfc5d4ba11f765c6ecd727838)  \n> Date: Wed, 5 Dec 2018 15:40:16 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 63814bd3e27245dcfc5d4ba11f765c6ecd727838)\n[//]: # (START_SECTION 2461fc8fd838e233301fd6268c73e227ea812483)\n### Update command_line_options.rst\n\n> Commit: [2461fc8fd838e233301fd6268c73e227ea812483](https://github.com/dOpensource/dsiprouter/commit/2461fc8fd838e233301fd6268c73e227ea812483)  \n> Date: Wed, 5 Dec 2018 15:39:59 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2461fc8fd838e233301fd6268c73e227ea812483)\n[//]: # (START_SECTION 39a8cbf850140154611de7842acbe94da5ebf3d5)\n### Update command_line_options.rst\n\n> Commit: [39a8cbf850140154611de7842acbe94da5ebf3d5](https://github.com/dOpensource/dsiprouter/commit/39a8cbf850140154611de7842acbe94da5ebf3d5)  \n> Date: Wed, 5 Dec 2018 15:38:43 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 39a8cbf850140154611de7842acbe94da5ebf3d5)\n[//]: # (START_SECTION 23b0452d3f57e7e1f1a04e9b65ac79ebf45fe94c)\n### Update command_line_options.rst\n\n> Commit: [23b0452d3f57e7e1f1a04e9b65ac79ebf45fe94c](https://github.com/dOpensource/dsiprouter/commit/23b0452d3f57e7e1f1a04e9b65ac79ebf45fe94c)  \n> Date: Wed, 5 Dec 2018 15:37:16 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 23b0452d3f57e7e1f1a04e9b65ac79ebf45fe94c)\n[//]: # (START_SECTION 63a6cedf489fe45966aa661ba42ef14cfb50af7f)\n### Update command_line_options.rst\n\n> Commit: [63a6cedf489fe45966aa661ba42ef14cfb50af7f](https://github.com/dOpensource/dsiprouter/commit/63a6cedf489fe45966aa661ba42ef14cfb50af7f)  \n> Date: Wed, 5 Dec 2018 15:33:47 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 63a6cedf489fe45966aa661ba42ef14cfb50af7f)\n[//]: # (START_SECTION 8861b4b932ea90f9ed891893eecfd4b6e360ad32)\n### Update command_line_options.rst\n\n> Commit: [8861b4b932ea90f9ed891893eecfd4b6e360ad32](https://github.com/dOpensource/dsiprouter/commit/8861b4b932ea90f9ed891893eecfd4b6e360ad32)  \n> Date: Wed, 5 Dec 2018 15:33:31 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8861b4b932ea90f9ed891893eecfd4b6e360ad32)\n[//]: # (START_SECTION e47c3301943a66984671a3fc238fc8d2c48804d7)\n### Update command_line_options.rst\n\n> Commit: [e47c3301943a66984671a3fc238fc8d2c48804d7](https://github.com/dOpensource/dsiprouter/commit/e47c3301943a66984671a3fc238fc8d2c48804d7)  \n> Date: Wed, 5 Dec 2018 15:32:59 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e47c3301943a66984671a3fc238fc8d2c48804d7)\n[//]: # (START_SECTION b5551d43be75252f3f08d9ba1984be8af9b0161b)\n### Update command_line_options.rst\n\n> Commit: [b5551d43be75252f3f08d9ba1984be8af9b0161b](https://github.com/dOpensource/dsiprouter/commit/b5551d43be75252f3f08d9ba1984be8af9b0161b)  \n> Date: Wed, 5 Dec 2018 15:27:54 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b5551d43be75252f3f08d9ba1984be8af9b0161b)\n[//]: # (START_SECTION ca51e56e4fad9d83d97dd625061a726c6737a127)\n### Update command_line_options.rst\n\n> Commit: [ca51e56e4fad9d83d97dd625061a726c6737a127](https://github.com/dOpensource/dsiprouter/commit/ca51e56e4fad9d83d97dd625061a726c6737a127)  \n> Date: Wed, 5 Dec 2018 15:26:39 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ca51e56e4fad9d83d97dd625061a726c6737a127)\n[//]: # (START_SECTION cb77b48b62a1149bbf016cacb19529328700942e)\n### Update command_line_options.rst\n\n> Commit: [cb77b48b62a1149bbf016cacb19529328700942e](https://github.com/dOpensource/dsiprouter/commit/cb77b48b62a1149bbf016cacb19529328700942e)  \n> Date: Wed, 5 Dec 2018 15:25:30 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cb77b48b62a1149bbf016cacb19529328700942e)\n[//]: # (START_SECTION e0cb8ca5c167da59831038ac75b1896648b844ad)\n### Update command_line_options.rst\n\n> Commit: [e0cb8ca5c167da59831038ac75b1896648b844ad](https://github.com/dOpensource/dsiprouter/commit/e0cb8ca5c167da59831038ac75b1896648b844ad)  \n> Date: Wed, 5 Dec 2018 15:24:34 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e0cb8ca5c167da59831038ac75b1896648b844ad)\n[//]: # (START_SECTION 56ed04546303de03c403ca658c16909edea8e265)\n### Update and rename uninstalling.rst to command_line_options.rst\n\n> Commit: [56ed04546303de03c403ca658c16909edea8e265](https://github.com/dOpensource/dsiprouter/commit/56ed04546303de03c403ca658c16909edea8e265)  \n> Date: Wed, 5 Dec 2018 12:39:42 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 56ed04546303de03c403ca658c16909edea8e265)\n[//]: # (START_SECTION 2ebbf629ddc5fbe7141f961c7a5d819a62d5f194)\n### Update configuring.rst\n\n> Commit: [2ebbf629ddc5fbe7141f961c7a5d819a62d5f194](https://github.com/dOpensource/dsiprouter/commit/2ebbf629ddc5fbe7141f961c7a5d819a62d5f194)  \n> Date: Wed, 5 Dec 2018 12:38:33 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2ebbf629ddc5fbe7141f961c7a5d819a62d5f194)\n[//]: # (START_SECTION f5ed526604dda2ea27d924e008fb8822a40b3b40)\n### Update configuring.rst\n\n> Commit: [f5ed526604dda2ea27d924e008fb8822a40b3b40](https://github.com/dOpensource/dsiprouter/commit/f5ed526604dda2ea27d924e008fb8822a40b3b40)  \n> Date: Wed, 5 Dec 2018 12:09:51 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f5ed526604dda2ea27d924e008fb8822a40b3b40)\n[//]: # (START_SECTION 68dbc78ea23bc3c094df7de434fb829cf5f71291)\n### Update uninstalling.rst\n\n> Commit: [68dbc78ea23bc3c094df7de434fb829cf5f71291](https://github.com/dOpensource/dsiprouter/commit/68dbc78ea23bc3c094df7de434fb829cf5f71291)  \n> Date: Wed, 5 Dec 2018 12:05:23 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 68dbc78ea23bc3c094df7de434fb829cf5f71291)\n[//]: # (START_SECTION d7ae04bc83484c23119be8595a4b02dc8fca604d)\n### Update uninstalling.rst\n\n> Commit: [d7ae04bc83484c23119be8595a4b02dc8fca604d](https://github.com/dOpensource/dsiprouter/commit/d7ae04bc83484c23119be8595a4b02dc8fca604d)  \n> Date: Wed, 5 Dec 2018 12:04:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d7ae04bc83484c23119be8595a4b02dc8fca604d)\n[//]: # (START_SECTION 747a95f491ea34c7e070d8f277b1385df9e4e233)\n### Update and rename uninstalling dSIPRouter.rst to uninstalling.rst\n\n> Commit: [747a95f491ea34c7e070d8f277b1385df9e4e233](https://github.com/dOpensource/dsiprouter/commit/747a95f491ea34c7e070d8f277b1385df9e4e233)  \n> Date: Wed, 5 Dec 2018 11:54:10 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 747a95f491ea34c7e070d8f277b1385df9e4e233)\n[//]: # (START_SECTION 1e46a6d9596d8f7b37f5fe4b532c45bd534d9341)\n### Create uninstalling dSIPRouter.rst\n\n> Commit: [1e46a6d9596d8f7b37f5fe4b532c45bd534d9341](https://github.com/dOpensource/dsiprouter/commit/1e46a6d9596d8f7b37f5fe4b532c45bd534d9341)  \n> Date: Wed, 5 Dec 2018 11:52:58 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1e46a6d9596d8f7b37f5fe4b532c45bd534d9341)\n[//]: # (START_SECTION 042000aa2f4103516b675429826aae27ee630cff)\n### Update use-cases.rst\n\n> Commit: [042000aa2f4103516b675429826aae27ee630cff](https://github.com/dOpensource/dsiprouter/commit/042000aa2f4103516b675429826aae27ee630cff)  \n> Date: Wed, 5 Dec 2018 11:50:50 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 042000aa2f4103516b675429826aae27ee630cff)\n[//]: # (START_SECTION 063cf906081d3894fb0dbe20e2233075d290b259)\n### Update use-cases.rst\n\n> Commit: [063cf906081d3894fb0dbe20e2233075d290b259](https://github.com/dOpensource/dsiprouter/commit/063cf906081d3894fb0dbe20e2233075d290b259)  \n> Date: Wed, 5 Dec 2018 11:49:29 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 063cf906081d3894fb0dbe20e2233075d290b259)\n[//]: # (START_SECTION 9e1a6b9565e264379026199507a5f12e9b18d244)\n### Update use-cases.rst\n\n> Commit: [9e1a6b9565e264379026199507a5f12e9b18d244](https://github.com/dOpensource/dsiprouter/commit/9e1a6b9565e264379026199507a5f12e9b18d244)  \n> Date: Wed, 5 Dec 2018 11:48:08 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9e1a6b9565e264379026199507a5f12e9b18d244)\n[//]: # (START_SECTION 8805ecf84743bc0cfeae3becde69e9b930a41bbc)\n### Update use-cases.rst\n\n> Commit: [8805ecf84743bc0cfeae3becde69e9b930a41bbc](https://github.com/dOpensource/dsiprouter/commit/8805ecf84743bc0cfeae3becde69e9b930a41bbc)  \n> Date: Tue, 4 Dec 2018 14:59:28 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8805ecf84743bc0cfeae3becde69e9b930a41bbc)\n[//]: # (START_SECTION 90b4ac84b052fd1eb963ae9ef179de54d17b9339)\n### Update use-cases.rst\n\n> Commit: [90b4ac84b052fd1eb963ae9ef179de54d17b9339](https://github.com/dOpensource/dsiprouter/commit/90b4ac84b052fd1eb963ae9ef179de54d17b9339)  \n> Date: Tue, 4 Dec 2018 14:57:47 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 90b4ac84b052fd1eb963ae9ef179de54d17b9339)\n[//]: # (START_SECTION 20bb8fad54023738be95050b43c584376eb38547)\n### Update use-cases.rst\n\n> Commit: [20bb8fad54023738be95050b43c584376eb38547](https://github.com/dOpensource/dsiprouter/commit/20bb8fad54023738be95050b43c584376eb38547)  \n> Date: Tue, 4 Dec 2018 14:47:07 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 20bb8fad54023738be95050b43c584376eb38547)\n[//]: # (START_SECTION c764685364cda45cd7f76ee61026fa92b3435e32)\n### Delete dialplan_11d.PNG\n\n> Commit: [c764685364cda45cd7f76ee61026fa92b3435e32](https://github.com/dOpensource/dsiprouter/commit/c764685364cda45cd7f76ee61026fa92b3435e32)  \n> Date: Tue, 4 Dec 2018 14:41:56 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c764685364cda45cd7f76ee61026fa92b3435e32)\n[//]: # (START_SECTION 7be3b97d5edc74d655caee605512830105737e9b)\n### Update use-cases.rst\n\n> Commit: [7be3b97d5edc74d655caee605512830105737e9b](https://github.com/dOpensource/dsiprouter/commit/7be3b97d5edc74d655caee605512830105737e9b)  \n> Date: Tue, 4 Dec 2018 14:36:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7be3b97d5edc74d655caee605512830105737e9b)\n[//]: # (START_SECTION 6e258008f3fe925994e072daaa9d75c9dbb7760a)\n### Add files via upload\n\n> Commit: [6e258008f3fe925994e072daaa9d75c9dbb7760a](https://github.com/dOpensource/dsiprouter/commit/6e258008f3fe925994e072daaa9d75c9dbb7760a)  \n> Date: Tue, 4 Dec 2018 14:35:55 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6e258008f3fe925994e072daaa9d75c9dbb7760a)\n[//]: # (START_SECTION c7ff8ae6f595b6655eac2bb180b6eb7eb157445a)\n### Update use-cases.rst\n\n> Commit: [c7ff8ae6f595b6655eac2bb180b6eb7eb157445a](https://github.com/dOpensource/dsiprouter/commit/c7ff8ae6f595b6655eac2bb180b6eb7eb157445a)  \n> Date: Tue, 4 Dec 2018 14:27:20 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c7ff8ae6f595b6655eac2bb180b6eb7eb157445a)\n[//]: # (START_SECTION ab13610276804818c8fc149180fbfe5f4529dbe4)\n### Update use-cases.rst\n\n> Commit: [ab13610276804818c8fc149180fbfe5f4529dbe4](https://github.com/dOpensource/dsiprouter/commit/ab13610276804818c8fc149180fbfe5f4529dbe4)  \n> Date: Tue, 4 Dec 2018 10:26:18 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ab13610276804818c8fc149180fbfe5f4529dbe4)\n[//]: # (START_SECTION dabb36bfa7ea0d68ad7f36e2d15a3f603ad86ac9)\n### Update use-cases.rst\n\n> Commit: [dabb36bfa7ea0d68ad7f36e2d15a3f603ad86ac9](https://github.com/dOpensource/dsiprouter/commit/dabb36bfa7ea0d68ad7f36e2d15a3f603ad86ac9)  \n> Date: Tue, 4 Dec 2018 10:24:48 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dabb36bfa7ea0d68ad7f36e2d15a3f603ad86ac9)\n[//]: # (START_SECTION 66f3b4e7aa86a9192c64fe6d1122ae5a29339b9f)\n### Update use-cases.rst\n\n> Commit: [66f3b4e7aa86a9192c64fe6d1122ae5a29339b9f](https://github.com/dOpensource/dsiprouter/commit/66f3b4e7aa86a9192c64fe6d1122ae5a29339b9f)  \n> Date: Tue, 4 Dec 2018 10:20:27 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 66f3b4e7aa86a9192c64fe6d1122ae5a29339b9f)\n[//]: # (START_SECTION a0d919e4c908cc6ac5720f4b147ed8733ebe9608)\n### Update use-cases.rst\n\n> Commit: [a0d919e4c908cc6ac5720f4b147ed8733ebe9608](https://github.com/dOpensource/dsiprouter/commit/a0d919e4c908cc6ac5720f4b147ed8733ebe9608)  \n> Date: Tue, 4 Dec 2018 10:17:04 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a0d919e4c908cc6ac5720f4b147ed8733ebe9608)\n[//]: # (START_SECTION a7c1280523780c351501ca0e25bd897de61c81a6)\n### Update use-cases.rst\n\n> Commit: [a7c1280523780c351501ca0e25bd897de61c81a6](https://github.com/dOpensource/dsiprouter/commit/a7c1280523780c351501ca0e25bd897de61c81a6)  \n> Date: Tue, 4 Dec 2018 10:04:02 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a7c1280523780c351501ca0e25bd897de61c81a6)\n[//]: # (START_SECTION 16e58ef3a16052bdcc77f8f7379dbfa8ce60571a)\n### Add files via upload\n\n> Commit: [16e58ef3a16052bdcc77f8f7379dbfa8ce60571a](https://github.com/dOpensource/dsiprouter/commit/16e58ef3a16052bdcc77f8f7379dbfa8ce60571a)  \n> Date: Tue, 4 Dec 2018 10:02:48 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 16e58ef3a16052bdcc77f8f7379dbfa8ce60571a)\n[//]: # (START_SECTION 66d404c23738071205eba79bf92da94c49f8618d)\n### Update use-cases.rst\n\n> Commit: [66d404c23738071205eba79bf92da94c49f8618d](https://github.com/dOpensource/dsiprouter/commit/66d404c23738071205eba79bf92da94c49f8618d)  \n> Date: Tue, 4 Dec 2018 09:46:30 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 66d404c23738071205eba79bf92da94c49f8618d)\n[//]: # (START_SECTION 29721571915c3304053c2a6564255d40fcb09e5f)\n### Update use-cases.rst\n\n> Commit: [29721571915c3304053c2a6564255d40fcb09e5f](https://github.com/dOpensource/dsiprouter/commit/29721571915c3304053c2a6564255d40fcb09e5f)  \n> Date: Tue, 4 Dec 2018 09:43:30 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 29721571915c3304053c2a6564255d40fcb09e5f)\n[//]: # (START_SECTION fc6564b7b42dcdc3b00384909c4b21ef8fcb3993)\n### Update use-cases.rst\n\n> Commit: [fc6564b7b42dcdc3b00384909c4b21ef8fcb3993](https://github.com/dOpensource/dsiprouter/commit/fc6564b7b42dcdc3b00384909c4b21ef8fcb3993)  \n> Date: Mon, 3 Dec 2018 14:48:24 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fc6564b7b42dcdc3b00384909c4b21ef8fcb3993)\n[//]: # (START_SECTION a28988b075950add016891cb1c0d76f28dfbeb21)\n### Update use-cases.rst\n\n> Commit: [a28988b075950add016891cb1c0d76f28dfbeb21](https://github.com/dOpensource/dsiprouter/commit/a28988b075950add016891cb1c0d76f28dfbeb21)  \n> Date: Mon, 3 Dec 2018 14:18:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a28988b075950add016891cb1c0d76f28dfbeb21)\n[//]: # (START_SECTION 3d083afdf332473516db6956c822b751574d059b)\n### Update use-cases.rst\n\n> Commit: [3d083afdf332473516db6956c822b751574d059b](https://github.com/dOpensource/dsiprouter/commit/3d083afdf332473516db6956c822b751574d059b)  \n> Date: Mon, 3 Dec 2018 14:16:35 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3d083afdf332473516db6956c822b751574d059b)\n[//]: # (START_SECTION 539c024c671d37fa48ab31cbb977af73913825aa)\n### Update use-cases.rst\n\n> Commit: [539c024c671d37fa48ab31cbb977af73913825aa](https://github.com/dOpensource/dsiprouter/commit/539c024c671d37fa48ab31cbb977af73913825aa)  \n> Date: Mon, 3 Dec 2018 14:15:11 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 539c024c671d37fa48ab31cbb977af73913825aa)\n[//]: # (START_SECTION 8be239fc61a6cba28bea5190d8885e1d1e25b598)\n### Update use-cases.rst\n\n> Commit: [8be239fc61a6cba28bea5190d8885e1d1e25b598](https://github.com/dOpensource/dsiprouter/commit/8be239fc61a6cba28bea5190d8885e1d1e25b598)  \n> Date: Mon, 3 Dec 2018 14:13:23 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8be239fc61a6cba28bea5190d8885e1d1e25b598)\n[//]: # (START_SECTION 69e69bd2d6d42ded7df8e31e78581df9167d35ef)\n### Update use-cases.rst\n\n> Commit: [69e69bd2d6d42ded7df8e31e78581df9167d35ef](https://github.com/dOpensource/dsiprouter/commit/69e69bd2d6d42ded7df8e31e78581df9167d35ef)  \n> Date: Mon, 3 Dec 2018 14:11:26 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 69e69bd2d6d42ded7df8e31e78581df9167d35ef)\n[//]: # (START_SECTION 018693e7ebd8c5cb202964177d5de298de356ec9)\n### Add files via upload\n\n> Commit: [018693e7ebd8c5cb202964177d5de298de356ec9](https://github.com/dOpensource/dsiprouter/commit/018693e7ebd8c5cb202964177d5de298de356ec9)  \n> Date: Mon, 3 Dec 2018 14:10:08 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 018693e7ebd8c5cb202964177d5de298de356ec9)\n[//]: # (START_SECTION 2373ea2a423fbf98ce4dd0c556bf1c7797ad14b2)\n### Delete fusionpbx_hosting2.PNG\n\n> Commit: [2373ea2a423fbf98ce4dd0c556bf1c7797ad14b2](https://github.com/dOpensource/dsiprouter/commit/2373ea2a423fbf98ce4dd0c556bf1c7797ad14b2)  \n> Date: Mon, 3 Dec 2018 14:09:42 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2373ea2a423fbf98ce4dd0c556bf1c7797ad14b2)\n[//]: # (START_SECTION 6e9f2b2e48b22d581ba1f26630c7f442e9ae5f82)\n### Update use-cases.rst\n\n> Commit: [6e9f2b2e48b22d581ba1f26630c7f442e9ae5f82](https://github.com/dOpensource/dsiprouter/commit/6e9f2b2e48b22d581ba1f26630c7f442e9ae5f82)  \n> Date: Mon, 3 Dec 2018 13:59:05 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6e9f2b2e48b22d581ba1f26630c7f442e9ae5f82)\n[//]: # (START_SECTION 2358e58442d261aa832a397f09f62d71ad1c9771)\n### Update use-cases.rst\n\n> Commit: [2358e58442d261aa832a397f09f62d71ad1c9771](https://github.com/dOpensource/dsiprouter/commit/2358e58442d261aa832a397f09f62d71ad1c9771)  \n> Date: Mon, 3 Dec 2018 13:58:30 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2358e58442d261aa832a397f09f62d71ad1c9771)\n[//]: # (START_SECTION 9a156aa2b8e8249e65999f5b08f81d6fe4ca53e2)\n### Update use-cases.rst\n\n> Commit: [9a156aa2b8e8249e65999f5b08f81d6fe4ca53e2](https://github.com/dOpensource/dsiprouter/commit/9a156aa2b8e8249e65999f5b08f81d6fe4ca53e2)  \n> Date: Mon, 3 Dec 2018 13:56:25 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9a156aa2b8e8249e65999f5b08f81d6fe4ca53e2)\n[//]: # (START_SECTION 6290ec80b465320bbd966c90f4bbc38a55a7bc76)\n### Update use-cases.rst\n\n> Commit: [6290ec80b465320bbd966c90f4bbc38a55a7bc76](https://github.com/dOpensource/dsiprouter/commit/6290ec80b465320bbd966c90f4bbc38a55a7bc76)  \n> Date: Mon, 3 Dec 2018 13:55:14 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6290ec80b465320bbd966c90f4bbc38a55a7bc76)\n[//]: # (START_SECTION dab065f980de93e5d53e031a1cb95a980b90b814)\n### Update use-cases.rst\n\n> Commit: [dab065f980de93e5d53e031a1cb95a980b90b814](https://github.com/dOpensource/dsiprouter/commit/dab065f980de93e5d53e031a1cb95a980b90b814)  \n> Date: Mon, 3 Dec 2018 13:53:57 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dab065f980de93e5d53e031a1cb95a980b90b814)\n[//]: # (START_SECTION 9bc941fa9f3270a5c8db6bad9574f30482f2fc10)\n### Update use-cases.rst\n\n> Commit: [9bc941fa9f3270a5c8db6bad9574f30482f2fc10](https://github.com/dOpensource/dsiprouter/commit/9bc941fa9f3270a5c8db6bad9574f30482f2fc10)  \n> Date: Mon, 3 Dec 2018 13:50:47 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9bc941fa9f3270a5c8db6bad9574f30482f2fc10)\n[//]: # (START_SECTION aa092df23f44030cc5996d6ccd8df0cffe4e8cfe)\n### Update use-cases.rst\n\n> Commit: [aa092df23f44030cc5996d6ccd8df0cffe4e8cfe](https://github.com/dOpensource/dsiprouter/commit/aa092df23f44030cc5996d6ccd8df0cffe4e8cfe)  \n> Date: Mon, 3 Dec 2018 13:49:48 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION aa092df23f44030cc5996d6ccd8df0cffe4e8cfe)\n[//]: # (START_SECTION 3160ab71dadf831e17091cc7215dd9f90d7d96af)\n### Update use-cases.rst\n\n> Commit: [3160ab71dadf831e17091cc7215dd9f90d7d96af](https://github.com/dOpensource/dsiprouter/commit/3160ab71dadf831e17091cc7215dd9f90d7d96af)  \n> Date: Mon, 3 Dec 2018 13:47:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3160ab71dadf831e17091cc7215dd9f90d7d96af)\n[//]: # (START_SECTION 92811fc87431326cb07f661db5d9ef72d9ce4e9a)\n### Update use-cases.rst\n\n> Commit: [92811fc87431326cb07f661db5d9ef72d9ce4e9a](https://github.com/dOpensource/dsiprouter/commit/92811fc87431326cb07f661db5d9ef72d9ce4e9a)  \n> Date: Mon, 3 Dec 2018 13:45:23 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 92811fc87431326cb07f661db5d9ef72d9ce4e9a)\n[//]: # (START_SECTION 3320d2b23e9e9d8b31d199dbb83c87351a1325a0)\n### Update use-cases.rst\n\n> Commit: [3320d2b23e9e9d8b31d199dbb83c87351a1325a0](https://github.com/dOpensource/dsiprouter/commit/3320d2b23e9e9d8b31d199dbb83c87351a1325a0)  \n> Date: Mon, 3 Dec 2018 13:44:16 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3320d2b23e9e9d8b31d199dbb83c87351a1325a0)\n[//]: # (START_SECTION a23762fe4157e4cae2715b063624ebb9be06a450)\n### Update use-cases.rst\n\n> Commit: [a23762fe4157e4cae2715b063624ebb9be06a450](https://github.com/dOpensource/dsiprouter/commit/a23762fe4157e4cae2715b063624ebb9be06a450)  \n> Date: Mon, 3 Dec 2018 13:43:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a23762fe4157e4cae2715b063624ebb9be06a450)\n[//]: # (START_SECTION a0a5a63f38340dce64acae8f23ef26ed1e247064)\n### Update use-cases.rst\n\n> Commit: [a0a5a63f38340dce64acae8f23ef26ed1e247064](https://github.com/dOpensource/dsiprouter/commit/a0a5a63f38340dce64acae8f23ef26ed1e247064)  \n> Date: Mon, 3 Dec 2018 13:42:10 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a0a5a63f38340dce64acae8f23ef26ed1e247064)\n[//]: # (START_SECTION 4e035bebb3622c07f57e392da8c6f2afaebc3261)\n### Update use-cases.rst\n\n> Commit: [4e035bebb3622c07f57e392da8c6f2afaebc3261](https://github.com/dOpensource/dsiprouter/commit/4e035bebb3622c07f57e392da8c6f2afaebc3261)  \n> Date: Mon, 3 Dec 2018 13:39:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4e035bebb3622c07f57e392da8c6f2afaebc3261)\n[//]: # (START_SECTION 22b1a37803ecbb339f016f881f26bbbe5f2f8c46)\n### Update use-cases.rst\n\n> Commit: [22b1a37803ecbb339f016f881f26bbbe5f2f8c46](https://github.com/dOpensource/dsiprouter/commit/22b1a37803ecbb339f016f881f26bbbe5f2f8c46)  \n> Date: Mon, 3 Dec 2018 13:34:55 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 22b1a37803ecbb339f016f881f26bbbe5f2f8c46)\n[//]: # (START_SECTION c312d8fb2f94a0f765ef40a7085da271ad8f3601)\n### Update use-cases.rst\n\n> Commit: [c312d8fb2f94a0f765ef40a7085da271ad8f3601](https://github.com/dOpensource/dsiprouter/commit/c312d8fb2f94a0f765ef40a7085da271ad8f3601)  \n> Date: Mon, 3 Dec 2018 13:33:36 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c312d8fb2f94a0f765ef40a7085da271ad8f3601)\n[//]: # (START_SECTION e83477e70105080f6daa89859888b9c788b64831)\n### Update use-cases.rst\n\n> Commit: [e83477e70105080f6daa89859888b9c788b64831](https://github.com/dOpensource/dsiprouter/commit/e83477e70105080f6daa89859888b9c788b64831)  \n> Date: Mon, 3 Dec 2018 13:30:35 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e83477e70105080f6daa89859888b9c788b64831)\n[//]: # (START_SECTION c40947ea2137dd384adb22367ddf1831d293df15)\n### Update use-cases.rst\n\n> Commit: [c40947ea2137dd384adb22367ddf1831d293df15](https://github.com/dOpensource/dsiprouter/commit/c40947ea2137dd384adb22367ddf1831d293df15)  \n> Date: Mon, 3 Dec 2018 13:28:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c40947ea2137dd384adb22367ddf1831d293df15)\n[//]: # (START_SECTION 5625d5f865c3760152f1ec90a35d57c22fdf0c0a)\n### Update use-cases.rst\n\n> Commit: [5625d5f865c3760152f1ec90a35d57c22fdf0c0a](https://github.com/dOpensource/dsiprouter/commit/5625d5f865c3760152f1ec90a35d57c22fdf0c0a)  \n> Date: Mon, 3 Dec 2018 13:26:34 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5625d5f865c3760152f1ec90a35d57c22fdf0c0a)\n[//]: # (START_SECTION 3e39fd2ac22d59097105e019c60ef1998508dd7d)\n### Add files via upload\n\n> Commit: [3e39fd2ac22d59097105e019c60ef1998508dd7d](https://github.com/dOpensource/dsiprouter/commit/3e39fd2ac22d59097105e019c60ef1998508dd7d)  \n> Date: Mon, 3 Dec 2018 13:22:54 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3e39fd2ac22d59097105e019c60ef1998508dd7d)\n[//]: # (START_SECTION dd0049686de8b640ca1123d4dc8f4194021ee887)\n### Update use-cases.rst\n\n> Commit: [dd0049686de8b640ca1123d4dc8f4194021ee887](https://github.com/dOpensource/dsiprouter/commit/dd0049686de8b640ca1123d4dc8f4194021ee887)  \n> Date: Mon, 3 Dec 2018 13:22:09 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dd0049686de8b640ca1123d4dc8f4194021ee887)\n[//]: # (START_SECTION af1c60e897334324e3d6dd1a7787d50d2e1ee973)\n### Update use-cases.rst\n\n> Commit: [af1c60e897334324e3d6dd1a7787d50d2e1ee973](https://github.com/dOpensource/dsiprouter/commit/af1c60e897334324e3d6dd1a7787d50d2e1ee973)  \n> Date: Mon, 3 Dec 2018 13:20:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION af1c60e897334324e3d6dd1a7787d50d2e1ee973)\n[//]: # (START_SECTION a3f8c6157c4263813331935a2635df856de10c27)\n### Add files via upload\n\n> Commit: [a3f8c6157c4263813331935a2635df856de10c27](https://github.com/dOpensource/dsiprouter/commit/a3f8c6157c4263813331935a2635df856de10c27)  \n> Date: Mon, 3 Dec 2018 13:19:34 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a3f8c6157c4263813331935a2635df856de10c27)\n[//]: # (START_SECTION 7b2da29bd66fec284a8ae0ed6e7c1fb58d8a75e5)\n### Delete 11d_dialplan_2.PNG\n\n> Commit: [7b2da29bd66fec284a8ae0ed6e7c1fb58d8a75e5](https://github.com/dOpensource/dsiprouter/commit/7b2da29bd66fec284a8ae0ed6e7c1fb58d8a75e5)  \n> Date: Mon, 3 Dec 2018 13:19:11 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7b2da29bd66fec284a8ae0ed6e7c1fb58d8a75e5)\n[//]: # (START_SECTION 126d16e9a6419ebc946d76283549d48edf8f577a)\n### Update use-cases.rst\n\n> Commit: [126d16e9a6419ebc946d76283549d48edf8f577a](https://github.com/dOpensource/dsiprouter/commit/126d16e9a6419ebc946d76283549d48edf8f577a)  \n> Date: Mon, 3 Dec 2018 13:18:45 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 126d16e9a6419ebc946d76283549d48edf8f577a)\n[//]: # (START_SECTION 6c2a6d5e2a602bca0e066a4338820cf2520300e5)\n### Update use-cases.rst\n\n> Commit: [6c2a6d5e2a602bca0e066a4338820cf2520300e5](https://github.com/dOpensource/dsiprouter/commit/6c2a6d5e2a602bca0e066a4338820cf2520300e5)  \n> Date: Mon, 3 Dec 2018 12:29:28 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6c2a6d5e2a602bca0e066a4338820cf2520300e5)\n[//]: # (START_SECTION b2500941ec0845017e78a79b376305197de374c1)\n### Update use-cases.rst\n\n> Commit: [b2500941ec0845017e78a79b376305197de374c1](https://github.com/dOpensource/dsiprouter/commit/b2500941ec0845017e78a79b376305197de374c1)  \n> Date: Mon, 3 Dec 2018 12:28:16 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b2500941ec0845017e78a79b376305197de374c1)\n[//]: # (START_SECTION 53d909fc90bed86c508e9602dd3dff0998a87fb4)\n### Update use-cases.rst\n\n> Commit: [53d909fc90bed86c508e9602dd3dff0998a87fb4](https://github.com/dOpensource/dsiprouter/commit/53d909fc90bed86c508e9602dd3dff0998a87fb4)  \n> Date: Mon, 3 Dec 2018 12:03:45 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 53d909fc90bed86c508e9602dd3dff0998a87fb4)\n[//]: # (START_SECTION 95673cb589c08f7b9b91f0565161fee9b89d252b)\n### Update use-cases.rst\n\n> Commit: [95673cb589c08f7b9b91f0565161fee9b89d252b](https://github.com/dOpensource/dsiprouter/commit/95673cb589c08f7b9b91f0565161fee9b89d252b)  \n> Date: Mon, 3 Dec 2018 11:54:16 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 95673cb589c08f7b9b91f0565161fee9b89d252b)\n[//]: # (START_SECTION 283bec8c2dd843bd9f89e4fea6ee6e001877c803)\n### Update use-cases.rst\n\n> Commit: [283bec8c2dd843bd9f89e4fea6ee6e001877c803](https://github.com/dOpensource/dsiprouter/commit/283bec8c2dd843bd9f89e4fea6ee6e001877c803)  \n> Date: Mon, 3 Dec 2018 11:52:27 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 283bec8c2dd843bd9f89e4fea6ee6e001877c803)\n[//]: # (START_SECTION 8ba974019ac339517ab15b654cccd3c9882d3d34)\n### Update use-cases.rst\n\n> Commit: [8ba974019ac339517ab15b654cccd3c9882d3d34](https://github.com/dOpensource/dsiprouter/commit/8ba974019ac339517ab15b654cccd3c9882d3d34)  \n> Date: Mon, 3 Dec 2018 11:48:50 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8ba974019ac339517ab15b654cccd3c9882d3d34)\n[//]: # (START_SECTION 77635a5a66d8aaf180316c16152ba6aa245f8996)\n### Add files via upload\n\n> Commit: [77635a5a66d8aaf180316c16152ba6aa245f8996](https://github.com/dOpensource/dsiprouter/commit/77635a5a66d8aaf180316c16152ba6aa245f8996)  \n> Date: Mon, 3 Dec 2018 11:38:34 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 77635a5a66d8aaf180316c16152ba6aa245f8996)\n[//]: # (START_SECTION 6dd381f8cd69e953a5598a0c26a1088649507964)\n### Update use-cases.rst\n\n> Commit: [6dd381f8cd69e953a5598a0c26a1088649507964](https://github.com/dOpensource/dsiprouter/commit/6dd381f8cd69e953a5598a0c26a1088649507964)  \n> Date: Mon, 3 Dec 2018 11:23:17 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6dd381f8cd69e953a5598a0c26a1088649507964)\n[//]: # (START_SECTION 80760c1ddc981f6e94f4e3dba1c98ac109806232)\n### Update use-cases.rst\n\n> Commit: [80760c1ddc981f6e94f4e3dba1c98ac109806232](https://github.com/dOpensource/dsiprouter/commit/80760c1ddc981f6e94f4e3dba1c98ac109806232)  \n> Date: Fri, 30 Nov 2018 11:34:34 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 80760c1ddc981f6e94f4e3dba1c98ac109806232)\n[//]: # (START_SECTION e045ec2409c0502e9b049e5566d5dbaf7d6d8788)\n### Update use-cases.rst\n\n> Commit: [e045ec2409c0502e9b049e5566d5dbaf7d6d8788](https://github.com/dOpensource/dsiprouter/commit/e045ec2409c0502e9b049e5566d5dbaf7d6d8788)  \n> Date: Fri, 30 Nov 2018 11:30:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e045ec2409c0502e9b049e5566d5dbaf7d6d8788)\n[//]: # (START_SECTION 69bd7624b90d527ed23406b956ec43dc451ccadf)\n### Update use-cases.rst\n\n> Commit: [69bd7624b90d527ed23406b956ec43dc451ccadf](https://github.com/dOpensource/dsiprouter/commit/69bd7624b90d527ed23406b956ec43dc451ccadf)  \n> Date: Fri, 30 Nov 2018 11:28:13 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 69bd7624b90d527ed23406b956ec43dc451ccadf)\n[//]: # (START_SECTION c371d7af384e8716ef3e0be02e2f8da6551e84dd)\n### Update use-cases.rst\n\n> Commit: [c371d7af384e8716ef3e0be02e2f8da6551e84dd](https://github.com/dOpensource/dsiprouter/commit/c371d7af384e8716ef3e0be02e2f8da6551e84dd)  \n> Date: Fri, 30 Nov 2018 11:07:32 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c371d7af384e8716ef3e0be02e2f8da6551e84dd)\n[//]: # (START_SECTION 041abb29901cd235202e0f22755b7db6b2596932)\n### Add files via upload\n\n> Commit: [041abb29901cd235202e0f22755b7db6b2596932](https://github.com/dOpensource/dsiprouter/commit/041abb29901cd235202e0f22755b7db6b2596932)  \n> Date: Fri, 30 Nov 2018 11:03:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 041abb29901cd235202e0f22755b7db6b2596932)\n[//]: # (START_SECTION 0c2b2794dc45b2566c1baa8b061bd60be3530cbe)\n### Update use-cases.rst\n\n> Commit: [0c2b2794dc45b2566c1baa8b061bd60be3530cbe](https://github.com/dOpensource/dsiprouter/commit/0c2b2794dc45b2566c1baa8b061bd60be3530cbe)  \n> Date: Fri, 30 Nov 2018 11:01:05 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0c2b2794dc45b2566c1baa8b061bd60be3530cbe)\n[//]: # (START_SECTION aaedb6a45fa3f154aefdd12b68ca3b10daa45d26)\n### Update use-cases.rst\n\n> Commit: [aaedb6a45fa3f154aefdd12b68ca3b10daa45d26](https://github.com/dOpensource/dsiprouter/commit/aaedb6a45fa3f154aefdd12b68ca3b10daa45d26)  \n> Date: Thu, 29 Nov 2018 14:20:03 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION aaedb6a45fa3f154aefdd12b68ca3b10daa45d26)\n[//]: # (START_SECTION 3dfd12b73b4c30337f0f007ba299b6c550667f52)\n### Add files via upload\n\n> Commit: [3dfd12b73b4c30337f0f007ba299b6c550667f52](https://github.com/dOpensource/dsiprouter/commit/3dfd12b73b4c30337f0f007ba299b6c550667f52)  \n> Date: Thu, 29 Nov 2018 14:17:35 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3dfd12b73b4c30337f0f007ba299b6c550667f52)\n[//]: # (START_SECTION 4a02d86bc5c0cb808bf8b8962fea0425521d3977)\n### Update use-cases.rst\n\n> Commit: [4a02d86bc5c0cb808bf8b8962fea0425521d3977](https://github.com/dOpensource/dsiprouter/commit/4a02d86bc5c0cb808bf8b8962fea0425521d3977)  \n> Date: Thu, 29 Nov 2018 14:17:06 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4a02d86bc5c0cb808bf8b8962fea0425521d3977)\n[//]: # (START_SECTION eb6c2b821e0f03e4976a073124f91c3025ca2d06)\n### Update use-cases.rst\n\n> Commit: [eb6c2b821e0f03e4976a073124f91c3025ca2d06](https://github.com/dOpensource/dsiprouter/commit/eb6c2b821e0f03e4976a073124f91c3025ca2d06)  \n> Date: Thu, 29 Nov 2018 13:42:47 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eb6c2b821e0f03e4976a073124f91c3025ca2d06)\n[//]: # (START_SECTION 7859611f7fe33530e181b48b558af5827a4022dd)\n### Update use-cases.rst\n\n> Commit: [7859611f7fe33530e181b48b558af5827a4022dd](https://github.com/dOpensource/dsiprouter/commit/7859611f7fe33530e181b48b558af5827a4022dd)  \n> Date: Thu, 29 Nov 2018 13:41:07 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7859611f7fe33530e181b48b558af5827a4022dd)\n[//]: # (START_SECTION e6ab0944d595e85ca3bdb683b42def15d7fa4ddc)\n### Update use-cases.rst\n\n> Commit: [e6ab0944d595e85ca3bdb683b42def15d7fa4ddc](https://github.com/dOpensource/dsiprouter/commit/e6ab0944d595e85ca3bdb683b42def15d7fa4ddc)  \n> Date: Thu, 29 Nov 2018 13:37:28 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e6ab0944d595e85ca3bdb683b42def15d7fa4ddc)\n[//]: # (START_SECTION c1955b27adfa21f3ae38b54178c69cb604e73ced)\n### Update use-cases.rst\n\n> Commit: [c1955b27adfa21f3ae38b54178c69cb604e73ced](https://github.com/dOpensource/dsiprouter/commit/c1955b27adfa21f3ae38b54178c69cb604e73ced)  \n> Date: Thu, 29 Nov 2018 13:36:32 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c1955b27adfa21f3ae38b54178c69cb604e73ced)\n[//]: # (START_SECTION 53ba73e1220feab3a4e6b54265338051fa7c9f91)\n### Update use-cases.rst\n\n> Commit: [53ba73e1220feab3a4e6b54265338051fa7c9f91](https://github.com/dOpensource/dsiprouter/commit/53ba73e1220feab3a4e6b54265338051fa7c9f91)  \n> Date: Thu, 29 Nov 2018 13:33:29 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 53ba73e1220feab3a4e6b54265338051fa7c9f91)\n[//]: # (START_SECTION 60594ed03141479578c6a07d11b5bf76753dece0)\n### Add files via upload\n\n> Commit: [60594ed03141479578c6a07d11b5bf76753dece0](https://github.com/dOpensource/dsiprouter/commit/60594ed03141479578c6a07d11b5bf76753dece0)  \n> Date: Thu, 29 Nov 2018 13:31:54 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 60594ed03141479578c6a07d11b5bf76753dece0)\n[//]: # (START_SECTION 038b5e5b1e4e9bb1e6f7d8d7969ce07279d474ec)\n### Update use-cases.rst\n\n> Commit: [038b5e5b1e4e9bb1e6f7d8d7969ce07279d474ec](https://github.com/dOpensource/dsiprouter/commit/038b5e5b1e4e9bb1e6f7d8d7969ce07279d474ec)  \n> Date: Thu, 29 Nov 2018 13:31:19 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 038b5e5b1e4e9bb1e6f7d8d7969ce07279d474ec)\n[//]: # (START_SECTION eafa71ff350fe9a4cc5b2ef66fa38117bfe094f7)\n### Update use-cases.rst\n\n> Commit: [eafa71ff350fe9a4cc5b2ef66fa38117bfe094f7](https://github.com/dOpensource/dsiprouter/commit/eafa71ff350fe9a4cc5b2ef66fa38117bfe094f7)  \n> Date: Thu, 29 Nov 2018 13:26:54 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eafa71ff350fe9a4cc5b2ef66fa38117bfe094f7)\n[//]: # (START_SECTION a370bcdbeace0ae48d100a2fcd2801bd4085126c)\n### Update use-cases.rst\n\n> Commit: [a370bcdbeace0ae48d100a2fcd2801bd4085126c](https://github.com/dOpensource/dsiprouter/commit/a370bcdbeace0ae48d100a2fcd2801bd4085126c)  \n> Date: Thu, 29 Nov 2018 13:23:53 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a370bcdbeace0ae48d100a2fcd2801bd4085126c)\n[//]: # (START_SECTION 0739c358539a0629bfcb26a661610748909efea1)\n### Update use-cases.rst\n\n> Commit: [0739c358539a0629bfcb26a661610748909efea1](https://github.com/dOpensource/dsiprouter/commit/0739c358539a0629bfcb26a661610748909efea1)  \n> Date: Thu, 29 Nov 2018 13:02:01 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0739c358539a0629bfcb26a661610748909efea1)\n[//]: # (START_SECTION 8c110668e59f3566b16296b91ea496f20aeecb18)\n### Update use-cases.rst\n\n> Commit: [8c110668e59f3566b16296b91ea496f20aeecb18](https://github.com/dOpensource/dsiprouter/commit/8c110668e59f3566b16296b91ea496f20aeecb18)  \n> Date: Thu, 29 Nov 2018 12:57:30 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8c110668e59f3566b16296b91ea496f20aeecb18)\n[//]: # (START_SECTION aac8b80f73e9b221e7dc4370aefe2e43a16df286)\n### Update use-cases.rst\n\n> Commit: [aac8b80f73e9b221e7dc4370aefe2e43a16df286](https://github.com/dOpensource/dsiprouter/commit/aac8b80f73e9b221e7dc4370aefe2e43a16df286)  \n> Date: Thu, 29 Nov 2018 12:56:18 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION aac8b80f73e9b221e7dc4370aefe2e43a16df286)\n[//]: # (START_SECTION b935064fa71c603cf65127440c336fef96683426)\n### Update use-cases.rst\n\n> Commit: [b935064fa71c603cf65127440c336fef96683426](https://github.com/dOpensource/dsiprouter/commit/b935064fa71c603cf65127440c336fef96683426)  \n> Date: Thu, 29 Nov 2018 12:55:39 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b935064fa71c603cf65127440c336fef96683426)\n[//]: # (START_SECTION 8cc818b9927be4dc0091eb9b93d86477d6c62c00)\n### Update use-cases.rst\n\n> Commit: [8cc818b9927be4dc0091eb9b93d86477d6c62c00](https://github.com/dOpensource/dsiprouter/commit/8cc818b9927be4dc0091eb9b93d86477d6c62c00)  \n> Date: Thu, 29 Nov 2018 12:54:10 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8cc818b9927be4dc0091eb9b93d86477d6c62c00)\n[//]: # (START_SECTION cc507c7d912a95f3f01b43188b422a6f268454e7)\n### Update use-cases.rst\n\n> Commit: [cc507c7d912a95f3f01b43188b422a6f268454e7](https://github.com/dOpensource/dsiprouter/commit/cc507c7d912a95f3f01b43188b422a6f268454e7)  \n> Date: Thu, 29 Nov 2018 12:51:01 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cc507c7d912a95f3f01b43188b422a6f268454e7)\n[//]: # (START_SECTION e4886f3453e9cdb71203ee66f2487d60ca4203ed)\n### Update use-cases.rst\n\n> Commit: [e4886f3453e9cdb71203ee66f2487d60ca4203ed](https://github.com/dOpensource/dsiprouter/commit/e4886f3453e9cdb71203ee66f2487d60ca4203ed)  \n> Date: Thu, 29 Nov 2018 12:50:22 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e4886f3453e9cdb71203ee66f2487d60ca4203ed)\n[//]: # (START_SECTION 1c6203c29dfa8ad426e201dfdd19c09b21fdc834)\n### Update use-cases.rst\n\n> Commit: [1c6203c29dfa8ad426e201dfdd19c09b21fdc834](https://github.com/dOpensource/dsiprouter/commit/1c6203c29dfa8ad426e201dfdd19c09b21fdc834)  \n> Date: Thu, 29 Nov 2018 12:48:40 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1c6203c29dfa8ad426e201dfdd19c09b21fdc834)\n[//]: # (START_SECTION 260133601bf69ecf226350be74a5fbfd20c85861)\n### Update use-cases.rst\n\n> Commit: [260133601bf69ecf226350be74a5fbfd20c85861](https://github.com/dOpensource/dsiprouter/commit/260133601bf69ecf226350be74a5fbfd20c85861)  \n> Date: Thu, 29 Nov 2018 12:45:39 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 260133601bf69ecf226350be74a5fbfd20c85861)\n[//]: # (START_SECTION 011becafa6b96e37fc37f9944c63a5b4ce2e4af4)\n### Update use-cases.rst\n\n> Commit: [011becafa6b96e37fc37f9944c63a5b4ce2e4af4](https://github.com/dOpensource/dsiprouter/commit/011becafa6b96e37fc37f9944c63a5b4ce2e4af4)  \n> Date: Thu, 29 Nov 2018 12:43:02 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 011becafa6b96e37fc37f9944c63a5b4ce2e4af4)\n[//]: # (START_SECTION 4c6e863cdc20372bed0b306a76443541989f04cb)\n### Update use-cases.rst\n\n> Commit: [4c6e863cdc20372bed0b306a76443541989f04cb](https://github.com/dOpensource/dsiprouter/commit/4c6e863cdc20372bed0b306a76443541989f04cb)  \n> Date: Thu, 29 Nov 2018 12:41:06 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4c6e863cdc20372bed0b306a76443541989f04cb)\n[//]: # (START_SECTION 8e3dddd53488518eb2af7aa822408cae39a60ca5)\n### Update use-cases.rst\n\n> Commit: [8e3dddd53488518eb2af7aa822408cae39a60ca5](https://github.com/dOpensource/dsiprouter/commit/8e3dddd53488518eb2af7aa822408cae39a60ca5)  \n> Date: Thu, 29 Nov 2018 12:38:31 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8e3dddd53488518eb2af7aa822408cae39a60ca5)\n[//]: # (START_SECTION 77f8d5efb19a381b5488a846362aff40ece9a3de)\n### Update use-cases.rst\n\n> Commit: [77f8d5efb19a381b5488a846362aff40ece9a3de](https://github.com/dOpensource/dsiprouter/commit/77f8d5efb19a381b5488a846362aff40ece9a3de)  \n> Date: Thu, 29 Nov 2018 12:35:59 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 77f8d5efb19a381b5488a846362aff40ece9a3de)\n[//]: # (START_SECTION 5eb05ac1c1e72234a30f04a7f52d4604bc8b6c06)\n### Update use-cases.rst\n\n> Commit: [5eb05ac1c1e72234a30f04a7f52d4604bc8b6c06](https://github.com/dOpensource/dsiprouter/commit/5eb05ac1c1e72234a30f04a7f52d4604bc8b6c06)  \n> Date: Thu, 29 Nov 2018 12:34:12 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5eb05ac1c1e72234a30f04a7f52d4604bc8b6c06)\n[//]: # (START_SECTION a11b72e26dc9a71430fb3fd5c4bfd5591409906c)\n### Update use-cases.rst\n\n> Commit: [a11b72e26dc9a71430fb3fd5c4bfd5591409906c](https://github.com/dOpensource/dsiprouter/commit/a11b72e26dc9a71430fb3fd5c4bfd5591409906c)  \n> Date: Thu, 29 Nov 2018 12:33:47 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a11b72e26dc9a71430fb3fd5c4bfd5591409906c)\n[//]: # (START_SECTION e37e821ee0e5e768c60afe17f0ef3959a7136f3f)\n### Update use-cases.rst\n\n> Commit: [e37e821ee0e5e768c60afe17f0ef3959a7136f3f](https://github.com/dOpensource/dsiprouter/commit/e37e821ee0e5e768c60afe17f0ef3959a7136f3f)  \n> Date: Thu, 29 Nov 2018 12:30:08 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e37e821ee0e5e768c60afe17f0ef3959a7136f3f)\n[//]: # (START_SECTION 81023b24f5a899da647ae7ce00669c040592430f)\n### Update use-cases.rst\n\n> Commit: [81023b24f5a899da647ae7ce00669c040592430f](https://github.com/dOpensource/dsiprouter/commit/81023b24f5a899da647ae7ce00669c040592430f)  \n> Date: Thu, 29 Nov 2018 12:25:19 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 81023b24f5a899da647ae7ce00669c040592430f)\n[//]: # (START_SECTION 2245c608ed6b13c94406c9d8b64626ab9dfd3d98)\n### Update use-cases.rst\n\n> Commit: [2245c608ed6b13c94406c9d8b64626ab9dfd3d98](https://github.com/dOpensource/dsiprouter/commit/2245c608ed6b13c94406c9d8b64626ab9dfd3d98)  \n> Date: Thu, 29 Nov 2018 12:23:43 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2245c608ed6b13c94406c9d8b64626ab9dfd3d98)\n[//]: # (START_SECTION 77bd401ef2b996e1709999668c2cf3852a691723)\n### Add files via upload\n\n> Commit: [77bd401ef2b996e1709999668c2cf3852a691723](https://github.com/dOpensource/dsiprouter/commit/77bd401ef2b996e1709999668c2cf3852a691723)  \n> Date: Thu, 29 Nov 2018 12:22:30 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 77bd401ef2b996e1709999668c2cf3852a691723)\n[//]: # (START_SECTION 2ed683ae1298506449b325df1ef81334f94bc79f)\n### Update use-cases.rst\n\n> Commit: [2ed683ae1298506449b325df1ef81334f94bc79f](https://github.com/dOpensource/dsiprouter/commit/2ed683ae1298506449b325df1ef81334f94bc79f)  \n> Date: Thu, 29 Nov 2018 12:21:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2ed683ae1298506449b325df1ef81334f94bc79f)\n[//]: # (START_SECTION 4f8689ee5d22cb18bc88f9d583dc1b083adc0cc1)\n### Update configuring.rst\n\n> Commit: [4f8689ee5d22cb18bc88f9d583dc1b083adc0cc1](https://github.com/dOpensource/dsiprouter/commit/4f8689ee5d22cb18bc88f9d583dc1b083adc0cc1)  \n> Date: Thu, 29 Nov 2018 10:42:32 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4f8689ee5d22cb18bc88f9d583dc1b083adc0cc1)\n[//]: # (START_SECTION aa6939f2229a12ef75fe06e5a9476a60fc07f066)\n### Update pbxs_and_endpoints.rst\n\n> Commit: [aa6939f2229a12ef75fe06e5a9476a60fc07f066](https://github.com/dOpensource/dsiprouter/commit/aa6939f2229a12ef75fe06e5a9476a60fc07f066)  \n> Date: Thu, 29 Nov 2018 10:40:04 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION aa6939f2229a12ef75fe06e5a9476a60fc07f066)\n[//]: # (START_SECTION 902370faf90e28437c058e8dcbba8e54b0c5cadd)\n### Update use-cases.rst\n\n> Commit: [902370faf90e28437c058e8dcbba8e54b0c5cadd](https://github.com/dOpensource/dsiprouter/commit/902370faf90e28437c058e8dcbba8e54b0c5cadd)  \n> Date: Wed, 28 Nov 2018 16:02:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 902370faf90e28437c058e8dcbba8e54b0c5cadd)\n[//]: # (START_SECTION f694ac7f54a186bdf9ddc123879c97fcb85e8e3f)\n### Update global_outbound_routes.rst\n\n> Commit: [f694ac7f54a186bdf9ddc123879c97fcb85e8e3f](https://github.com/dOpensource/dsiprouter/commit/f694ac7f54a186bdf9ddc123879c97fcb85e8e3f)  \n> Date: Wed, 28 Nov 2018 16:00:13 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f694ac7f54a186bdf9ddc123879c97fcb85e8e3f)\n[//]: # (START_SECTION ff660123dca05e16b49e4dc31e55e078dc584110)\n### Update use-cases.rst\n\n> Commit: [ff660123dca05e16b49e4dc31e55e078dc584110](https://github.com/dOpensource/dsiprouter/commit/ff660123dca05e16b49e4dc31e55e078dc584110)  \n> Date: Wed, 28 Nov 2018 15:53:58 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ff660123dca05e16b49e4dc31e55e078dc584110)\n[//]: # (START_SECTION 2a605297459206702f8f3a713577961f49d68b18)\n### Update use-cases.rst\n\n> Commit: [2a605297459206702f8f3a713577961f49d68b18](https://github.com/dOpensource/dsiprouter/commit/2a605297459206702f8f3a713577961f49d68b18)  \n> Date: Wed, 28 Nov 2018 15:35:52 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2a605297459206702f8f3a713577961f49d68b18)\n[//]: # (START_SECTION 42be0e853e6e504790761eb655f6028771cee855)\n### Update use-cases.rst\n\n> Commit: [42be0e853e6e504790761eb655f6028771cee855](https://github.com/dOpensource/dsiprouter/commit/42be0e853e6e504790761eb655f6028771cee855)  \n> Date: Wed, 28 Nov 2018 15:35:06 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 42be0e853e6e504790761eb655f6028771cee855)\n[//]: # (START_SECTION e388cae6f1a14f729f86d57ddf430678ac298f43)\n### Update use-cases.rst\n\n> Commit: [e388cae6f1a14f729f86d57ddf430678ac298f43](https://github.com/dOpensource/dsiprouter/commit/e388cae6f1a14f729f86d57ddf430678ac298f43)  \n> Date: Wed, 28 Nov 2018 15:32:47 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e388cae6f1a14f729f86d57ddf430678ac298f43)\n[//]: # (START_SECTION 5762ec24ff2ccd9a5731a5204015d347ef3b1dd9)\n### Update use-cases.rst\n\n> Commit: [5762ec24ff2ccd9a5731a5204015d347ef3b1dd9](https://github.com/dOpensource/dsiprouter/commit/5762ec24ff2ccd9a5731a5204015d347ef3b1dd9)  \n> Date: Wed, 28 Nov 2018 15:31:10 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5762ec24ff2ccd9a5731a5204015d347ef3b1dd9)\n[//]: # (START_SECTION 91e1ea02b1fbf3f0066af5f6161b11298fdf5feb)\n### Update use-cases.rst\n\n> Commit: [91e1ea02b1fbf3f0066af5f6161b11298fdf5feb](https://github.com/dOpensource/dsiprouter/commit/91e1ea02b1fbf3f0066af5f6161b11298fdf5feb)  \n> Date: Wed, 28 Nov 2018 15:26:56 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 91e1ea02b1fbf3f0066af5f6161b11298fdf5feb)\n[//]: # (START_SECTION b0dae40725f2be1ada240392874748c7f8f5ecb1)\n### Update use-cases.rst\n\n> Commit: [b0dae40725f2be1ada240392874748c7f8f5ecb1](https://github.com/dOpensource/dsiprouter/commit/b0dae40725f2be1ada240392874748c7f8f5ecb1)  \n> Date: Wed, 28 Nov 2018 15:25:55 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b0dae40725f2be1ada240392874748c7f8f5ecb1)\n[//]: # (START_SECTION 141dacddf82b531a609d64023e44a0b78db94ad5)\n### Update use-cases.rst\n\n> Commit: [141dacddf82b531a609d64023e44a0b78db94ad5](https://github.com/dOpensource/dsiprouter/commit/141dacddf82b531a609d64023e44a0b78db94ad5)  \n> Date: Wed, 28 Nov 2018 15:24:59 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 141dacddf82b531a609d64023e44a0b78db94ad5)\n[//]: # (START_SECTION 90b11bf5c894ecb81c395235ef3b036282c648f6)\n### Update use-cases.rst\n\n> Commit: [90b11bf5c894ecb81c395235ef3b036282c648f6](https://github.com/dOpensource/dsiprouter/commit/90b11bf5c894ecb81c395235ef3b036282c648f6)  \n> Date: Wed, 28 Nov 2018 15:23:23 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 90b11bf5c894ecb81c395235ef3b036282c648f6)\n[//]: # (START_SECTION c0856b90f4271b5e2281a15dad9fc9edbb75452c)\n### Update use-cases.rst\n\n> Commit: [c0856b90f4271b5e2281a15dad9fc9edbb75452c](https://github.com/dOpensource/dsiprouter/commit/c0856b90f4271b5e2281a15dad9fc9edbb75452c)  \n> Date: Wed, 28 Nov 2018 15:21:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c0856b90f4271b5e2281a15dad9fc9edbb75452c)\n[//]: # (START_SECTION 9e752dfd6a77d89fef094c0b3b852f1a2e945b82)\n### Update use-cases.rst\n\n> Commit: [9e752dfd6a77d89fef094c0b3b852f1a2e945b82](https://github.com/dOpensource/dsiprouter/commit/9e752dfd6a77d89fef094c0b3b852f1a2e945b82)  \n> Date: Wed, 28 Nov 2018 15:20:47 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9e752dfd6a77d89fef094c0b3b852f1a2e945b82)\n[//]: # (START_SECTION f6b39da05d9d0c96d73b4b9bb733f70c5a892c1c)\n### Update use-cases.rst\n\n> Commit: [f6b39da05d9d0c96d73b4b9bb733f70c5a892c1c](https://github.com/dOpensource/dsiprouter/commit/f6b39da05d9d0c96d73b4b9bb733f70c5a892c1c)  \n> Date: Wed, 28 Nov 2018 15:19:03 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f6b39da05d9d0c96d73b4b9bb733f70c5a892c1c)\n[//]: # (START_SECTION f8b22d9a12a9cc43e70086231388c4bff00a9b77)\n### Update use-cases.rst\n\n> Commit: [f8b22d9a12a9cc43e70086231388c4bff00a9b77](https://github.com/dOpensource/dsiprouter/commit/f8b22d9a12a9cc43e70086231388c4bff00a9b77)  \n> Date: Wed, 28 Nov 2018 15:15:10 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f8b22d9a12a9cc43e70086231388c4bff00a9b77)\n[//]: # (START_SECTION a95d1faa9b969ffe0dbcc8033e3c0da37840560d)\n### Update use-cases.rst\n\n> Commit: [a95d1faa9b969ffe0dbcc8033e3c0da37840560d](https://github.com/dOpensource/dsiprouter/commit/a95d1faa9b969ffe0dbcc8033e3c0da37840560d)  \n> Date: Wed, 28 Nov 2018 15:09:37 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a95d1faa9b969ffe0dbcc8033e3c0da37840560d)\n[//]: # (START_SECTION 33433486d2acc9e1aa4dee8c39e2478a2a5596f7)\n### Update use-cases.rst\n\n> Commit: [33433486d2acc9e1aa4dee8c39e2478a2a5596f7](https://github.com/dOpensource/dsiprouter/commit/33433486d2acc9e1aa4dee8c39e2478a2a5596f7)  \n> Date: Wed, 28 Nov 2018 14:23:10 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 33433486d2acc9e1aa4dee8c39e2478a2a5596f7)\n[//]: # (START_SECTION e3c410eb88521e5453d127fae15cb4ef8e98e3b6)\n### Update use-cases.rst\n\n> Commit: [e3c410eb88521e5453d127fae15cb4ef8e98e3b6](https://github.com/dOpensource/dsiprouter/commit/e3c410eb88521e5453d127fae15cb4ef8e98e3b6)  \n> Date: Wed, 28 Nov 2018 14:19:58 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e3c410eb88521e5453d127fae15cb4ef8e98e3b6)\n[//]: # (START_SECTION 5aed2da88eef6ca84b46e16f5085b77bfbcb98bb)\n### Update use-cases.rst\n\n> Commit: [5aed2da88eef6ca84b46e16f5085b77bfbcb98bb](https://github.com/dOpensource/dsiprouter/commit/5aed2da88eef6ca84b46e16f5085b77bfbcb98bb)  \n> Date: Wed, 28 Nov 2018 14:05:18 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5aed2da88eef6ca84b46e16f5085b77bfbcb98bb)\n[//]: # (START_SECTION ed285c33d2b968def5bd3e3e8445d294fa24f244)\n### Update use-cases.rst\n\n> Commit: [ed285c33d2b968def5bd3e3e8445d294fa24f244](https://github.com/dOpensource/dsiprouter/commit/ed285c33d2b968def5bd3e3e8445d294fa24f244)  \n> Date: Wed, 28 Nov 2018 14:03:55 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ed285c33d2b968def5bd3e3e8445d294fa24f244)\n[//]: # (START_SECTION c9aee0b1847dbc71fdaaa33be01e7d3a65c5408f)\n### Update use-cases.rst\n\n> Commit: [c9aee0b1847dbc71fdaaa33be01e7d3a65c5408f](https://github.com/dOpensource/dsiprouter/commit/c9aee0b1847dbc71fdaaa33be01e7d3a65c5408f)  \n> Date: Wed, 28 Nov 2018 13:01:40 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c9aee0b1847dbc71fdaaa33be01e7d3a65c5408f)\n[//]: # (START_SECTION c4dbb41e7b37e9e0fed01bf48eb4c4698bd6456a)\n### Update use-cases.rst\n\n> Commit: [c4dbb41e7b37e9e0fed01bf48eb4c4698bd6456a](https://github.com/dOpensource/dsiprouter/commit/c4dbb41e7b37e9e0fed01bf48eb4c4698bd6456a)  \n> Date: Wed, 28 Nov 2018 13:00:15 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c4dbb41e7b37e9e0fed01bf48eb4c4698bd6456a)\n[//]: # (START_SECTION b7c8216d1611dddd849b19920d1407fe68f3a7e0)\n### Update use-cases.rst\n\n> Commit: [b7c8216d1611dddd849b19920d1407fe68f3a7e0](https://github.com/dOpensource/dsiprouter/commit/b7c8216d1611dddd849b19920d1407fe68f3a7e0)  \n> Date: Wed, 28 Nov 2018 12:58:33 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b7c8216d1611dddd849b19920d1407fe68f3a7e0)\n[//]: # (START_SECTION 30ec734b5ff705a6b7a18dd7cdd6c5ef0ebd5231)\n### Update use-cases.rst\n\n> Commit: [30ec734b5ff705a6b7a18dd7cdd6c5ef0ebd5231](https://github.com/dOpensource/dsiprouter/commit/30ec734b5ff705a6b7a18dd7cdd6c5ef0ebd5231)  \n> Date: Wed, 28 Nov 2018 12:57:35 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 30ec734b5ff705a6b7a18dd7cdd6c5ef0ebd5231)\n[//]: # (START_SECTION 4e9375a0ab4f304a1fee44b01d68d9e30cf0c02a)\n### Update use-cases.rst\n\n> Commit: [4e9375a0ab4f304a1fee44b01d68d9e30cf0c02a](https://github.com/dOpensource/dsiprouter/commit/4e9375a0ab4f304a1fee44b01d68d9e30cf0c02a)  \n> Date: Wed, 28 Nov 2018 12:52:01 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4e9375a0ab4f304a1fee44b01d68d9e30cf0c02a)\n[//]: # (START_SECTION 4bf3482f0b7032d8b42df232d85ffaac68d9b0e8)\n### Update use-cases.rst\n\n> Commit: [4bf3482f0b7032d8b42df232d85ffaac68d9b0e8](https://github.com/dOpensource/dsiprouter/commit/4bf3482f0b7032d8b42df232d85ffaac68d9b0e8)  \n> Date: Wed, 28 Nov 2018 12:51:21 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4bf3482f0b7032d8b42df232d85ffaac68d9b0e8)\n[//]: # (START_SECTION fb3da587b3194dbc450734af89119ad69bda4456)\n### Update use-cases.rst\n\n> Commit: [fb3da587b3194dbc450734af89119ad69bda4456](https://github.com/dOpensource/dsiprouter/commit/fb3da587b3194dbc450734af89119ad69bda4456)  \n> Date: Wed, 28 Nov 2018 12:47:01 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fb3da587b3194dbc450734af89119ad69bda4456)\n[//]: # (START_SECTION eff9bf8e4c3b1ddc6619b6b6ce74238cce74bffb)\n### Update use-cases.rst\n\n> Commit: [eff9bf8e4c3b1ddc6619b6b6ce74238cce74bffb](https://github.com/dOpensource/dsiprouter/commit/eff9bf8e4c3b1ddc6619b6b6ce74238cce74bffb)  \n> Date: Wed, 28 Nov 2018 12:45:58 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eff9bf8e4c3b1ddc6619b6b6ce74238cce74bffb)\n[//]: # (START_SECTION d8926c0360032655ffb5bab4cd1fb6f34d9025de)\n### Update use-cases.rst\n\n> Commit: [d8926c0360032655ffb5bab4cd1fb6f34d9025de](https://github.com/dOpensource/dsiprouter/commit/d8926c0360032655ffb5bab4cd1fb6f34d9025de)  \n> Date: Wed, 28 Nov 2018 12:40:21 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d8926c0360032655ffb5bab4cd1fb6f34d9025de)\n[//]: # (START_SECTION 0d3ac2dc82d3d2cbd18381aa7ba3c924114d1650)\n### Update use-cases.rst\n\n> Commit: [0d3ac2dc82d3d2cbd18381aa7ba3c924114d1650](https://github.com/dOpensource/dsiprouter/commit/0d3ac2dc82d3d2cbd18381aa7ba3c924114d1650)  \n> Date: Wed, 28 Nov 2018 12:33:39 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0d3ac2dc82d3d2cbd18381aa7ba3c924114d1650)\n[//]: # (START_SECTION 17e888fb78f4c94277dd9be49b8913966ce80361)\n### Update use-cases.rst\n\n> Commit: [17e888fb78f4c94277dd9be49b8913966ce80361](https://github.com/dOpensource/dsiprouter/commit/17e888fb78f4c94277dd9be49b8913966ce80361)  \n> Date: Wed, 28 Nov 2018 12:28:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 17e888fb78f4c94277dd9be49b8913966ce80361)\n[//]: # (START_SECTION 92e94b9a2f2a4daa7d6174b2a19156ce5e3242f4)\n### Update use-cases.rst\n\n> Commit: [92e94b9a2f2a4daa7d6174b2a19156ce5e3242f4](https://github.com/dOpensource/dsiprouter/commit/92e94b9a2f2a4daa7d6174b2a19156ce5e3242f4)  \n> Date: Wed, 28 Nov 2018 12:27:08 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 92e94b9a2f2a4daa7d6174b2a19156ce5e3242f4)\n[//]: # (START_SECTION 15aeaacc15e86c6fe879d131853c4e2aafd404e2)\n### Update use-cases.rst\n\n> Commit: [15aeaacc15e86c6fe879d131853c4e2aafd404e2](https://github.com/dOpensource/dsiprouter/commit/15aeaacc15e86c6fe879d131853c4e2aafd404e2)  \n> Date: Wed, 28 Nov 2018 09:34:04 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 15aeaacc15e86c6fe879d131853c4e2aafd404e2)\n[//]: # (START_SECTION 9b7a09695d8906d7760c4f8a603ee6834639145a)\n### Update carrier_groups.rst\n\n> Commit: [9b7a09695d8906d7760c4f8a603ee6834639145a](https://github.com/dOpensource/dsiprouter/commit/9b7a09695d8906d7760c4f8a603ee6834639145a)  \n> Date: Tue, 27 Nov 2018 15:43:23 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9b7a09695d8906d7760c4f8a603ee6834639145a)\n[//]: # (START_SECTION d254d07482d160713f4962ee83b2ab3ab0a061a5)\n### Update carrier_groups.rst\n\n> Commit: [d254d07482d160713f4962ee83b2ab3ab0a061a5](https://github.com/dOpensource/dsiprouter/commit/d254d07482d160713f4962ee83b2ab3ab0a061a5)  \n> Date: Tue, 27 Nov 2018 15:42:16 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d254d07482d160713f4962ee83b2ab3ab0a061a5)\n[//]: # (START_SECTION d981beee8af23eb1a88959f273c265893706c36a)\n### Update carrier_groups.rst\n\n> Commit: [d981beee8af23eb1a88959f273c265893706c36a](https://github.com/dOpensource/dsiprouter/commit/d981beee8af23eb1a88959f273c265893706c36a)  \n> Date: Tue, 27 Nov 2018 15:38:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d981beee8af23eb1a88959f273c265893706c36a)\n[//]: # (START_SECTION 0e829d781bc79e9aafaf07112a252d3989d07ae1)\n### Update carrier_groups.rst\n\n> Commit: [0e829d781bc79e9aafaf07112a252d3989d07ae1](https://github.com/dOpensource/dsiprouter/commit/0e829d781bc79e9aafaf07112a252d3989d07ae1)  \n> Date: Tue, 27 Nov 2018 15:37:32 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0e829d781bc79e9aafaf07112a252d3989d07ae1)\n[//]: # (START_SECTION 59b8313b93e359e2e953f5759cfbee35555fc6f3)\n### Update carrier_groups.rst\n\n> Commit: [59b8313b93e359e2e953f5759cfbee35555fc6f3](https://github.com/dOpensource/dsiprouter/commit/59b8313b93e359e2e953f5759cfbee35555fc6f3)  \n> Date: Tue, 27 Nov 2018 15:36:22 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 59b8313b93e359e2e953f5759cfbee35555fc6f3)\n[//]: # (START_SECTION a927825f41807b8ba3fe834bb4cdf9b19f380160)\n### Add files via upload\n\n> Commit: [a927825f41807b8ba3fe834bb4cdf9b19f380160](https://github.com/dOpensource/dsiprouter/commit/a927825f41807b8ba3fe834bb4cdf9b19f380160)  \n> Date: Tue, 27 Nov 2018 15:34:57 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a927825f41807b8ba3fe834bb4cdf9b19f380160)\n[//]: # (START_SECTION 956943487a928049235d880e5756392f7b646766)\n### Delete IP authenication.PNG\n\n> Commit: [956943487a928049235d880e5756392f7b646766](https://github.com/dOpensource/dsiprouter/commit/956943487a928049235d880e5756392f7b646766)  \n> Date: Tue, 27 Nov 2018 15:34:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 956943487a928049235d880e5756392f7b646766)\n[//]: # (START_SECTION 942b24602598af530f0b853e5e99a06786a870bd)\n### Update carrier_groups.rst\n\n> Commit: [942b24602598af530f0b853e5e99a06786a870bd](https://github.com/dOpensource/dsiprouter/commit/942b24602598af530f0b853e5e99a06786a870bd)  \n> Date: Tue, 27 Nov 2018 15:34:04 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 942b24602598af530f0b853e5e99a06786a870bd)\n[//]: # (START_SECTION 203831d712b3691c1b9167048326e433475bef56)\n### Add files via upload\n\n> Commit: [203831d712b3691c1b9167048326e433475bef56](https://github.com/dOpensource/dsiprouter/commit/203831d712b3691c1b9167048326e433475bef56)  \n> Date: Tue, 27 Nov 2018 15:30:36 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 203831d712b3691c1b9167048326e433475bef56)\n[//]: # (START_SECTION b77e88e29d7f10ba18566064095cf43575c7ef33)\n### Update use-cases.rst\n\n> Commit: [b77e88e29d7f10ba18566064095cf43575c7ef33](https://github.com/dOpensource/dsiprouter/commit/b77e88e29d7f10ba18566064095cf43575c7ef33)  \n> Date: Tue, 27 Nov 2018 15:08:47 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b77e88e29d7f10ba18566064095cf43575c7ef33)\n[//]: # (START_SECTION 9665f3c663cae93a060a8d42345493467b2afaf4)\n### Update use-cases.rst\n\n> Commit: [9665f3c663cae93a060a8d42345493467b2afaf4](https://github.com/dOpensource/dsiprouter/commit/9665f3c663cae93a060a8d42345493467b2afaf4)  \n> Date: Tue, 27 Nov 2018 15:07:53 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9665f3c663cae93a060a8d42345493467b2afaf4)\n[//]: # (START_SECTION 75e44d879c72f2d5fe03dcfd1be1730d191c34f1)\n### Update use-cases.rst\n\n> Commit: [75e44d879c72f2d5fe03dcfd1be1730d191c34f1](https://github.com/dOpensource/dsiprouter/commit/75e44d879c72f2d5fe03dcfd1be1730d191c34f1)  \n> Date: Tue, 27 Nov 2018 15:05:02 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 75e44d879c72f2d5fe03dcfd1be1730d191c34f1)\n[//]: # (START_SECTION 9241e581a68df76911cb74c0d241cdfb98abb97c)\n### Update use-cases.rst\n\n> Commit: [9241e581a68df76911cb74c0d241cdfb98abb97c](https://github.com/dOpensource/dsiprouter/commit/9241e581a68df76911cb74c0d241cdfb98abb97c)  \n> Date: Tue, 27 Nov 2018 14:56:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9241e581a68df76911cb74c0d241cdfb98abb97c)\n[//]: # (START_SECTION 589fd170829697735036d0baa90cdcc2c9461e2c)\n### Update use-cases.rst\n\n> Commit: [589fd170829697735036d0baa90cdcc2c9461e2c](https://github.com/dOpensource/dsiprouter/commit/589fd170829697735036d0baa90cdcc2c9461e2c)  \n> Date: Tue, 27 Nov 2018 10:43:19 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 589fd170829697735036d0baa90cdcc2c9461e2c)\n[//]: # (START_SECTION 42d9fb33b7cac00cb0bf98dc91556db5978c07b9)\n### Update global_outbound_routes.rst\n\n> Commit: [42d9fb33b7cac00cb0bf98dc91556db5978c07b9](https://github.com/dOpensource/dsiprouter/commit/42d9fb33b7cac00cb0bf98dc91556db5978c07b9)  \n> Date: Tue, 27 Nov 2018 10:03:20 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 42d9fb33b7cac00cb0bf98dc91556db5978c07b9)\n[//]: # (START_SECTION 4c46c9c2c6507485b069901b400ff2fbfdeb58a4)\n### Create global_outbound_routes.rst\n\n> Commit: [4c46c9c2c6507485b069901b400ff2fbfdeb58a4](https://github.com/dOpensource/dsiprouter/commit/4c46c9c2c6507485b069901b400ff2fbfdeb58a4)  \n> Date: Tue, 27 Nov 2018 10:02:24 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4c46c9c2c6507485b069901b400ff2fbfdeb58a4)\n[//]: # (START_SECTION 058c2fd0fbe424772c4ac8da958b108b149fe665)\n### Update use-cases.rst\n\n> Commit: [058c2fd0fbe424772c4ac8da958b108b149fe665](https://github.com/dOpensource/dsiprouter/commit/058c2fd0fbe424772c4ac8da958b108b149fe665)  \n> Date: Tue, 27 Nov 2018 10:01:52 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 058c2fd0fbe424772c4ac8da958b108b149fe665)\n[//]: # (START_SECTION 2779721a299d975eb9b449f9fe7d5b933ad4c541)\n### Update carrier_groups.rst\n\n> Commit: [2779721a299d975eb9b449f9fe7d5b933ad4c541](https://github.com/dOpensource/dsiprouter/commit/2779721a299d975eb9b449f9fe7d5b933ad4c541)  \n> Date: Tue, 27 Nov 2018 09:58:34 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2779721a299d975eb9b449f9fe7d5b933ad4c541)\n[//]: # (START_SECTION 6dbc1d7982c8727e149a2c026ab1ebe88d4007a5)\n### Update use-cases.rst\n\n> Commit: [6dbc1d7982c8727e149a2c026ab1ebe88d4007a5](https://github.com/dOpensource/dsiprouter/commit/6dbc1d7982c8727e149a2c026ab1ebe88d4007a5)  \n> Date: Tue, 27 Nov 2018 09:45:20 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6dbc1d7982c8727e149a2c026ab1ebe88d4007a5)\n[//]: # (START_SECTION 8b1b20920acf629cadbe99b4b06769e8e63912d9)\n### Update carrier_groups.rst\n\n> Commit: [8b1b20920acf629cadbe99b4b06769e8e63912d9](https://github.com/dOpensource/dsiprouter/commit/8b1b20920acf629cadbe99b4b06769e8e63912d9)  \n> Date: Tue, 27 Nov 2018 09:43:18 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8b1b20920acf629cadbe99b4b06769e8e63912d9)\n[//]: # (START_SECTION 3c602d135d6f170f910c7893c96487db949637af)\n### Update use-cases.rst\n\n> Commit: [3c602d135d6f170f910c7893c96487db949637af](https://github.com/dOpensource/dsiprouter/commit/3c602d135d6f170f910c7893c96487db949637af)  \n> Date: Tue, 27 Nov 2018 09:26:36 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3c602d135d6f170f910c7893c96487db949637af)\n[//]: # (START_SECTION ce76652240bca2e369fb7d09cf7d2c90a465e53e)\n### Letsencrypt will not work since the machine doesn't have a routeable domain name\n\n> Commit: [ce76652240bca2e369fb7d09cf7d2c90a465e53e](https://github.com/dOpensource/dsiprouter/commit/ce76652240bca2e369fb7d09cf7d2c90a465e53e)  \n> Date: Tue, 27 Nov 2018 00:22:51 +0000  \n> Author: mhendricks (root@debian-dsip-51-build.localdomain)  \n> Committer: mhendricks (root@debian-dsip-51-build.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ce76652240bca2e369fb7d09cf7d2c90a465e53e)\n[//]: # (START_SECTION b88fcccee58017d8eca65fca4188a9d8aeef4721)\n### Fixed some more conflicts with datatables.js\n\n> Commit: [b88fcccee58017d8eca65fca4188a9d8aeef4721](https://github.com/dOpensource/dsiprouter/commit/b88fcccee58017d8eca65fca4188a9d8aeef4721)  \n> Date: Mon, 26 Nov 2018 20:54:13 +0000  \n> Author: mhendricks (root@debian-dsip-51-build.localdomain)  \n> Committer: mhendricks (root@debian-dsip-51-build.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b88fcccee58017d8eca65fca4188a9d8aeef4721)\n[//]: # (START_SECTION 59044ef87ccf8565906b5fd8a1df49a4d450c621)\n### Update use-cases.rst\n\n> Commit: [59044ef87ccf8565906b5fd8a1df49a4d450c621](https://github.com/dOpensource/dsiprouter/commit/59044ef87ccf8565906b5fd8a1df49a4d450c621)  \n> Date: Mon, 26 Nov 2018 15:07:45 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 59044ef87ccf8565906b5fd8a1df49a4d450c621)\n[//]: # (START_SECTION 4a42678222549258646340aae4e1cde768f7abc1)\n### Update use-cases.rst\n\n> Commit: [4a42678222549258646340aae4e1cde768f7abc1](https://github.com/dOpensource/dsiprouter/commit/4a42678222549258646340aae4e1cde768f7abc1)  \n> Date: Mon, 26 Nov 2018 15:00:14 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4a42678222549258646340aae4e1cde768f7abc1)\n[//]: # (START_SECTION 13b6151582c0f91a91b3af81a79a5c5b71bdbe82)\n### Add files via upload\n\n> Commit: [13b6151582c0f91a91b3af81a79a5c5b71bdbe82](https://github.com/dOpensource/dsiprouter/commit/13b6151582c0f91a91b3af81a79a5c5b71bdbe82)  \n> Date: Mon, 26 Nov 2018 14:40:39 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 13b6151582c0f91a91b3af81a79a5c5b71bdbe82)\n[//]: # (START_SECTION 16dcdaaf7183e24e0244c572f87567afd3ee43b9)\n### Delete sip_trunking_freepbx_pjsip.png\n\n> Commit: [16dcdaaf7183e24e0244c572f87567afd3ee43b9](https://github.com/dOpensource/dsiprouter/commit/16dcdaaf7183e24e0244c572f87567afd3ee43b9)  \n> Date: Mon, 26 Nov 2018 14:34:51 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 16dcdaaf7183e24e0244c572f87567afd3ee43b9)\n[//]: # (START_SECTION 513a30c8e55f18e73dae972ce10d9db9563352e8)\n### Add files via upload\n\n> Commit: [513a30c8e55f18e73dae972ce10d9db9563352e8](https://github.com/dOpensource/dsiprouter/commit/513a30c8e55f18e73dae972ce10d9db9563352e8)  \n> Date: Sat, 24 Nov 2018 14:50:49 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 513a30c8e55f18e73dae972ce10d9db9563352e8)\n[//]: # (START_SECTION f59d5160a28f8f4407c2c989f4d41b8b8312b123)\n### Update use-cases.rst\n\n> Commit: [f59d5160a28f8f4407c2c989f4d41b8b8312b123](https://github.com/dOpensource/dsiprouter/commit/f59d5160a28f8f4407c2c989f4d41b8b8312b123)  \n> Date: Sat, 24 Nov 2018 14:50:11 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f59d5160a28f8f4407c2c989f4d41b8b8312b123)\n[//]: # (START_SECTION 3fa8411d14a191f218d4d13edef3d9858bc2e1f4)\n### Update use-cases.rst\n\n> Commit: [3fa8411d14a191f218d4d13edef3d9858bc2e1f4](https://github.com/dOpensource/dsiprouter/commit/3fa8411d14a191f218d4d13edef3d9858bc2e1f4)  \n> Date: Sat, 24 Nov 2018 14:49:29 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3fa8411d14a191f218d4d13edef3d9858bc2e1f4)\n[//]: # (START_SECTION c6976fc95170d3f8ff7ae5b6ffc261b3a0c8ebbf)\n### Update use-cases.rst\n\n> Commit: [c6976fc95170d3f8ff7ae5b6ffc261b3a0c8ebbf](https://github.com/dOpensource/dsiprouter/commit/c6976fc95170d3f8ff7ae5b6ffc261b3a0c8ebbf)  \n> Date: Sat, 24 Nov 2018 13:21:59 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c6976fc95170d3f8ff7ae5b6ffc261b3a0c8ebbf)\n[//]: # (START_SECTION e7e5b1a40b91e62a5c4b3407035d6eadde539374)\n### Update use-cases.rst\n\n> Commit: [e7e5b1a40b91e62a5c4b3407035d6eadde539374](https://github.com/dOpensource/dsiprouter/commit/e7e5b1a40b91e62a5c4b3407035d6eadde539374)  \n> Date: Sat, 24 Nov 2018 13:19:48 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e7e5b1a40b91e62a5c4b3407035d6eadde539374)\n[//]: # (START_SECTION 3bf9b70afc3ba297a2e384a431867f5b0ed00e54)\n### Update use-cases.rst\n\n> Commit: [3bf9b70afc3ba297a2e384a431867f5b0ed00e54](https://github.com/dOpensource/dsiprouter/commit/3bf9b70afc3ba297a2e384a431867f5b0ed00e54)  \n> Date: Sat, 24 Nov 2018 13:15:29 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3bf9b70afc3ba297a2e384a431867f5b0ed00e54)\n[//]: # (START_SECTION ce56e4d5705b73accffbff60048fb92f75b106bd)\n### Update use-cases.rst\n\n> Commit: [ce56e4d5705b73accffbff60048fb92f75b106bd](https://github.com/dOpensource/dsiprouter/commit/ce56e4d5705b73accffbff60048fb92f75b106bd)  \n> Date: Sat, 24 Nov 2018 13:14:37 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ce56e4d5705b73accffbff60048fb92f75b106bd)\n[//]: # (START_SECTION c04f2724aed299780444fe36da320baf4cd58f6b)\n### Update use-cases.rst\n\n> Commit: [c04f2724aed299780444fe36da320baf4cd58f6b](https://github.com/dOpensource/dsiprouter/commit/c04f2724aed299780444fe36da320baf4cd58f6b)  \n> Date: Sat, 24 Nov 2018 13:12:52 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c04f2724aed299780444fe36da320baf4cd58f6b)\n[//]: # (START_SECTION eb5d6336b9f7acfbdc5110ada01bfd8946fc4d73)\n### Update use-cases.rst\n\n> Commit: [eb5d6336b9f7acfbdc5110ada01bfd8946fc4d73](https://github.com/dOpensource/dsiprouter/commit/eb5d6336b9f7acfbdc5110ada01bfd8946fc4d73)  \n> Date: Sat, 24 Nov 2018 08:37:51 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eb5d6336b9f7acfbdc5110ada01bfd8946fc4d73)\n[//]: # (START_SECTION 3c014f58a59a8b657652c9c4babd71ff79f3d7c9)\n### Applied a patch to deal with the stale database connections, Fixed Carrier Registraton so that the Registrar Server IP is addeded to the Address table, Fixed a conflict with the datatables javascript file that was preventing other javascript from operating correctly\n\n> Commit: [3c014f58a59a8b657652c9c4babd71ff79f3d7c9](https://github.com/dOpensource/dsiprouter/commit/3c014f58a59a8b657652c9c4babd71ff79f3d7c9)  \n> Date: Mon, 19 Nov 2018 04:00:41 +0000  \n> Author: mhendricks (root@debian-dsip-51-build.localdomain)  \n> Committer: mhendricks (root@debian-dsip-51-build.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3c014f58a59a8b657652c9c4babd71ff79f3d7c9)\n[//]: # (START_SECTION 457938c59d0867580e96ae978aab47d10424ab2c)\n### Add files via upload\n\n> Commit: [457938c59d0867580e96ae978aab47d10424ab2c](https://github.com/dOpensource/dsiprouter/commit/457938c59d0867580e96ae978aab47d10424ab2c)  \n> Date: Fri, 16 Nov 2018 14:46:58 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 457938c59d0867580e96ae978aab47d10424ab2c)\n[//]: # (START_SECTION 83ecc170dca6746831ee32deeaa692ec5d97fa74)\n### Update use-cases.rst\n\n> Commit: [83ecc170dca6746831ee32deeaa692ec5d97fa74](https://github.com/dOpensource/dsiprouter/commit/83ecc170dca6746831ee32deeaa692ec5d97fa74)  \n> Date: Fri, 16 Nov 2018 13:10:42 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 83ecc170dca6746831ee32deeaa692ec5d97fa74)\n[//]: # (START_SECTION 793e678e8c54db07602dc70f5d52112e5e186f62)\n### Update use-cases.rst\n\n> Commit: [793e678e8c54db07602dc70f5d52112e5e186f62](https://github.com/dOpensource/dsiprouter/commit/793e678e8c54db07602dc70f5d52112e5e186f62)  \n> Date: Fri, 16 Nov 2018 13:04:50 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 793e678e8c54db07602dc70f5d52112e5e186f62)\n[//]: # (START_SECTION 86b349224e745fbd675d56e113138227e0574e9e)\n### Update use-cases.rst\n\n> Commit: [86b349224e745fbd675d56e113138227e0574e9e](https://github.com/dOpensource/dsiprouter/commit/86b349224e745fbd675d56e113138227e0574e9e)  \n> Date: Fri, 16 Nov 2018 13:02:31 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 86b349224e745fbd675d56e113138227e0574e9e)\n[//]: # (START_SECTION 4fdcf23e77468179dafa74a2850e0ff0b3cc72b7)\n### Add files via upload\n\n> Commit: [4fdcf23e77468179dafa74a2850e0ff0b3cc72b7](https://github.com/dOpensource/dsiprouter/commit/4fdcf23e77468179dafa74a2850e0ff0b3cc72b7)  \n> Date: Fri, 16 Nov 2018 13:00:31 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4fdcf23e77468179dafa74a2850e0ff0b3cc72b7)\n[//]: # (START_SECTION 9b2c3db2e020b5728ff8047c254b476008f1a4de)\n### Update use-cases.rst\n\n> Commit: [9b2c3db2e020b5728ff8047c254b476008f1a4de](https://github.com/dOpensource/dsiprouter/commit/9b2c3db2e020b5728ff8047c254b476008f1a4de)  \n> Date: Fri, 16 Nov 2018 13:00:02 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9b2c3db2e020b5728ff8047c254b476008f1a4de)\n[//]: # (START_SECTION bdf63dd83891ea9cddd40d02c2278cc8f4ed009f)\n### Add files via upload\n\n> Commit: [bdf63dd83891ea9cddd40d02c2278cc8f4ed009f](https://github.com/dOpensource/dsiprouter/commit/bdf63dd83891ea9cddd40d02c2278cc8f4ed009f)  \n> Date: Fri, 16 Nov 2018 12:35:30 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bdf63dd83891ea9cddd40d02c2278cc8f4ed009f)\n[//]: # (START_SECTION 84cc1f485cdb313c0e8f2085711f4307bcd931dc)\n### Create use-cases.rst\n\n> Commit: [84cc1f485cdb313c0e8f2085711f4307bcd931dc](https://github.com/dOpensource/dsiprouter/commit/84cc1f485cdb313c0e8f2085711f4307bcd931dc)  \n> Date: Fri, 16 Nov 2018 12:33:31 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 84cc1f485cdb313c0e8f2085711f4307bcd931dc)\n[//]: # (START_SECTION 1209735bc4779ed18949ef75074c7f6f2468d51e)\n### Add SSL configuratoin to install script\n\n> Commit: [1209735bc4779ed18949ef75074c7f6f2468d51e](https://github.com/dOpensource/dsiprouter/commit/1209735bc4779ed18949ef75074c7f6f2468d51e)  \n> Date: Thu, 15 Nov 2018 18:29:46 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- -- added ssl default configuration to install process\n\n\n---\n\n[//]: # (END_SECTION 1209735bc4779ed18949ef75074c7f6f2468d51e)\n[//]: # (START_SECTION f5a18b7cbe4263f45718a257c2a45f574e8af619)\n### Fixed an issue that prevented the nginx docker image from starting after the server is rebooted\n\n> Commit: [f5a18b7cbe4263f45718a257c2a45f574e8af619](https://github.com/dOpensource/dsiprouter/commit/f5a18b7cbe4263f45718a257c2a45f574e8af619)  \n> Date: Thu, 15 Nov 2018 22:54:23 +0000  \n> Author: root (root@debian-dsip-51-build.localdomain)  \n> Committer: root (root@debian-dsip-51-build.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f5a18b7cbe4263f45718a257c2a45f574e8af619)\n[//]: # (START_SECTION 82eca5ccc5d8b09ce7ab6fe8ebbd95119fdc9484)\n### Update installing.rst\n\n> Commit: [82eca5ccc5d8b09ce7ab6fe8ebbd95119fdc9484](https://github.com/dOpensource/dsiprouter/commit/82eca5ccc5d8b09ce7ab6fe8ebbd95119fdc9484)  \n> Date: Thu, 15 Nov 2018 14:58:31 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 82eca5ccc5d8b09ce7ab6fe8ebbd95119fdc9484)\n[//]: # (START_SECTION ab1d6631e5a193b7ef8902ff2201c4f4618043fa)\n### Update installing.rst\n\n> Commit: [ab1d6631e5a193b7ef8902ff2201c4f4618043fa](https://github.com/dOpensource/dsiprouter/commit/ab1d6631e5a193b7ef8902ff2201c4f4618043fa)  \n> Date: Thu, 15 Nov 2018 14:56:47 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ab1d6631e5a193b7ef8902ff2201c4f4618043fa)\n[//]: # (START_SECTION 4d3b1682c6b865b5367ecc13e32b2338e2f15a1e)\n### Update installing.rst\n\n> Commit: [4d3b1682c6b865b5367ecc13e32b2338e2f15a1e](https://github.com/dOpensource/dsiprouter/commit/4d3b1682c6b865b5367ecc13e32b2338e2f15a1e)  \n> Date: Thu, 15 Nov 2018 14:54:40 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4d3b1682c6b865b5367ecc13e32b2338e2f15a1e)\n[//]: # (START_SECTION 8b592bcdcb4b112516600b4be426f20558492029)\n### Update installing.rst\n\n> Commit: [8b592bcdcb4b112516600b4be426f20558492029](https://github.com/dOpensource/dsiprouter/commit/8b592bcdcb4b112516600b4be426f20558492029)  \n> Date: Thu, 15 Nov 2018 14:53:00 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8b592bcdcb4b112516600b4be426f20558492029)\n[//]: # (START_SECTION a60b30d533ddff616416883e153ed1d9fec6a26b)\n### Update installing.rst\n\n> Commit: [a60b30d533ddff616416883e153ed1d9fec6a26b](https://github.com/dOpensource/dsiprouter/commit/a60b30d533ddff616416883e153ed1d9fec6a26b)  \n> Date: Thu, 15 Nov 2018 14:50:33 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a60b30d533ddff616416883e153ed1d9fec6a26b)\n[//]: # (START_SECTION 91030ad3b9e0b39481dc276fe243e55a7ddc219a)\n### Turned off the debug statement\n\n> Commit: [91030ad3b9e0b39481dc276fe243e55a7ddc219a](https://github.com/dOpensource/dsiprouter/commit/91030ad3b9e0b39481dc276fe243e55a7ddc219a)  \n> Date: Thu, 15 Nov 2018 11:57:05 +0000  \n> Author: root (root@dSIPRouter-v051-build.localdomain)  \n> Committer: root (root@dSIPRouter-v051-build.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 91030ad3b9e0b39481dc276fe243e55a7ddc219a)\n[//]: # (START_SECTION 1165bb26023a7b24757c7360657008438ffeed2e)\n### Update dsiprouter.sh\n\n> Commit: [1165bb26023a7b24757c7360657008438ffeed2e](https://github.com/dOpensource/dsiprouter/commit/1165bb26023a7b24757c7360657008438ffeed2e)  \n> Date: Thu, 15 Nov 2018 06:50:58 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1165bb26023a7b24757c7360657008438ffeed2e)\n[//]: # (START_SECTION b2e277da3de0961b6717a7cc08bc23aa49de6960)\n### Fixed installer on Debian\n\n> Commit: [b2e277da3de0961b6717a7cc08bc23aa49de6960](https://github.com/dOpensource/dsiprouter/commit/b2e277da3de0961b6717a7cc08bc23aa49de6960)  \n> Date: Thu, 15 Nov 2018 11:39:56 +0000  \n> Author: root (root@dSIPRouter-v051-build.localdomain)  \n> Committer: root (root@dSIPRouter-v051-build.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b2e277da3de0961b6717a7cc08bc23aa49de6960)\n[//]: # (START_SECTION 1f767694669f7b7484cf951f7057d05386900a49)\n### Update installing.rst\n\n> Commit: [1f767694669f7b7484cf951f7057d05386900a49](https://github.com/dOpensource/dsiprouter/commit/1f767694669f7b7484cf951f7057d05386900a49)  \n> Date: Tue, 13 Nov 2018 19:42:43 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1f767694669f7b7484cf951f7057d05386900a49)\n[//]: # (START_SECTION 778c025c71c48dede13c4da1fe117677a5f9033a)\n### Update README.md\n\n> Commit: [778c025c71c48dede13c4da1fe117677a5f9033a](https://github.com/dOpensource/dsiprouter/commit/778c025c71c48dede13c4da1fe117677a5f9033a)  \n> Date: Tue, 13 Nov 2018 19:37:33 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 778c025c71c48dede13c4da1fe117677a5f9033a)\n[//]: # (START_SECTION 5f93fa3171f93c0edc84806c5924d6361644852b)\n### Update README.md\n\n> Commit: [5f93fa3171f93c0edc84806c5924d6361644852b](https://github.com/dOpensource/dsiprouter/commit/5f93fa3171f93c0edc84806c5924d6361644852b)  \n> Date: Tue, 13 Nov 2018 19:12:03 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5f93fa3171f93c0edc84806c5924d6361644852b)\n[//]: # (START_SECTION a1e1a90f319fb0440c6c321e1ecfc6ea37002891)\n### Update carrier_groups.rst\n\n> Commit: [a1e1a90f319fb0440c6c321e1ecfc6ea37002891](https://github.com/dOpensource/dsiprouter/commit/a1e1a90f319fb0440c6c321e1ecfc6ea37002891)  \n> Date: Fri, 9 Nov 2018 14:56:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a1e1a90f319fb0440c6c321e1ecfc6ea37002891)\n[//]: # (START_SECTION 75b8116f0e2c8e32e5a1df7efe04582ac9ce46f3)\n### Update carrier_groups.rst\n\n> Commit: [75b8116f0e2c8e32e5a1df7efe04582ac9ce46f3](https://github.com/dOpensource/dsiprouter/commit/75b8116f0e2c8e32e5a1df7efe04582ac9ce46f3)  \n> Date: Fri, 9 Nov 2018 14:55:12 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 75b8116f0e2c8e32e5a1df7efe04582ac9ce46f3)\n[//]: # (START_SECTION 49a18e01a1dce832861babbd2c15fa2079618389)\n### Update installing.rst\n\n> Commit: [49a18e01a1dce832861babbd2c15fa2079618389](https://github.com/dOpensource/dsiprouter/commit/49a18e01a1dce832861babbd2c15fa2079618389)  \n> Date: Fri, 9 Nov 2018 14:14:00 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 49a18e01a1dce832861babbd2c15fa2079618389)\n[//]: # (START_SECTION 99ae7d5df176c47c5ea230f8392728cffbe02030)\n### Update installing.rst\n\n> Commit: [99ae7d5df176c47c5ea230f8392728cffbe02030](https://github.com/dOpensource/dsiprouter/commit/99ae7d5df176c47c5ea230f8392728cffbe02030)  \n> Date: Fri, 9 Nov 2018 14:10:59 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 99ae7d5df176c47c5ea230f8392728cffbe02030)\n[//]: # (START_SECTION 66040b97ed790dc5312dfbc9e8592a35e6910c4e)\n### Update installing.rst\n\n> Commit: [66040b97ed790dc5312dfbc9e8592a35e6910c4e](https://github.com/dOpensource/dsiprouter/commit/66040b97ed790dc5312dfbc9e8592a35e6910c4e)  \n> Date: Fri, 9 Nov 2018 14:09:18 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 66040b97ed790dc5312dfbc9e8592a35e6910c4e)\n[//]: # (START_SECTION 1583b45b01afe9434db335b92fa314a1e1b32f7f)\n### Update installing.rst\n\n> Commit: [1583b45b01afe9434db335b92fa314a1e1b32f7f](https://github.com/dOpensource/dsiprouter/commit/1583b45b01afe9434db335b92fa314a1e1b32f7f)  \n> Date: Fri, 9 Nov 2018 14:02:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1583b45b01afe9434db335b92fa314a1e1b32f7f)\n[//]: # (START_SECTION ed8da72e7c2efe2cab3c103e6e2c2645dc1150c7)\n### Update installing.rst\n\n> Commit: [ed8da72e7c2efe2cab3c103e6e2c2645dc1150c7](https://github.com/dOpensource/dsiprouter/commit/ed8da72e7c2efe2cab3c103e6e2c2645dc1150c7)  \n> Date: Fri, 9 Nov 2018 14:00:50 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ed8da72e7c2efe2cab3c103e6e2c2645dc1150c7)\n[//]: # (START_SECTION 15e06f183af7d908ad11e8b946f575d48323ebec)\n### Update installing.rst\n\n> Commit: [15e06f183af7d908ad11e8b946f575d48323ebec](https://github.com/dOpensource/dsiprouter/commit/15e06f183af7d908ad11e8b946f575d48323ebec)  \n> Date: Fri, 9 Nov 2018 13:57:09 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 15e06f183af7d908ad11e8b946f575d48323ebec)\n[//]: # (START_SECTION bd863c42a007fb2e991d5312b03ab16fca92efc3)\n### Update installing.rst\n\n> Commit: [bd863c42a007fb2e991d5312b03ab16fca92efc3](https://github.com/dOpensource/dsiprouter/commit/bd863c42a007fb2e991d5312b03ab16fca92efc3)  \n> Date: Fri, 9 Nov 2018 13:56:04 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bd863c42a007fb2e991d5312b03ab16fca92efc3)\n[//]: # (START_SECTION 5caf4a6578033a6e34c335b0b9ba6934eb9e0733)\n### Update installing.rst\n\n> Commit: [5caf4a6578033a6e34c335b0b9ba6934eb9e0733](https://github.com/dOpensource/dsiprouter/commit/5caf4a6578033a6e34c335b0b9ba6934eb9e0733)  \n> Date: Fri, 9 Nov 2018 13:54:59 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5caf4a6578033a6e34c335b0b9ba6934eb9e0733)\n[//]: # (START_SECTION c76115212f20709531696572e531d1808d06eaa7)\n### Update installing.rst\n\n> Commit: [c76115212f20709531696572e531d1808d06eaa7](https://github.com/dOpensource/dsiprouter/commit/c76115212f20709531696572e531d1808d06eaa7)  \n> Date: Fri, 9 Nov 2018 13:51:15 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c76115212f20709531696572e531d1808d06eaa7)\n[//]: # (START_SECTION 4c8b8da65f27daa006e41869fa6dbcea848134fc)\n### Update installing.rst\n\n> Commit: [4c8b8da65f27daa006e41869fa6dbcea848134fc](https://github.com/dOpensource/dsiprouter/commit/4c8b8da65f27daa006e41869fa6dbcea848134fc)  \n> Date: Fri, 9 Nov 2018 13:46:27 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4c8b8da65f27daa006e41869fa6dbcea848134fc)\n[//]: # (START_SECTION 99bed286b6b15f62e27a13e614c00ed6688eab1e)\n### Update installing.rst\n\n> Commit: [99bed286b6b15f62e27a13e614c00ed6688eab1e](https://github.com/dOpensource/dsiprouter/commit/99bed286b6b15f62e27a13e614c00ed6688eab1e)  \n> Date: Fri, 9 Nov 2018 13:44:45 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 99bed286b6b15f62e27a13e614c00ed6688eab1e)\n[//]: # (START_SECTION 9a98edc95087411000326c3ef635792bf2399b16)\n### Update installing.rst\n\n> Commit: [9a98edc95087411000326c3ef635792bf2399b16](https://github.com/dOpensource/dsiprouter/commit/9a98edc95087411000326c3ef635792bf2399b16)  \n> Date: Fri, 9 Nov 2018 13:43:15 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9a98edc95087411000326c3ef635792bf2399b16)\n[//]: # (START_SECTION 2cfd118ad8a498a6a59c04964d85c29d9c3bb99e)\n### Update domains.rst\n\n> Commit: [2cfd118ad8a498a6a59c04964d85c29d9c3bb99e](https://github.com/dOpensource/dsiprouter/commit/2cfd118ad8a498a6a59c04964d85c29d9c3bb99e)  \n> Date: Fri, 9 Nov 2018 13:18:02 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2cfd118ad8a498a6a59c04964d85c29d9c3bb99e)\n[//]: # (START_SECTION 5b3805a2715738d8cb999690f3b441c34de25a05)\n### Update domains.rst\n\n> Commit: [5b3805a2715738d8cb999690f3b441c34de25a05](https://github.com/dOpensource/dsiprouter/commit/5b3805a2715738d8cb999690f3b441c34de25a05)  \n> Date: Fri, 9 Nov 2018 13:16:15 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5b3805a2715738d8cb999690f3b441c34de25a05)\n[//]: # (START_SECTION c5dee805da8ec12b2c1cdeb67d371fe61f673ce6)\n### Update domains.rst\n\n> Commit: [c5dee805da8ec12b2c1cdeb67d371fe61f673ce6](https://github.com/dOpensource/dsiprouter/commit/c5dee805da8ec12b2c1cdeb67d371fe61f673ce6)  \n> Date: Fri, 9 Nov 2018 13:12:47 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c5dee805da8ec12b2c1cdeb67d371fe61f673ce6)\n[//]: # (START_SECTION 7c3d12fb7a2be0e0fe22e04b9947b299a700fc06)\n### Update configuring.rst\n\n> Commit: [7c3d12fb7a2be0e0fe22e04b9947b299a700fc06](https://github.com/dOpensource/dsiprouter/commit/7c3d12fb7a2be0e0fe22e04b9947b299a700fc06)  \n> Date: Fri, 9 Nov 2018 13:11:40 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7c3d12fb7a2be0e0fe22e04b9947b299a700fc06)\n[//]: # (START_SECTION b3e095f30f3b5498926016a3c6825e9982a468eb)\n### Update domains.rst\n\n> Commit: [b3e095f30f3b5498926016a3c6825e9982a468eb](https://github.com/dOpensource/dsiprouter/commit/b3e095f30f3b5498926016a3c6825e9982a468eb)  \n> Date: Fri, 9 Nov 2018 12:27:55 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b3e095f30f3b5498926016a3c6825e9982a468eb)\n[//]: # (START_SECTION 921324ddfa980fcc711de28abc906517ee4c40b6)\n### Update domains.rst\n\n> Commit: [921324ddfa980fcc711de28abc906517ee4c40b6](https://github.com/dOpensource/dsiprouter/commit/921324ddfa980fcc711de28abc906517ee4c40b6)  \n> Date: Fri, 9 Nov 2018 12:19:39 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 921324ddfa980fcc711de28abc906517ee4c40b6)\n[//]: # (START_SECTION 2ecea64d78def7c33892f5cea8d2cf28c6d27108)\n### Add files via upload\n\n> Commit: [2ecea64d78def7c33892f5cea8d2cf28c6d27108](https://github.com/dOpensource/dsiprouter/commit/2ecea64d78def7c33892f5cea8d2cf28c6d27108)  \n> Date: Fri, 9 Nov 2018 12:17:11 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2ecea64d78def7c33892f5cea8d2cf28c6d27108)\n[//]: # (START_SECTION 2593339f116791259429a6ad1cb1e9712bc19310)\n### Delete list_of_domains.PNG\n\n> Commit: [2593339f116791259429a6ad1cb1e9712bc19310](https://github.com/dOpensource/dsiprouter/commit/2593339f116791259429a6ad1cb1e9712bc19310)  \n> Date: Fri, 9 Nov 2018 12:16:38 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2593339f116791259429a6ad1cb1e9712bc19310)\n[//]: # (START_SECTION 7dd8a17b8153ac0622ba98c720ea260b30b6b194)\n### Delete add_new_domain2.PNG\n\n> Commit: [7dd8a17b8153ac0622ba98c720ea260b30b6b194](https://github.com/dOpensource/dsiprouter/commit/7dd8a17b8153ac0622ba98c720ea260b30b6b194)  \n> Date: Fri, 9 Nov 2018 12:16:16 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7dd8a17b8153ac0622ba98c720ea260b30b6b194)\n[//]: # (START_SECTION 1750b693ae732cdf6932c1e1b5d5faf57ed4ad94)\n### Update carrier_groups.rst\n\n> Commit: [1750b693ae732cdf6932c1e1b5d5faf57ed4ad94](https://github.com/dOpensource/dsiprouter/commit/1750b693ae732cdf6932c1e1b5d5faf57ed4ad94)  \n> Date: Fri, 9 Nov 2018 12:05:37 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1750b693ae732cdf6932c1e1b5d5faf57ed4ad94)\n[//]: # (START_SECTION cfb00c047adb0dcb0415d1ded819063bc2ea8d3e)\n### Add files via upload\n\n> Commit: [cfb00c047adb0dcb0415d1ded819063bc2ea8d3e](https://github.com/dOpensource/dsiprouter/commit/cfb00c047adb0dcb0415d1ded819063bc2ea8d3e)  \n> Date: Fri, 9 Nov 2018 12:04:51 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cfb00c047adb0dcb0415d1ded819063bc2ea8d3e)\n[//]: # (START_SECTION 93a98ff4aa0a6275db7ce16c01acabfb4968deb3)\n### Update carrier_groups.rst\n\n> Commit: [93a98ff4aa0a6275db7ce16c01acabfb4968deb3](https://github.com/dOpensource/dsiprouter/commit/93a98ff4aa0a6275db7ce16c01acabfb4968deb3)  \n> Date: Fri, 9 Nov 2018 12:01:00 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 93a98ff4aa0a6275db7ce16c01acabfb4968deb3)\n[//]: # (START_SECTION 0d5f2f340ce0326222c34c2d8613ff326ffc2345)\n### Add files via upload\n\n> Commit: [0d5f2f340ce0326222c34c2d8613ff326ffc2345](https://github.com/dOpensource/dsiprouter/commit/0d5f2f340ce0326222c34c2d8613ff326ffc2345)  \n> Date: Fri, 9 Nov 2018 11:57:28 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0d5f2f340ce0326222c34c2d8613ff326ffc2345)\n[//]: # (START_SECTION 099d3584c1f3118b18af460c01fad9f6b432cd32)\n### Update carrier_groups.rst\n\n> Commit: [099d3584c1f3118b18af460c01fad9f6b432cd32](https://github.com/dOpensource/dsiprouter/commit/099d3584c1f3118b18af460c01fad9f6b432cd32)  \n> Date: Fri, 9 Nov 2018 11:50:44 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 099d3584c1f3118b18af460c01fad9f6b432cd32)\n[//]: # (START_SECTION 882de80116bc399a484d6cd6afbfea244c6e3426)\n### Update index.rst\n\n> Commit: [882de80116bc399a484d6cd6afbfea244c6e3426](https://github.com/dOpensource/dsiprouter/commit/882de80116bc399a484d6cd6afbfea244c6e3426)  \n> Date: Thu, 8 Nov 2018 15:53:24 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 882de80116bc399a484d6cd6afbfea244c6e3426)\n[//]: # (START_SECTION 6157a28a60dc731c02ac8fb31cc8591c04eb918d)\n### Update index.rst\n\n> Commit: [6157a28a60dc731c02ac8fb31cc8591c04eb918d](https://github.com/dOpensource/dsiprouter/commit/6157a28a60dc731c02ac8fb31cc8591c04eb918d)  \n> Date: Thu, 8 Nov 2018 15:51:36 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6157a28a60dc731c02ac8fb31cc8591c04eb918d)\n[//]: # (START_SECTION 7558d895a2e25800ece0e181ed1b7788e800608b)\n### Update index.rst\n\n> Commit: [7558d895a2e25800ece0e181ed1b7788e800608b](https://github.com/dOpensource/dsiprouter/commit/7558d895a2e25800ece0e181ed1b7788e800608b)  \n> Date: Thu, 8 Nov 2018 15:49:06 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7558d895a2e25800ece0e181ed1b7788e800608b)\n[//]: # (START_SECTION aedfb0d1ee3b89213984234979f2137b2a59df0b)\n### Update carrier_groups.rst\n\n> Commit: [aedfb0d1ee3b89213984234979f2137b2a59df0b](https://github.com/dOpensource/dsiprouter/commit/aedfb0d1ee3b89213984234979f2137b2a59df0b)  \n> Date: Thu, 8 Nov 2018 15:46:02 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION aedfb0d1ee3b89213984234979f2137b2a59df0b)\n[//]: # (START_SECTION fa3e1f7bf740445e180fc574fee2a15265d7c08b)\n### Update carrier_groups.rst\n\n> Commit: [fa3e1f7bf740445e180fc574fee2a15265d7c08b](https://github.com/dOpensource/dsiprouter/commit/fa3e1f7bf740445e180fc574fee2a15265d7c08b)  \n> Date: Thu, 8 Nov 2018 15:45:20 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fa3e1f7bf740445e180fc574fee2a15265d7c08b)\n[//]: # (START_SECTION 9d794dd375b30004efa9c5d0301522ba80ceb6b0)\n### Update domains.rst\n\n> Commit: [9d794dd375b30004efa9c5d0301522ba80ceb6b0](https://github.com/dOpensource/dsiprouter/commit/9d794dd375b30004efa9c5d0301522ba80ceb6b0)  \n> Date: Thu, 8 Nov 2018 15:43:28 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9d794dd375b30004efa9c5d0301522ba80ceb6b0)\n[//]: # (START_SECTION 2b15d386650893591c23669c2ecbfb529556d518)\n### Update domains.rst\n\n> Commit: [2b15d386650893591c23669c2ecbfb529556d518](https://github.com/dOpensource/dsiprouter/commit/2b15d386650893591c23669c2ecbfb529556d518)  \n> Date: Thu, 8 Nov 2018 15:42:50 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2b15d386650893591c23669c2ecbfb529556d518)\n[//]: # (START_SECTION 73f6fea1d902e7a9abf341dde9d1725b17e854b9)\n### Update domains.rst\n\n> Commit: [73f6fea1d902e7a9abf341dde9d1725b17e854b9](https://github.com/dOpensource/dsiprouter/commit/73f6fea1d902e7a9abf341dde9d1725b17e854b9)  \n> Date: Thu, 8 Nov 2018 15:32:53 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 73f6fea1d902e7a9abf341dde9d1725b17e854b9)\n[//]: # (START_SECTION 2c9d64d46eee85049b66048fa9eef659d826f9a4)\n### Update domains.rst\n\n> Commit: [2c9d64d46eee85049b66048fa9eef659d826f9a4](https://github.com/dOpensource/dsiprouter/commit/2c9d64d46eee85049b66048fa9eef659d826f9a4)  \n> Date: Thu, 8 Nov 2018 15:32:05 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2c9d64d46eee85049b66048fa9eef659d826f9a4)\n[//]: # (START_SECTION 235f903c9b8c4897a03df7bfae30f1bf45ee2f1d)\n### Update domains.rst\n\n> Commit: [235f903c9b8c4897a03df7bfae30f1bf45ee2f1d](https://github.com/dOpensource/dsiprouter/commit/235f903c9b8c4897a03df7bfae30f1bf45ee2f1d)  \n> Date: Thu, 8 Nov 2018 15:31:33 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 235f903c9b8c4897a03df7bfae30f1bf45ee2f1d)\n[//]: # (START_SECTION 9e596e2ce492b51c134c3dad22a1ba4ca35c3b02)\n### Update domains.rst\n\n> Commit: [9e596e2ce492b51c134c3dad22a1ba4ca35c3b02](https://github.com/dOpensource/dsiprouter/commit/9e596e2ce492b51c134c3dad22a1ba4ca35c3b02)  \n> Date: Thu, 8 Nov 2018 15:30:37 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9e596e2ce492b51c134c3dad22a1ba4ca35c3b02)\n[//]: # (START_SECTION b6f2f27fffabc0b0a28774c450000e24d4567371)\n### Update domains.rst\n\n> Commit: [b6f2f27fffabc0b0a28774c450000e24d4567371](https://github.com/dOpensource/dsiprouter/commit/b6f2f27fffabc0b0a28774c450000e24d4567371)  \n> Date: Thu, 8 Nov 2018 15:29:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b6f2f27fffabc0b0a28774c450000e24d4567371)\n[//]: # (START_SECTION 7a2199cb5b30f0a8f08a5975aedbb718aae4778c)\n### Update domains.rst\n\n> Commit: [7a2199cb5b30f0a8f08a5975aedbb718aae4778c](https://github.com/dOpensource/dsiprouter/commit/7a2199cb5b30f0a8f08a5975aedbb718aae4778c)  \n> Date: Thu, 8 Nov 2018 15:29:19 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7a2199cb5b30f0a8f08a5975aedbb718aae4778c)\n[//]: # (START_SECTION 66c3fe9e2253315cb6b3150eb23f0ac01f99f6b8)\n### Update domains.rst\n\n> Commit: [66c3fe9e2253315cb6b3150eb23f0ac01f99f6b8](https://github.com/dOpensource/dsiprouter/commit/66c3fe9e2253315cb6b3150eb23f0ac01f99f6b8)  \n> Date: Thu, 8 Nov 2018 15:28:40 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 66c3fe9e2253315cb6b3150eb23f0ac01f99f6b8)\n[//]: # (START_SECTION 960074e6bf4996db7b0c73c908aae5fea252623e)\n### Update domains.rst\n\n> Commit: [960074e6bf4996db7b0c73c908aae5fea252623e](https://github.com/dOpensource/dsiprouter/commit/960074e6bf4996db7b0c73c908aae5fea252623e)  \n> Date: Thu, 8 Nov 2018 15:26:44 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 960074e6bf4996db7b0c73c908aae5fea252623e)\n[//]: # (START_SECTION 44393a6e59288da83f0611f92e67119cf55b11a4)\n### Update domains.rst\n\n> Commit: [44393a6e59288da83f0611f92e67119cf55b11a4](https://github.com/dOpensource/dsiprouter/commit/44393a6e59288da83f0611f92e67119cf55b11a4)  \n> Date: Thu, 8 Nov 2018 15:25:59 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 44393a6e59288da83f0611f92e67119cf55b11a4)\n[//]: # (START_SECTION 7048f82452877ded0f3aa5dd6c51dc3c9fe3078c)\n### Update domains.rst\n\n> Commit: [7048f82452877ded0f3aa5dd6c51dc3c9fe3078c](https://github.com/dOpensource/dsiprouter/commit/7048f82452877ded0f3aa5dd6c51dc3c9fe3078c)  \n> Date: Thu, 8 Nov 2018 15:24:44 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7048f82452877ded0f3aa5dd6c51dc3c9fe3078c)\n[//]: # (START_SECTION ba2fb8652c31e34ed00a7b447bbcbca3e17e0dd6)\n### Add files via upload\n\n> Commit: [ba2fb8652c31e34ed00a7b447bbcbca3e17e0dd6](https://github.com/dOpensource/dsiprouter/commit/ba2fb8652c31e34ed00a7b447bbcbca3e17e0dd6)  \n> Date: Thu, 8 Nov 2018 15:23:15 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ba2fb8652c31e34ed00a7b447bbcbca3e17e0dd6)\n[//]: # (START_SECTION 5b4c13a16c7ff45c7f7586ae9c2d0d8a9c1cf454)\n### Update domains.rst\n\n> Commit: [5b4c13a16c7ff45c7f7586ae9c2d0d8a9c1cf454](https://github.com/dOpensource/dsiprouter/commit/5b4c13a16c7ff45c7f7586ae9c2d0d8a9c1cf454)  \n> Date: Thu, 8 Nov 2018 15:22:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5b4c13a16c7ff45c7f7586ae9c2d0d8a9c1cf454)\n[//]: # (START_SECTION cada6f3b68096eabc8f62c8a0b6f52f2f2ac181a)\n### Update carrier_groups.rst\n\n> Commit: [cada6f3b68096eabc8f62c8a0b6f52f2f2ac181a](https://github.com/dOpensource/dsiprouter/commit/cada6f3b68096eabc8f62c8a0b6f52f2f2ac181a)  \n> Date: Thu, 8 Nov 2018 15:20:18 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cada6f3b68096eabc8f62c8a0b6f52f2f2ac181a)\n[//]: # (START_SECTION 906b33da57e3aacd23f255616057dd9da460f7c1)\n### Update carrier_groups.rst\n\n> Commit: [906b33da57e3aacd23f255616057dd9da460f7c1](https://github.com/dOpensource/dsiprouter/commit/906b33da57e3aacd23f255616057dd9da460f7c1)  \n> Date: Thu, 8 Nov 2018 15:19:35 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 906b33da57e3aacd23f255616057dd9da460f7c1)\n[//]: # (START_SECTION 6969d79f6b18b40cc065ffbc52191a5bb8e5f786)\n### Update carrier_groups.rst\n\n> Commit: [6969d79f6b18b40cc065ffbc52191a5bb8e5f786](https://github.com/dOpensource/dsiprouter/commit/6969d79f6b18b40cc065ffbc52191a5bb8e5f786)  \n> Date: Thu, 8 Nov 2018 15:11:18 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6969d79f6b18b40cc065ffbc52191a5bb8e5f786)\n[//]: # (START_SECTION 20ad3212c883e78ad15895069a6f23b4e5a46ae8)\n### Update carrier_groups.rst\n\n> Commit: [20ad3212c883e78ad15895069a6f23b4e5a46ae8](https://github.com/dOpensource/dsiprouter/commit/20ad3212c883e78ad15895069a6f23b4e5a46ae8)  \n> Date: Thu, 8 Nov 2018 15:10:36 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 20ad3212c883e78ad15895069a6f23b4e5a46ae8)\n[//]: # (START_SECTION 78b6265a9d3e0a2dc89cbf259b0317e8ebe22762)\n### Update carrier_groups.rst\n\n> Commit: [78b6265a9d3e0a2dc89cbf259b0317e8ebe22762](https://github.com/dOpensource/dsiprouter/commit/78b6265a9d3e0a2dc89cbf259b0317e8ebe22762)  \n> Date: Thu, 8 Nov 2018 15:10:02 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 78b6265a9d3e0a2dc89cbf259b0317e8ebe22762)\n[//]: # (START_SECTION 349e61f8d63ac3e021b9181f92e49c7503678f5f)\n### Update carrier_groups.rst\n\n> Commit: [349e61f8d63ac3e021b9181f92e49c7503678f5f](https://github.com/dOpensource/dsiprouter/commit/349e61f8d63ac3e021b9181f92e49c7503678f5f)  \n> Date: Thu, 8 Nov 2018 15:09:27 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 349e61f8d63ac3e021b9181f92e49c7503678f5f)\n[//]: # (START_SECTION 14e6cb436f44a7f265c7ed613a2a9d52d313f056)\n### Update carrier_groups.rst\n\n> Commit: [14e6cb436f44a7f265c7ed613a2a9d52d313f056](https://github.com/dOpensource/dsiprouter/commit/14e6cb436f44a7f265c7ed613a2a9d52d313f056)  \n> Date: Thu, 8 Nov 2018 14:51:31 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 14e6cb436f44a7f265c7ed613a2a9d52d313f056)\n[//]: # (START_SECTION f7ef55ffd6b038fa002c795613e37a1c040ea732)\n### Update carrier_groups.rst\n\n> Commit: [f7ef55ffd6b038fa002c795613e37a1c040ea732](https://github.com/dOpensource/dsiprouter/commit/f7ef55ffd6b038fa002c795613e37a1c040ea732)  \n> Date: Thu, 8 Nov 2018 14:50:53 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f7ef55ffd6b038fa002c795613e37a1c040ea732)\n[//]: # (START_SECTION 35388acaaa4b66c8f1347d494ec5e3f38f44a4f2)\n### Update carrier_groups.rst\n\n> Commit: [35388acaaa4b66c8f1347d494ec5e3f38f44a4f2](https://github.com/dOpensource/dsiprouter/commit/35388acaaa4b66c8f1347d494ec5e3f38f44a4f2)  \n> Date: Thu, 8 Nov 2018 14:50:15 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 35388acaaa4b66c8f1347d494ec5e3f38f44a4f2)\n[//]: # (START_SECTION ddcbba0a2c19a2ae786675ecbf2bb40ffdab3a19)\n### Update carrier_groups.rst\n\n> Commit: [ddcbba0a2c19a2ae786675ecbf2bb40ffdab3a19](https://github.com/dOpensource/dsiprouter/commit/ddcbba0a2c19a2ae786675ecbf2bb40ffdab3a19)  \n> Date: Thu, 8 Nov 2018 14:49:12 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ddcbba0a2c19a2ae786675ecbf2bb40ffdab3a19)\n[//]: # (START_SECTION 6380ca9f409b6ac30e59fcab658ace2587ab45d4)\n### Update carrier_groups.rst\n\n> Commit: [6380ca9f409b6ac30e59fcab658ace2587ab45d4](https://github.com/dOpensource/dsiprouter/commit/6380ca9f409b6ac30e59fcab658ace2587ab45d4)  \n> Date: Thu, 8 Nov 2018 14:47:02 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6380ca9f409b6ac30e59fcab658ace2587ab45d4)\n[//]: # (START_SECTION 8322b7b03295246732ec4334f8c66c9073990faa)\n### Delete add_carrier_details.PNG\n\n> Commit: [8322b7b03295246732ec4334f8c66c9073990faa](https://github.com/dOpensource/dsiprouter/commit/8322b7b03295246732ec4334f8c66c9073990faa)  \n> Date: Thu, 8 Nov 2018 14:46:20 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8322b7b03295246732ec4334f8c66c9073990faa)\n[//]: # (START_SECTION 659de73d00997d6ef6fbe9bace3a4c9939a5df3d)\n### Delete add_new_carrier_details.JPG\n\n> Commit: [659de73d00997d6ef6fbe9bace3a4c9939a5df3d](https://github.com/dOpensource/dsiprouter/commit/659de73d00997d6ef6fbe9bace3a4c9939a5df3d)  \n> Date: Thu, 8 Nov 2018 14:45:43 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 659de73d00997d6ef6fbe9bace3a4c9939a5df3d)\n[//]: # (START_SECTION e6ff5cf5cb93dae1a7355a6b495f94bd9c746d55)\n### Update carrier_groups.rst\n\n> Commit: [e6ff5cf5cb93dae1a7355a6b495f94bd9c746d55](https://github.com/dOpensource/dsiprouter/commit/e6ff5cf5cb93dae1a7355a6b495f94bd9c746d55)  \n> Date: Thu, 8 Nov 2018 14:44:56 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e6ff5cf5cb93dae1a7355a6b495f94bd9c746d55)\n[//]: # (START_SECTION 0bfb0af828d877f5e65004dc3780bda2649e80f6)\n### Update carrier_groups.rst\n\n> Commit: [0bfb0af828d877f5e65004dc3780bda2649e80f6](https://github.com/dOpensource/dsiprouter/commit/0bfb0af828d877f5e65004dc3780bda2649e80f6)  \n> Date: Thu, 8 Nov 2018 14:44:12 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0bfb0af828d877f5e65004dc3780bda2649e80f6)\n[//]: # (START_SECTION 45dccd21c2191002a5bed33c744a397b895c1b0a)\n### Add files via upload\n\n> Commit: [45dccd21c2191002a5bed33c744a397b895c1b0a](https://github.com/dOpensource/dsiprouter/commit/45dccd21c2191002a5bed33c744a397b895c1b0a)  \n> Date: Thu, 8 Nov 2018 14:42:18 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 45dccd21c2191002a5bed33c744a397b895c1b0a)\n[//]: # (START_SECTION d726380487cef794abd00e00b27d3c535e18d92b)\n### Update carrier_groups.rst\n\n> Commit: [d726380487cef794abd00e00b27d3c535e18d92b](https://github.com/dOpensource/dsiprouter/commit/d726380487cef794abd00e00b27d3c535e18d92b)  \n> Date: Thu, 8 Nov 2018 14:37:53 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d726380487cef794abd00e00b27d3c535e18d92b)\n[//]: # (START_SECTION 5357705e425b25400ee584bc84d56ede14667eec)\n### Update carrier_groups.rst\n\n> Commit: [5357705e425b25400ee584bc84d56ede14667eec](https://github.com/dOpensource/dsiprouter/commit/5357705e425b25400ee584bc84d56ede14667eec)  \n> Date: Thu, 8 Nov 2018 11:28:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5357705e425b25400ee584bc84d56ede14667eec)\n[//]: # (START_SECTION a1409c59deb3714ffab1cba0d486cb3ccb2fa51f)\n### Update carrier_groups.rst\n\n> Commit: [a1409c59deb3714ffab1cba0d486cb3ccb2fa51f](https://github.com/dOpensource/dsiprouter/commit/a1409c59deb3714ffab1cba0d486cb3ccb2fa51f)  \n> Date: Thu, 8 Nov 2018 11:25:10 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a1409c59deb3714ffab1cba0d486cb3ccb2fa51f)\n[//]: # (START_SECTION 013023749f30c305652c61d35ed73d4f58795809)\n### Update index.rst\n\n> Commit: [013023749f30c305652c61d35ed73d4f58795809](https://github.com/dOpensource/dsiprouter/commit/013023749f30c305652c61d35ed73d4f58795809)  \n> Date: Thu, 8 Nov 2018 11:20:16 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 013023749f30c305652c61d35ed73d4f58795809)\n[//]: # (START_SECTION c3f97845900139ba10929105a9c3e8dca34c6672)\n### Update index.rst\n\n> Commit: [c3f97845900139ba10929105a9c3e8dca34c6672](https://github.com/dOpensource/dsiprouter/commit/c3f97845900139ba10929105a9c3e8dca34c6672)  \n> Date: Thu, 8 Nov 2018 11:19:37 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c3f97845900139ba10929105a9c3e8dca34c6672)\n[//]: # (START_SECTION 3f7f2a13de3b202ed48ecce83c8f093bcf421bd9)\n### Update index.rst\n\n> Commit: [3f7f2a13de3b202ed48ecce83c8f093bcf421bd9](https://github.com/dOpensource/dsiprouter/commit/3f7f2a13de3b202ed48ecce83c8f093bcf421bd9)  \n> Date: Thu, 8 Nov 2018 11:18:08 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3f7f2a13de3b202ed48ecce83c8f093bcf421bd9)\n[//]: # (START_SECTION 3d133ee7a6ae25903e1cc01eb8e6a9384b34ac44)\n### Update index.rst\n\n> Commit: [3d133ee7a6ae25903e1cc01eb8e6a9384b34ac44](https://github.com/dOpensource/dsiprouter/commit/3d133ee7a6ae25903e1cc01eb8e6a9384b34ac44)  \n> Date: Thu, 8 Nov 2018 11:16:52 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3d133ee7a6ae25903e1cc01eb8e6a9384b34ac44)\n[//]: # (START_SECTION 4a0337f57b3727affc04511ce9547483986bfb77)\n### Update index.rst\n\n> Commit: [4a0337f57b3727affc04511ce9547483986bfb77](https://github.com/dOpensource/dsiprouter/commit/4a0337f57b3727affc04511ce9547483986bfb77)  \n> Date: Thu, 8 Nov 2018 11:15:44 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4a0337f57b3727affc04511ce9547483986bfb77)\n[//]: # (START_SECTION 3674dc9a515e798d00a84ac4dbf22f7198d9e0ba)\n### Update index.rst\n\n> Commit: [3674dc9a515e798d00a84ac4dbf22f7198d9e0ba](https://github.com/dOpensource/dsiprouter/commit/3674dc9a515e798d00a84ac4dbf22f7198d9e0ba)  \n> Date: Thu, 8 Nov 2018 11:02:21 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3674dc9a515e798d00a84ac4dbf22f7198d9e0ba)\n[//]: # (START_SECTION cef282028674bf43872059c47c179dd4b0170572)\n### Update index.rst\n\n> Commit: [cef282028674bf43872059c47c179dd4b0170572](https://github.com/dOpensource/dsiprouter/commit/cef282028674bf43872059c47c179dd4b0170572)  \n> Date: Thu, 8 Nov 2018 10:56:16 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cef282028674bf43872059c47c179dd4b0170572)\n[//]: # (START_SECTION 9c8404c23592bc4d5b6d932b55932b981a2f63e9)\n### Update installing.rst\n\n> Commit: [9c8404c23592bc4d5b6d932b55932b981a2f63e9](https://github.com/dOpensource/dsiprouter/commit/9c8404c23592bc4d5b6d932b55932b981a2f63e9)  \n> Date: Thu, 8 Nov 2018 07:19:02 -0800  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9c8404c23592bc4d5b6d932b55932b981a2f63e9)\n[//]: # (START_SECTION 67148356831d88c45e6879817903e8c24157b96d)\n### Update installing.rst\n\n> Commit: [67148356831d88c45e6879817903e8c24157b96d](https://github.com/dOpensource/dsiprouter/commit/67148356831d88c45e6879817903e8c24157b96d)  \n> Date: Thu, 8 Nov 2018 07:16:38 -0800  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 67148356831d88c45e6879817903e8c24157b96d)\n[//]: # (START_SECTION f7b184afe38ba624148fd86bc3ab5a4da05bd588)\n### Update index.rst\n\n> Commit: [f7b184afe38ba624148fd86bc3ab5a4da05bd588](https://github.com/dOpensource/dsiprouter/commit/f7b184afe38ba624148fd86bc3ab5a4da05bd588)  \n> Date: Thu, 8 Nov 2018 07:12:44 -0800  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f7b184afe38ba624148fd86bc3ab5a4da05bd588)\n[//]: # (START_SECTION 3d275be3ee2013f36441293189b6af4ded7c7a15)\n### Update index.rst\n\n> Commit: [3d275be3ee2013f36441293189b6af4ded7c7a15](https://github.com/dOpensource/dsiprouter/commit/3d275be3ee2013f36441293189b6af4ded7c7a15)  \n> Date: Thu, 8 Nov 2018 07:09:13 -0800  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3d275be3ee2013f36441293189b6af4ded7c7a15)\n[//]: # (START_SECTION 0708b729cacfcad3fd4d28b10f228bc1c542af74)\n### Update index.rst\n\n> Commit: [0708b729cacfcad3fd4d28b10f228bc1c542af74](https://github.com/dOpensource/dsiprouter/commit/0708b729cacfcad3fd4d28b10f228bc1c542af74)  \n> Date: Thu, 8 Nov 2018 07:08:41 -0800  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0708b729cacfcad3fd4d28b10f228bc1c542af74)\n[//]: # (START_SECTION ae02588f5a9b9cd669949895c527aee4c70b2774)\n### Update index.rst\n\n> Commit: [ae02588f5a9b9cd669949895c527aee4c70b2774](https://github.com/dOpensource/dsiprouter/commit/ae02588f5a9b9cd669949895c527aee4c70b2774)  \n> Date: Thu, 8 Nov 2018 07:08:23 -0800  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ae02588f5a9b9cd669949895c527aee4c70b2774)\n[//]: # (START_SECTION b6936e2311fa65faf0c3a17393d57f8227581f8d)\n### Update pbxs_and_endpoints.rst\n\n> Commit: [b6936e2311fa65faf0c3a17393d57f8227581f8d](https://github.com/dOpensource/dsiprouter/commit/b6936e2311fa65faf0c3a17393d57f8227581f8d)  \n> Date: Thu, 8 Nov 2018 07:32:40 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b6936e2311fa65faf0c3a17393d57f8227581f8d)\n[//]: # (START_SECTION 24306942fc9cc6c9d87fbd8d92be46558af62cb2)\n### Update pbxs_and_endpoints.rst\n\n> Commit: [24306942fc9cc6c9d87fbd8d92be46558af62cb2](https://github.com/dOpensource/dsiprouter/commit/24306942fc9cc6c9d87fbd8d92be46558af62cb2)  \n> Date: Thu, 8 Nov 2018 07:23:15 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 24306942fc9cc6c9d87fbd8d92be46558af62cb2)\n[//]: # (START_SECTION 3d81b30cf8af7ba01385391d40474159300a46a2)\n### Update pbxs_and_endpoints.rst\n\n> Commit: [3d81b30cf8af7ba01385391d40474159300a46a2](https://github.com/dOpensource/dsiprouter/commit/3d81b30cf8af7ba01385391d40474159300a46a2)  \n> Date: Thu, 8 Nov 2018 07:06:50 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3d81b30cf8af7ba01385391d40474159300a46a2)\n[//]: # (START_SECTION 645597d5629e3f45fe86c4e3d053aca3733f5d65)\n### Add files via upload\n\n> Commit: [645597d5629e3f45fe86c4e3d053aca3733f5d65](https://github.com/dOpensource/dsiprouter/commit/645597d5629e3f45fe86c4e3d053aca3733f5d65)  \n> Date: Thu, 8 Nov 2018 06:58:12 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 645597d5629e3f45fe86c4e3d053aca3733f5d65)\n[//]: # (START_SECTION 8fcb849ece00cfcddd2c9f605edeff9a64c21ce6)\n### Add files via upload\n\n> Commit: [8fcb849ece00cfcddd2c9f605edeff9a64c21ce6](https://github.com/dOpensource/dsiprouter/commit/8fcb849ece00cfcddd2c9f605edeff9a64c21ce6)  \n> Date: Thu, 8 Nov 2018 06:55:24 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8fcb849ece00cfcddd2c9f605edeff9a64c21ce6)\n[//]: # (START_SECTION 05c43abafeff549ecbd9e53cc35116bcebde1504)\n### Update domains.rst\n\n> Commit: [05c43abafeff549ecbd9e53cc35116bcebde1504](https://github.com/dOpensource/dsiprouter/commit/05c43abafeff549ecbd9e53cc35116bcebde1504)  \n> Date: Wed, 7 Nov 2018 15:21:13 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 05c43abafeff549ecbd9e53cc35116bcebde1504)\n[//]: # (START_SECTION 6119a97e5eda62f0c9930266736db34df40d9ab5)\n### Add files via upload\n\n> Commit: [6119a97e5eda62f0c9930266736db34df40d9ab5](https://github.com/dOpensource/dsiprouter/commit/6119a97e5eda62f0c9930266736db34df40d9ab5)  \n> Date: Wed, 7 Nov 2018 15:19:58 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6119a97e5eda62f0c9930266736db34df40d9ab5)\n[//]: # (START_SECTION e6afe014e74faf0727ae620fdff2dd9b2620558f)\n### Delete list_of_domains.PNG\n\n> Commit: [e6afe014e74faf0727ae620fdff2dd9b2620558f](https://github.com/dOpensource/dsiprouter/commit/e6afe014e74faf0727ae620fdff2dd9b2620558f)  \n> Date: Wed, 7 Nov 2018 15:19:35 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e6afe014e74faf0727ae620fdff2dd9b2620558f)\n[//]: # (START_SECTION d824515e08296521080f0715d5b1a2cecab028bf)\n### Update domains.rst\n\n> Commit: [d824515e08296521080f0715d5b1a2cecab028bf](https://github.com/dOpensource/dsiprouter/commit/d824515e08296521080f0715d5b1a2cecab028bf)  \n> Date: Wed, 7 Nov 2018 15:16:25 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d824515e08296521080f0715d5b1a2cecab028bf)\n[//]: # (START_SECTION e84029b031eaabf3e7b428cac52edb1026e05f57)\n### Update domains.rst\n\n> Commit: [e84029b031eaabf3e7b428cac52edb1026e05f57](https://github.com/dOpensource/dsiprouter/commit/e84029b031eaabf3e7b428cac52edb1026e05f57)  \n> Date: Wed, 7 Nov 2018 15:15:43 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e84029b031eaabf3e7b428cac52edb1026e05f57)\n[//]: # (START_SECTION 84c26dff4642f9f148a8a4fb3a24d24e4ba4c842)\n### Update domains.rst\n\n> Commit: [84c26dff4642f9f148a8a4fb3a24d24e4ba4c842](https://github.com/dOpensource/dsiprouter/commit/84c26dff4642f9f148a8a4fb3a24d24e4ba4c842)  \n> Date: Wed, 7 Nov 2018 15:03:51 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 84c26dff4642f9f148a8a4fb3a24d24e4ba4c842)\n[//]: # (START_SECTION 36adeabb0282fa7aa704023be624704aeaa7737e)\n### Update domains.rst\n\n> Commit: [36adeabb0282fa7aa704023be624704aeaa7737e](https://github.com/dOpensource/dsiprouter/commit/36adeabb0282fa7aa704023be624704aeaa7737e)  \n> Date: Wed, 7 Nov 2018 15:02:20 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 36adeabb0282fa7aa704023be624704aeaa7737e)\n[//]: # (START_SECTION 8c5369e76acb622ae7c9e7b0a9f74a288777ee58)\n### Add files via upload\n\n> Commit: [8c5369e76acb622ae7c9e7b0a9f74a288777ee58](https://github.com/dOpensource/dsiprouter/commit/8c5369e76acb622ae7c9e7b0a9f74a288777ee58)  \n> Date: Wed, 7 Nov 2018 14:58:42 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8c5369e76acb622ae7c9e7b0a9f74a288777ee58)\n[//]: # (START_SECTION bf65ac599de2482fd4016aba17374dd4365c6ab4)\n### Add files via upload\n\n> Commit: [bf65ac599de2482fd4016aba17374dd4365c6ab4](https://github.com/dOpensource/dsiprouter/commit/bf65ac599de2482fd4016aba17374dd4365c6ab4)  \n> Date: Wed, 7 Nov 2018 14:56:34 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bf65ac599de2482fd4016aba17374dd4365c6ab4)\n[//]: # (START_SECTION ab0422df0420288dfd62d32b0da5f33abfa95058)\n### Update domains.rst\n\n> Commit: [ab0422df0420288dfd62d32b0da5f33abfa95058](https://github.com/dOpensource/dsiprouter/commit/ab0422df0420288dfd62d32b0da5f33abfa95058)  \n> Date: Wed, 7 Nov 2018 14:56:12 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ab0422df0420288dfd62d32b0da5f33abfa95058)\n[//]: # (START_SECTION 1c309d0d097ce264b2c749277e22f6afb96d31fa)\n### Update domains.rst\n\n> Commit: [1c309d0d097ce264b2c749277e22f6afb96d31fa](https://github.com/dOpensource/dsiprouter/commit/1c309d0d097ce264b2c749277e22f6afb96d31fa)  \n> Date: Wed, 7 Nov 2018 14:40:31 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1c309d0d097ce264b2c749277e22f6afb96d31fa)\n[//]: # (START_SECTION f3c5d0af8f533c56483cc2c32dd59e55ef979553)\n### Add files via upload\n\n> Commit: [f3c5d0af8f533c56483cc2c32dd59e55ef979553](https://github.com/dOpensource/dsiprouter/commit/f3c5d0af8f533c56483cc2c32dd59e55ef979553)  \n> Date: Wed, 7 Nov 2018 14:39:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f3c5d0af8f533c56483cc2c32dd59e55ef979553)\n[//]: # (START_SECTION 9fe740e810c39f990d8281bc33cf6614d0a12aba)\n### Update carrier_groups.rst\n\n> Commit: [9fe740e810c39f990d8281bc33cf6614d0a12aba](https://github.com/dOpensource/dsiprouter/commit/9fe740e810c39f990d8281bc33cf6614d0a12aba)  \n> Date: Wed, 7 Nov 2018 14:38:42 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9fe740e810c39f990d8281bc33cf6614d0a12aba)\n[//]: # (START_SECTION 44345eab11a70553e1aa7c8561fe587279df20a6)\n### Update carrier_groups.rst\n\n> Commit: [44345eab11a70553e1aa7c8561fe587279df20a6](https://github.com/dOpensource/dsiprouter/commit/44345eab11a70553e1aa7c8561fe587279df20a6)  \n> Date: Wed, 7 Nov 2018 14:37:07 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 44345eab11a70553e1aa7c8561fe587279df20a6)\n[//]: # (START_SECTION 6a67776c44afc44deeb53558b02a76fdc9c176a4)\n### Update domains.rst\n\n> Commit: [6a67776c44afc44deeb53558b02a76fdc9c176a4](https://github.com/dOpensource/dsiprouter/commit/6a67776c44afc44deeb53558b02a76fdc9c176a4)  \n> Date: Wed, 7 Nov 2018 14:35:36 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6a67776c44afc44deeb53558b02a76fdc9c176a4)\n[//]: # (START_SECTION da8c57e2686d704b3a51357a70176c412816c2f3)\n### Update domains.rst\n\n> Commit: [da8c57e2686d704b3a51357a70176c412816c2f3](https://github.com/dOpensource/dsiprouter/commit/da8c57e2686d704b3a51357a70176c412816c2f3)  \n> Date: Wed, 7 Nov 2018 14:20:51 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION da8c57e2686d704b3a51357a70176c412816c2f3)\n[//]: # (START_SECTION d9e62ccce9363a4e36520ddf53c92023c87fab33)\n### Update domains.rst\n\n> Commit: [d9e62ccce9363a4e36520ddf53c92023c87fab33](https://github.com/dOpensource/dsiprouter/commit/d9e62ccce9363a4e36520ddf53c92023c87fab33)  \n> Date: Wed, 7 Nov 2018 14:20:19 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d9e62ccce9363a4e36520ddf53c92023c87fab33)\n[//]: # (START_SECTION 838d4db662059e1b0a3dcd9895b33acf606b5d35)\n### Update domains.rst\n\n> Commit: [838d4db662059e1b0a3dcd9895b33acf606b5d35](https://github.com/dOpensource/dsiprouter/commit/838d4db662059e1b0a3dcd9895b33acf606b5d35)  \n> Date: Wed, 7 Nov 2018 14:20:01 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 838d4db662059e1b0a3dcd9895b33acf606b5d35)\n[//]: # (START_SECTION 1bfdb1e48899bfe083d359d5cf3a1641bde446fa)\n### Add files via upload\n\n> Commit: [1bfdb1e48899bfe083d359d5cf3a1641bde446fa](https://github.com/dOpensource/dsiprouter/commit/1bfdb1e48899bfe083d359d5cf3a1641bde446fa)  \n> Date: Wed, 7 Nov 2018 14:19:17 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1bfdb1e48899bfe083d359d5cf3a1641bde446fa)\n[//]: # (START_SECTION ec2ffb962fe60b2874cd281f5519ba2226620d88)\n### Update domains.rst\n\n> Commit: [ec2ffb962fe60b2874cd281f5519ba2226620d88](https://github.com/dOpensource/dsiprouter/commit/ec2ffb962fe60b2874cd281f5519ba2226620d88)  \n> Date: Wed, 7 Nov 2018 14:18:33 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ec2ffb962fe60b2874cd281f5519ba2226620d88)\n[//]: # (START_SECTION 087b3022cdd7d6fe2fbd47326f9a9bfdde8105ef)\n### Update domains.rst\n\n> Commit: [087b3022cdd7d6fe2fbd47326f9a9bfdde8105ef](https://github.com/dOpensource/dsiprouter/commit/087b3022cdd7d6fe2fbd47326f9a9bfdde8105ef)  \n> Date: Wed, 7 Nov 2018 14:17:35 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 087b3022cdd7d6fe2fbd47326f9a9bfdde8105ef)\n[//]: # (START_SECTION 3f6dec3f751e3625de3027551713e3a04af0940f)\n### Update carrier_groups.rst\n\n> Commit: [3f6dec3f751e3625de3027551713e3a04af0940f](https://github.com/dOpensource/dsiprouter/commit/3f6dec3f751e3625de3027551713e3a04af0940f)  \n> Date: Wed, 7 Nov 2018 14:14:07 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3f6dec3f751e3625de3027551713e3a04af0940f)\n[//]: # (START_SECTION 77d4d74989a6cd745cfc20b016fd0cb28515ba1a)\n### Update carrier_groups.rst\n\n> Commit: [77d4d74989a6cd745cfc20b016fd0cb28515ba1a](https://github.com/dOpensource/dsiprouter/commit/77d4d74989a6cd745cfc20b016fd0cb28515ba1a)  \n> Date: Wed, 7 Nov 2018 14:08:24 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 77d4d74989a6cd745cfc20b016fd0cb28515ba1a)\n[//]: # (START_SECTION 6b77313ce0cacfb1dfaf08bbacd804652b763235)\n### Update carrier_groups.rst\n\n> Commit: [6b77313ce0cacfb1dfaf08bbacd804652b763235](https://github.com/dOpensource/dsiprouter/commit/6b77313ce0cacfb1dfaf08bbacd804652b763235)  \n> Date: Wed, 7 Nov 2018 14:03:58 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6b77313ce0cacfb1dfaf08bbacd804652b763235)\n[//]: # (START_SECTION 515cf15dfc88da8999fa60e018bacf4040f2e64b)\n### Add files via upload\n\n> Commit: [515cf15dfc88da8999fa60e018bacf4040f2e64b](https://github.com/dOpensource/dsiprouter/commit/515cf15dfc88da8999fa60e018bacf4040f2e64b)  \n> Date: Wed, 7 Nov 2018 14:02:43 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 515cf15dfc88da8999fa60e018bacf4040f2e64b)\n[//]: # (START_SECTION a56006401802aa6637d781fcb7216ed8d70d1d00)\n### Update carrier_groups.rst\n\n> Commit: [a56006401802aa6637d781fcb7216ed8d70d1d00](https://github.com/dOpensource/dsiprouter/commit/a56006401802aa6637d781fcb7216ed8d70d1d00)  \n> Date: Wed, 7 Nov 2018 13:59:26 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a56006401802aa6637d781fcb7216ed8d70d1d00)\n[//]: # (START_SECTION 6e3d80f7d4a3c6ab51e75d07a58944ce16a196f7)\n### Update carrier_groups.rst\n\n> Commit: [6e3d80f7d4a3c6ab51e75d07a58944ce16a196f7](https://github.com/dOpensource/dsiprouter/commit/6e3d80f7d4a3c6ab51e75d07a58944ce16a196f7)  \n> Date: Wed, 7 Nov 2018 13:58:33 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6e3d80f7d4a3c6ab51e75d07a58944ce16a196f7)\n[//]: # (START_SECTION e325034ee2f4ed1a4b7929b7a8100cd287d703ff)\n### Update carrier_groups.rst\n\n> Commit: [e325034ee2f4ed1a4b7929b7a8100cd287d703ff](https://github.com/dOpensource/dsiprouter/commit/e325034ee2f4ed1a4b7929b7a8100cd287d703ff)  \n> Date: Wed, 7 Nov 2018 13:57:09 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e325034ee2f4ed1a4b7929b7a8100cd287d703ff)\n[//]: # (START_SECTION 9ae76d535a2fc2e746b914fbf83dc3b097f2855a)\n### Update carrier_groups.rst\n\n> Commit: [9ae76d535a2fc2e746b914fbf83dc3b097f2855a](https://github.com/dOpensource/dsiprouter/commit/9ae76d535a2fc2e746b914fbf83dc3b097f2855a)  \n> Date: Wed, 7 Nov 2018 13:52:23 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9ae76d535a2fc2e746b914fbf83dc3b097f2855a)\n[//]: # (START_SECTION d807063563a63b7451fdbabe27127f786e4b3ad9)\n### Update carrier_groups.rst\n\n> Commit: [d807063563a63b7451fdbabe27127f786e4b3ad9](https://github.com/dOpensource/dsiprouter/commit/d807063563a63b7451fdbabe27127f786e4b3ad9)  \n> Date: Wed, 7 Nov 2018 13:51:18 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d807063563a63b7451fdbabe27127f786e4b3ad9)\n[//]: # (START_SECTION f2d8742473a1630d56bea75ac5dff0d01d9afa99)\n### Update carrier_groups.rst\n\n> Commit: [f2d8742473a1630d56bea75ac5dff0d01d9afa99](https://github.com/dOpensource/dsiprouter/commit/f2d8742473a1630d56bea75ac5dff0d01d9afa99)  \n> Date: Wed, 7 Nov 2018 13:47:23 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f2d8742473a1630d56bea75ac5dff0d01d9afa99)\n[//]: # (START_SECTION a26aee01aa92ab38bc22e8b71a9262a4b617a821)\n### Update carrier_groups.rst\n\n> Commit: [a26aee01aa92ab38bc22e8b71a9262a4b617a821](https://github.com/dOpensource/dsiprouter/commit/a26aee01aa92ab38bc22e8b71a9262a4b617a821)  \n> Date: Wed, 7 Nov 2018 13:46:20 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a26aee01aa92ab38bc22e8b71a9262a4b617a821)\n[//]: # (START_SECTION d317f3ee531139dd11203d344e6bbbf9f4487d39)\n### Add files via upload\n\n> Commit: [d317f3ee531139dd11203d344e6bbbf9f4487d39](https://github.com/dOpensource/dsiprouter/commit/d317f3ee531139dd11203d344e6bbbf9f4487d39)  \n> Date: Wed, 7 Nov 2018 13:45:02 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d317f3ee531139dd11203d344e6bbbf9f4487d39)\n[//]: # (START_SECTION e4b77fcd3992a422072251bbeae4132848d80216)\n### Delete add_carrier_details.PNG\n\n> Commit: [e4b77fcd3992a422072251bbeae4132848d80216](https://github.com/dOpensource/dsiprouter/commit/e4b77fcd3992a422072251bbeae4132848d80216)  \n> Date: Wed, 7 Nov 2018 13:13:56 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e4b77fcd3992a422072251bbeae4132848d80216)\n[//]: # (START_SECTION 2d2d33ee45056b4e43d52d731e47bb59f93c1f3f)\n### Update carrier_groups.rst\n\n> Commit: [2d2d33ee45056b4e43d52d731e47bb59f93c1f3f](https://github.com/dOpensource/dsiprouter/commit/2d2d33ee45056b4e43d52d731e47bb59f93c1f3f)  \n> Date: Wed, 7 Nov 2018 13:09:38 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2d2d33ee45056b4e43d52d731e47bb59f93c1f3f)\n[//]: # (START_SECTION 69c3f84f4636dc56fdec90e2b2ab7dc09ad70a10)\n### Update carrier_groups.rst\n\n> Commit: [69c3f84f4636dc56fdec90e2b2ab7dc09ad70a10](https://github.com/dOpensource/dsiprouter/commit/69c3f84f4636dc56fdec90e2b2ab7dc09ad70a10)  \n> Date: Wed, 7 Nov 2018 13:08:07 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 69c3f84f4636dc56fdec90e2b2ab7dc09ad70a10)\n[//]: # (START_SECTION 871ca3f67956ee0a8911c9378643e4acafdce45f)\n### Update carrier_groups.rst\n\n> Commit: [871ca3f67956ee0a8911c9378643e4acafdce45f](https://github.com/dOpensource/dsiprouter/commit/871ca3f67956ee0a8911c9378643e4acafdce45f)  \n> Date: Wed, 7 Nov 2018 13:01:46 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 871ca3f67956ee0a8911c9378643e4acafdce45f)\n[//]: # (START_SECTION 47c3c0f1ede32fbe67ae4136eee3a9ba20c6adc5)\n### Update carrier_groups.rst\n\n> Commit: [47c3c0f1ede32fbe67ae4136eee3a9ba20c6adc5](https://github.com/dOpensource/dsiprouter/commit/47c3c0f1ede32fbe67ae4136eee3a9ba20c6adc5)  \n> Date: Wed, 7 Nov 2018 13:00:39 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 47c3c0f1ede32fbe67ae4136eee3a9ba20c6adc5)\n[//]: # (START_SECTION 3a0ad942a884c78fc01fa88ca4cb46a58c85064c)\n### Update carrier_groups.rst\n\n> Commit: [3a0ad942a884c78fc01fa88ca4cb46a58c85064c](https://github.com/dOpensource/dsiprouter/commit/3a0ad942a884c78fc01fa88ca4cb46a58c85064c)  \n> Date: Wed, 7 Nov 2018 13:00:02 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3a0ad942a884c78fc01fa88ca4cb46a58c85064c)\n[//]: # (START_SECTION fcdff6652036f4ffef6051f1c9454c4527f5e12a)\n### Add files via upload\n\n> Commit: [fcdff6652036f4ffef6051f1c9454c4527f5e12a](https://github.com/dOpensource/dsiprouter/commit/fcdff6652036f4ffef6051f1c9454c4527f5e12a)  \n> Date: Wed, 7 Nov 2018 12:58:21 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fcdff6652036f4ffef6051f1c9454c4527f5e12a)\n[//]: # (START_SECTION efb20947688a815fd2fe6a80f195486fb12881d2)\n### Add files via upload\n\n> Commit: [efb20947688a815fd2fe6a80f195486fb12881d2](https://github.com/dOpensource/dsiprouter/commit/efb20947688a815fd2fe6a80f195486fb12881d2)  \n> Date: Wed, 7 Nov 2018 12:57:42 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION efb20947688a815fd2fe6a80f195486fb12881d2)\n[//]: # (START_SECTION 7dfc66fe597230c70477ab11cfdde39769ee4695)\n### Update carrier_groups.rst\n\n> Commit: [7dfc66fe597230c70477ab11cfdde39769ee4695](https://github.com/dOpensource/dsiprouter/commit/7dfc66fe597230c70477ab11cfdde39769ee4695)  \n> Date: Wed, 7 Nov 2018 12:57:22 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7dfc66fe597230c70477ab11cfdde39769ee4695)\n[//]: # (START_SECTION cc9c143fa472cdbd7805fc7f55b30773b67c1bbf)\n### Update carrier_groups.rst\n\n> Commit: [cc9c143fa472cdbd7805fc7f55b30773b67c1bbf](https://github.com/dOpensource/dsiprouter/commit/cc9c143fa472cdbd7805fc7f55b30773b67c1bbf)  \n> Date: Wed, 7 Nov 2018 12:52:17 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cc9c143fa472cdbd7805fc7f55b30773b67c1bbf)\n[//]: # (START_SECTION 50987ac70b7e56827635bbb7cdf47728d93f630c)\n### Update carrier_groups.rst\n\n> Commit: [50987ac70b7e56827635bbb7cdf47728d93f630c](https://github.com/dOpensource/dsiprouter/commit/50987ac70b7e56827635bbb7cdf47728d93f630c)  \n> Date: Wed, 7 Nov 2018 12:47:15 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 50987ac70b7e56827635bbb7cdf47728d93f630c)\n[//]: # (START_SECTION 28cf51f82d6992312b31bb44b6da8ce0524526d0)\n### Add files via upload\n\n> Commit: [28cf51f82d6992312b31bb44b6da8ce0524526d0](https://github.com/dOpensource/dsiprouter/commit/28cf51f82d6992312b31bb44b6da8ce0524526d0)  \n> Date: Wed, 7 Nov 2018 12:45:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 28cf51f82d6992312b31bb44b6da8ce0524526d0)\n[//]: # (START_SECTION 20d4b7cf6e1e9985858c4ee2d88980ab01d66808)\n### Update carrier_groups.rst\n\n> Commit: [20d4b7cf6e1e9985858c4ee2d88980ab01d66808](https://github.com/dOpensource/dsiprouter/commit/20d4b7cf6e1e9985858c4ee2d88980ab01d66808)  \n> Date: Wed, 7 Nov 2018 12:44:45 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 20d4b7cf6e1e9985858c4ee2d88980ab01d66808)\n[//]: # (START_SECTION 808b69afc436d118108029034614c19e52b872db)\n### Update carrier_groups.rst\n\n> Commit: [808b69afc436d118108029034614c19e52b872db](https://github.com/dOpensource/dsiprouter/commit/808b69afc436d118108029034614c19e52b872db)  \n> Date: Wed, 7 Nov 2018 12:43:31 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 808b69afc436d118108029034614c19e52b872db)\n[//]: # (START_SECTION 0cf97ee957242a8c63993253b38c6907507b413b)\n### Add files via upload\n\n> Commit: [0cf97ee957242a8c63993253b38c6907507b413b](https://github.com/dOpensource/dsiprouter/commit/0cf97ee957242a8c63993253b38c6907507b413b)  \n> Date: Wed, 7 Nov 2018 12:42:15 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0cf97ee957242a8c63993253b38c6907507b413b)\n[//]: # (START_SECTION d958c93d8bb186b6bf27b71b215bf5a43162efef)\n### Update carrier_groups.rst\n\n> Commit: [d958c93d8bb186b6bf27b71b215bf5a43162efef](https://github.com/dOpensource/dsiprouter/commit/d958c93d8bb186b6bf27b71b215bf5a43162efef)  \n> Date: Wed, 7 Nov 2018 12:38:48 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d958c93d8bb186b6bf27b71b215bf5a43162efef)\n[//]: # (START_SECTION c51b8d173491c8f031d228178411bc7f3c570be9)\n### Update carrier_groups.rst\n\n> Commit: [c51b8d173491c8f031d228178411bc7f3c570be9](https://github.com/dOpensource/dsiprouter/commit/c51b8d173491c8f031d228178411bc7f3c570be9)  \n> Date: Wed, 7 Nov 2018 12:35:37 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c51b8d173491c8f031d228178411bc7f3c570be9)\n[//]: # (START_SECTION fbe0b0368dbac3168e3b1344149a36f82bce698d)\n### Update carrier_groups.rst\n\n> Commit: [fbe0b0368dbac3168e3b1344149a36f82bce698d](https://github.com/dOpensource/dsiprouter/commit/fbe0b0368dbac3168e3b1344149a36f82bce698d)  \n> Date: Wed, 7 Nov 2018 12:31:43 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fbe0b0368dbac3168e3b1344149a36f82bce698d)\n[//]: # (START_SECTION 52e8475c0a63aaf90888339b8fc7c09d33b7d62e)\n### Create domains.rst\n\n> Commit: [52e8475c0a63aaf90888339b8fc7c09d33b7d62e](https://github.com/dOpensource/dsiprouter/commit/52e8475c0a63aaf90888339b8fc7c09d33b7d62e)  \n> Date: Wed, 7 Nov 2018 10:25:51 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 52e8475c0a63aaf90888339b8fc7c09d33b7d62e)\n[//]: # (START_SECTION 74d5d64ea961bd4e5c7c0fd3cc08af9702960a12)\n### Update configuring.rst\n\n> Commit: [74d5d64ea961bd4e5c7c0fd3cc08af9702960a12](https://github.com/dOpensource/dsiprouter/commit/74d5d64ea961bd4e5c7c0fd3cc08af9702960a12)  \n> Date: Wed, 7 Nov 2018 10:03:23 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 74d5d64ea961bd4e5c7c0fd3cc08af9702960a12)\n[//]: # (START_SECTION 1011dee05818fc5fcde8cbd554c495378333df72)\n### Add files via upload\n\n> Commit: [1011dee05818fc5fcde8cbd554c495378333df72](https://github.com/dOpensource/dsiprouter/commit/1011dee05818fc5fcde8cbd554c495378333df72)  \n> Date: Tue, 6 Nov 2018 21:04:54 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1011dee05818fc5fcde8cbd554c495378333df72)\n[//]: # (START_SECTION f5c077f3fb4013d56891810f4c6c3a44813d067a)\n### Delete dSIP_PBX_ADD_New_PBX.png\n\n> Commit: [f5c077f3fb4013d56891810f4c6c3a44813d067a](https://github.com/dOpensource/dsiprouter/commit/f5c077f3fb4013d56891810f4c6c3a44813d067a)  \n> Date: Tue, 6 Nov 2018 21:04:32 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f5c077f3fb4013d56891810f4c6c3a44813d067a)\n[//]: # (START_SECTION 63d466d3429e1c5e022da92b027ac6f972db6c66)\n### Add files via upload\n\n> Commit: [63d466d3429e1c5e022da92b027ac6f972db6c66](https://github.com/dOpensource/dsiprouter/commit/63d466d3429e1c5e022da92b027ac6f972db6c66)  \n> Date: Tue, 6 Nov 2018 21:02:41 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 63d466d3429e1c5e022da92b027ac6f972db6c66)\n[//]: # (START_SECTION 97e50e2c965a9f51aad3a29ff0a8d3da9098afb6)\n### Delete dSIP_PBX_ADD_New_PBX.png\n\n> Commit: [97e50e2c965a9f51aad3a29ff0a8d3da9098afb6](https://github.com/dOpensource/dsiprouter/commit/97e50e2c965a9f51aad3a29ff0a8d3da9098afb6)  \n> Date: Tue, 6 Nov 2018 21:02:17 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 97e50e2c965a9f51aad3a29ff0a8d3da9098afb6)\n[//]: # (START_SECTION cc0e3c88bd89802b937d7e6086a6efe1890fca4d)\n### Add files via upload\n\n> Commit: [cc0e3c88bd89802b937d7e6086a6efe1890fca4d](https://github.com/dOpensource/dsiprouter/commit/cc0e3c88bd89802b937d7e6086a6efe1890fca4d)  \n> Date: Tue, 6 Nov 2018 20:58:16 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cc0e3c88bd89802b937d7e6086a6efe1890fca4d)\n[//]: # (START_SECTION ed4e3511ad2d7d68dd8dc625c0cf369ee3b860aa)\n### Delete dSIP_dashboard.png\n\n> Commit: [ed4e3511ad2d7d68dd8dc625c0cf369ee3b860aa](https://github.com/dOpensource/dsiprouter/commit/ed4e3511ad2d7d68dd8dc625c0cf369ee3b860aa)  \n> Date: Tue, 6 Nov 2018 20:57:58 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ed4e3511ad2d7d68dd8dc625c0cf369ee3b860aa)\n[//]: # (START_SECTION 019ed3d873e8befadbddb1f56c43314e9d2cb4f5)\n### Add files via upload\n\n> Commit: [019ed3d873e8befadbddb1f56c43314e9d2cb4f5](https://github.com/dOpensource/dsiprouter/commit/019ed3d873e8befadbddb1f56c43314e9d2cb4f5)  \n> Date: Tue, 6 Nov 2018 20:57:31 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 019ed3d873e8befadbddb1f56c43314e9d2cb4f5)\n[//]: # (START_SECTION c99ae5928fcfa2ea35b455e3f9b3ff95aa80ebe7)\n### Delete dSIP_PBX_Add.png\n\n> Commit: [c99ae5928fcfa2ea35b455e3f9b3ff95aa80ebe7](https://github.com/dOpensource/dsiprouter/commit/c99ae5928fcfa2ea35b455e3f9b3ff95aa80ebe7)  \n> Date: Tue, 6 Nov 2018 20:57:13 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c99ae5928fcfa2ea35b455e3f9b3ff95aa80ebe7)\n[//]: # (START_SECTION 4c5061669b68ab639c5c1d541684722e6731e73f)\n### Add files via upload\n\n> Commit: [4c5061669b68ab639c5c1d541684722e6731e73f](https://github.com/dOpensource/dsiprouter/commit/4c5061669b68ab639c5c1d541684722e6731e73f)  \n> Date: Tue, 6 Nov 2018 20:56:42 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4c5061669b68ab639c5c1d541684722e6731e73f)\n[//]: # (START_SECTION e1b894edb52211d0f28c06e6f1144cac6b55440a)\n### Delete dSIP_PBX_ADD_New_PBX.png\n\n> Commit: [e1b894edb52211d0f28c06e6f1144cac6b55440a](https://github.com/dOpensource/dsiprouter/commit/e1b894edb52211d0f28c06e6f1144cac6b55440a)  \n> Date: Tue, 6 Nov 2018 20:56:21 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e1b894edb52211d0f28c06e6f1144cac6b55440a)\n[//]: # (START_SECTION ea91bf399e03c75ad6c87138eea3974bf7eacb1e)\n### Add files via upload\n\n> Commit: [ea91bf399e03c75ad6c87138eea3974bf7eacb1e](https://github.com/dOpensource/dsiprouter/commit/ea91bf399e03c75ad6c87138eea3974bf7eacb1e)  \n> Date: Tue, 6 Nov 2018 20:55:37 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ea91bf399e03c75ad6c87138eea3974bf7eacb1e)\n[//]: # (START_SECTION 4cfca2ca106590bfc26cab6d867d9330393106d1)\n### Delete dSIP_IN_Manual_Add.png\n\n> Commit: [4cfca2ca106590bfc26cab6d867d9330393106d1](https://github.com/dOpensource/dsiprouter/commit/4cfca2ca106590bfc26cab6d867d9330393106d1)  \n> Date: Tue, 6 Nov 2018 20:55:19 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4cfca2ca106590bfc26cab6d867d9330393106d1)\n[//]: # (START_SECTION 79fbf8685209d7cd2f796a132a6aa7296106ea65)\n### Add files via upload\n\n> Commit: [79fbf8685209d7cd2f796a132a6aa7296106ea65](https://github.com/dOpensource/dsiprouter/commit/79fbf8685209d7cd2f796a132a6aa7296106ea65)  \n> Date: Tue, 6 Nov 2018 20:54:42 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 79fbf8685209d7cd2f796a132a6aa7296106ea65)\n[//]: # (START_SECTION 62e57d310d794ccae30840af16211fefdc7b92cc)\n### Delete dSIP_IN_Import_DID.png\n\n> Commit: [62e57d310d794ccae30840af16211fefdc7b92cc](https://github.com/dOpensource/dsiprouter/commit/62e57d310d794ccae30840af16211fefdc7b92cc)  \n> Date: Tue, 6 Nov 2018 20:54:15 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 62e57d310d794ccae30840af16211fefdc7b92cc)\n[//]: # (START_SECTION a3bcc40ab1ea1243ffda1e621c15fd8d645998f1)\n### Add files via upload\n\n> Commit: [a3bcc40ab1ea1243ffda1e621c15fd8d645998f1](https://github.com/dOpensource/dsiprouter/commit/a3bcc40ab1ea1243ffda1e621c15fd8d645998f1)  \n> Date: Tue, 6 Nov 2018 20:53:28 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a3bcc40ab1ea1243ffda1e621c15fd8d645998f1)\n[//]: # (START_SECTION 97d82365edb7cb1c4169223f6046b9e7a9a7dbc4)\n### Delete dSIP_IN_DID_Map.png\n\n> Commit: [97d82365edb7cb1c4169223f6046b9e7a9a7dbc4](https://github.com/dOpensource/dsiprouter/commit/97d82365edb7cb1c4169223f6046b9e7a9a7dbc4)  \n> Date: Tue, 6 Nov 2018 20:53:03 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 97d82365edb7cb1c4169223f6046b9e7a9a7dbc4)\n[//]: # (START_SECTION e7e33fed434bd06a1c57287865e4187ce9237818)\n### Update pbxs_and_endpoints.rst\n\n> Commit: [e7e33fed434bd06a1c57287865e4187ce9237818](https://github.com/dOpensource/dsiprouter/commit/e7e33fed434bd06a1c57287865e4187ce9237818)  \n> Date: Tue, 6 Nov 2018 20:40:18 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e7e33fed434bd06a1c57287865e4187ce9237818)\n[//]: # (START_SECTION 689259043d83b6e3eb598d138adf6bfbcd18f38a)\n### Update pbxs_and_endpoints.rst\n\n> Commit: [689259043d83b6e3eb598d138adf6bfbcd18f38a](https://github.com/dOpensource/dsiprouter/commit/689259043d83b6e3eb598d138adf6bfbcd18f38a)  \n> Date: Tue, 6 Nov 2018 20:31:15 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 689259043d83b6e3eb598d138adf6bfbcd18f38a)\n[//]: # (START_SECTION 998b0be23813de5e4348296f0728e3eed9e51967)\n### Add files via upload\n\n> Commit: [998b0be23813de5e4348296f0728e3eed9e51967](https://github.com/dOpensource/dsiprouter/commit/998b0be23813de5e4348296f0728e3eed9e51967)  \n> Date: Tue, 6 Nov 2018 20:29:05 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 998b0be23813de5e4348296f0728e3eed9e51967)\n[//]: # (START_SECTION 2662c086013f5415753cce2c68e1b1b9af86154c)\n### Add files via upload\n\n> Commit: [2662c086013f5415753cce2c68e1b1b9af86154c](https://github.com/dOpensource/dsiprouter/commit/2662c086013f5415753cce2c68e1b1b9af86154c)  \n> Date: Tue, 6 Nov 2018 20:23:05 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2662c086013f5415753cce2c68e1b1b9af86154c)\n[//]: # (START_SECTION 50434336d76dd930cda3d78b5c783c5fe1fe9247)\n### Update pbxs_and_endpoints.rst\n\n> Commit: [50434336d76dd930cda3d78b5c783c5fe1fe9247](https://github.com/dOpensource/dsiprouter/commit/50434336d76dd930cda3d78b5c783c5fe1fe9247)  \n> Date: Tue, 6 Nov 2018 20:14:20 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 50434336d76dd930cda3d78b5c783c5fe1fe9247)\n[//]: # (START_SECTION 64e257b489b20a21a51825dc26270887ce65d7dc)\n### Update pbxs_and_endpoints.rst\n\n> Commit: [64e257b489b20a21a51825dc26270887ce65d7dc](https://github.com/dOpensource/dsiprouter/commit/64e257b489b20a21a51825dc26270887ce65d7dc)  \n> Date: Tue, 6 Nov 2018 20:07:53 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 64e257b489b20a21a51825dc26270887ce65d7dc)\n[//]: # (START_SECTION 8f403c25f510d4214b2b443ab70fda5dcb6e5112)\n### Update pbxs_and_endpoints.rst\n\n> Commit: [8f403c25f510d4214b2b443ab70fda5dcb6e5112](https://github.com/dOpensource/dsiprouter/commit/8f403c25f510d4214b2b443ab70fda5dcb6e5112)  \n> Date: Tue, 6 Nov 2018 19:56:22 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8f403c25f510d4214b2b443ab70fda5dcb6e5112)\n[//]: # (START_SECTION d1079ea98ed68b616e38d0399df138f32c4603e3)\n### Add files via upload\n\n> Commit: [d1079ea98ed68b616e38d0399df138f32c4603e3](https://github.com/dOpensource/dsiprouter/commit/d1079ea98ed68b616e38d0399df138f32c4603e3)  \n> Date: Tue, 6 Nov 2018 19:56:12 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d1079ea98ed68b616e38d0399df138f32c4603e3)\n[//]: # (START_SECTION 6a40809978d10ffa91f7be5d4dba309b99843b5e)\n### Delete dSIP_IN_Manual_Add.png\n\n> Commit: [6a40809978d10ffa91f7be5d4dba309b99843b5e](https://github.com/dOpensource/dsiprouter/commit/6a40809978d10ffa91f7be5d4dba309b99843b5e)  \n> Date: Tue, 6 Nov 2018 19:55:49 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6a40809978d10ffa91f7be5d4dba309b99843b5e)\n[//]: # (START_SECTION 083930d4bdcfab64fa0dee2e5dff0f89a0f0cc00)\n### Add files via upload\n\n> Commit: [083930d4bdcfab64fa0dee2e5dff0f89a0f0cc00](https://github.com/dOpensource/dsiprouter/commit/083930d4bdcfab64fa0dee2e5dff0f89a0f0cc00)  \n> Date: Tue, 6 Nov 2018 19:46:07 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 083930d4bdcfab64fa0dee2e5dff0f89a0f0cc00)\n[//]: # (START_SECTION 56ebe63e308dad18280e711792b7b3639a445bdc)\n### Update carrier_groups.rst\n\n> Commit: [56ebe63e308dad18280e711792b7b3639a445bdc](https://github.com/dOpensource/dsiprouter/commit/56ebe63e308dad18280e711792b7b3639a445bdc)  \n> Date: Tue, 6 Nov 2018 16:32:53 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 56ebe63e308dad18280e711792b7b3639a445bdc)\n[//]: # (START_SECTION c6ebcbbe2152f441a7d2144378d44b8c241001a7)\n### Update carrier_groups.rst\n\n> Commit: [c6ebcbbe2152f441a7d2144378d44b8c241001a7](https://github.com/dOpensource/dsiprouter/commit/c6ebcbbe2152f441a7d2144378d44b8c241001a7)  \n> Date: Tue, 6 Nov 2018 16:23:58 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c6ebcbbe2152f441a7d2144378d44b8c241001a7)\n[//]: # (START_SECTION 55c6d3a8a0bbacbda24099fb569320f0c4e39acd)\n### Update carrier_groups.rst\n\n> Commit: [55c6d3a8a0bbacbda24099fb569320f0c4e39acd](https://github.com/dOpensource/dsiprouter/commit/55c6d3a8a0bbacbda24099fb569320f0c4e39acd)  \n> Date: Tue, 6 Nov 2018 16:22:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 55c6d3a8a0bbacbda24099fb569320f0c4e39acd)\n[//]: # (START_SECTION 505160dc6af62806f7b7c413686ed997e77ff93b)\n### Update carrier_groups.rst\n\n> Commit: [505160dc6af62806f7b7c413686ed997e77ff93b](https://github.com/dOpensource/dsiprouter/commit/505160dc6af62806f7b7c413686ed997e77ff93b)  \n> Date: Tue, 6 Nov 2018 16:21:51 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 505160dc6af62806f7b7c413686ed997e77ff93b)\n[//]: # (START_SECTION 610834d41f8405322ab34d03cfe980bdc2a56d84)\n### Update carrier_groups.rst\n\n> Commit: [610834d41f8405322ab34d03cfe980bdc2a56d84](https://github.com/dOpensource/dsiprouter/commit/610834d41f8405322ab34d03cfe980bdc2a56d84)  \n> Date: Tue, 6 Nov 2018 16:21:11 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 610834d41f8405322ab34d03cfe980bdc2a56d84)\n[//]: # (START_SECTION bdb7edec43840d9dc08ee617189f028a270bae93)\n### Update carrier_groups.rst\n\n> Commit: [bdb7edec43840d9dc08ee617189f028a270bae93](https://github.com/dOpensource/dsiprouter/commit/bdb7edec43840d9dc08ee617189f028a270bae93)  \n> Date: Tue, 6 Nov 2018 16:20:36 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bdb7edec43840d9dc08ee617189f028a270bae93)\n[//]: # (START_SECTION e68e584ba418c5666457e1bc32ac203f79f2cc72)\n### Update carrier_groups.rst\n\n> Commit: [e68e584ba418c5666457e1bc32ac203f79f2cc72](https://github.com/dOpensource/dsiprouter/commit/e68e584ba418c5666457e1bc32ac203f79f2cc72)  \n> Date: Tue, 6 Nov 2018 16:19:43 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e68e584ba418c5666457e1bc32ac203f79f2cc72)\n[//]: # (START_SECTION cde2c99ed797eca9cc70508049de0de621990375)\n### Update carrier_groups.rst\n\n> Commit: [cde2c99ed797eca9cc70508049de0de621990375](https://github.com/dOpensource/dsiprouter/commit/cde2c99ed797eca9cc70508049de0de621990375)  \n> Date: Tue, 6 Nov 2018 16:18:53 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cde2c99ed797eca9cc70508049de0de621990375)\n[//]: # (START_SECTION f4eba5be0915806967fd6e14ed3f2b7e3e43374a)\n### Update carrier_groups.rst\n\n> Commit: [f4eba5be0915806967fd6e14ed3f2b7e3e43374a](https://github.com/dOpensource/dsiprouter/commit/f4eba5be0915806967fd6e14ed3f2b7e3e43374a)  \n> Date: Tue, 6 Nov 2018 16:18:04 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f4eba5be0915806967fd6e14ed3f2b7e3e43374a)\n[//]: # (START_SECTION c5fe042405e48e100513c545191cf4a575dd1164)\n### Update carrier_groups.rst\n\n> Commit: [c5fe042405e48e100513c545191cf4a575dd1164](https://github.com/dOpensource/dsiprouter/commit/c5fe042405e48e100513c545191cf4a575dd1164)  \n> Date: Tue, 6 Nov 2018 16:16:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c5fe042405e48e100513c545191cf4a575dd1164)\n[//]: # (START_SECTION 8d4c4f9393dc9f301f81265d86383dd56af1ce7a)\n### Update carrier_groups.rst\n\n> Commit: [8d4c4f9393dc9f301f81265d86383dd56af1ce7a](https://github.com/dOpensource/dsiprouter/commit/8d4c4f9393dc9f301f81265d86383dd56af1ce7a)  \n> Date: Tue, 6 Nov 2018 16:14:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8d4c4f9393dc9f301f81265d86383dd56af1ce7a)\n[//]: # (START_SECTION 84fb9ac5f657b63463d0bafb64cbceefc8f801db)\n### Update carrier_groups.rst\n\n> Commit: [84fb9ac5f657b63463d0bafb64cbceefc8f801db](https://github.com/dOpensource/dsiprouter/commit/84fb9ac5f657b63463d0bafb64cbceefc8f801db)  \n> Date: Tue, 6 Nov 2018 16:11:44 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 84fb9ac5f657b63463d0bafb64cbceefc8f801db)\n[//]: # (START_SECTION ec2ebaa5bbcba32a227d91376fc372e0dcd51a72)\n### Update carrier_groups.rst\n\n> Commit: [ec2ebaa5bbcba32a227d91376fc372e0dcd51a72](https://github.com/dOpensource/dsiprouter/commit/ec2ebaa5bbcba32a227d91376fc372e0dcd51a72)  \n> Date: Tue, 6 Nov 2018 16:11:05 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ec2ebaa5bbcba32a227d91376fc372e0dcd51a72)\n[//]: # (START_SECTION c4f07ecf7334ebc66c2b74942cd8ad19c9b80bc1)\n### Update carrier_groups.rst\n\n> Commit: [c4f07ecf7334ebc66c2b74942cd8ad19c9b80bc1](https://github.com/dOpensource/dsiprouter/commit/c4f07ecf7334ebc66c2b74942cd8ad19c9b80bc1)  \n> Date: Tue, 6 Nov 2018 16:10:24 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c4f07ecf7334ebc66c2b74942cd8ad19c9b80bc1)\n[//]: # (START_SECTION 6a6244ae5ae66857116546a84c64aa70da9fef2c)\n### Update carrier_groups.rst\n\n> Commit: [6a6244ae5ae66857116546a84c64aa70da9fef2c](https://github.com/dOpensource/dsiprouter/commit/6a6244ae5ae66857116546a84c64aa70da9fef2c)  \n> Date: Tue, 6 Nov 2018 16:06:45 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6a6244ae5ae66857116546a84c64aa70da9fef2c)\n[//]: # (START_SECTION e8c3513f99d1f734281c893f2357374893a689d9)\n### Update pbxs_and_endpoints.rst\n\n> Commit: [e8c3513f99d1f734281c893f2357374893a689d9](https://github.com/dOpensource/dsiprouter/commit/e8c3513f99d1f734281c893f2357374893a689d9)  \n> Date: Tue, 6 Nov 2018 15:55:10 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e8c3513f99d1f734281c893f2357374893a689d9)\n[//]: # (START_SECTION b389459485722e58c508fff3829687f038a14d87)\n### Add files via upload\n\n> Commit: [b389459485722e58c508fff3829687f038a14d87](https://github.com/dOpensource/dsiprouter/commit/b389459485722e58c508fff3829687f038a14d87)  \n> Date: Tue, 6 Nov 2018 15:46:15 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b389459485722e58c508fff3829687f038a14d87)\n[//]: # (START_SECTION eb75eaf1e8f52081d3ffa19737cf1e386a8e9a69)\n### Add files via upload\n\n> Commit: [eb75eaf1e8f52081d3ffa19737cf1e386a8e9a69](https://github.com/dOpensource/dsiprouter/commit/eb75eaf1e8f52081d3ffa19737cf1e386a8e9a69)  \n> Date: Tue, 6 Nov 2018 15:45:16 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eb75eaf1e8f52081d3ffa19737cf1e386a8e9a69)\n[//]: # (START_SECTION 5de2b94560963733af93ea01cfb48fb3587c3817)\n### Add files via upload\n\n> Commit: [5de2b94560963733af93ea01cfb48fb3587c3817](https://github.com/dOpensource/dsiprouter/commit/5de2b94560963733af93ea01cfb48fb3587c3817)  \n> Date: Tue, 6 Nov 2018 15:44:02 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5de2b94560963733af93ea01cfb48fb3587c3817)\n[//]: # (START_SECTION 82d3f51b5a3f6517489599c678f815f19dbee1eb)\n### Delete dsiprouter-carriers.jpg\n\n> Commit: [82d3f51b5a3f6517489599c678f815f19dbee1eb](https://github.com/dOpensource/dsiprouter/commit/82d3f51b5a3f6517489599c678f815f19dbee1eb)  \n> Date: Tue, 6 Nov 2018 15:42:34 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 82d3f51b5a3f6517489599c678f815f19dbee1eb)\n[//]: # (START_SECTION 425983a27b432824a84b090c08b16ee3f2030659)\n### Update pbxs_and_endpoints.rst\n\n> Commit: [425983a27b432824a84b090c08b16ee3f2030659](https://github.com/dOpensource/dsiprouter/commit/425983a27b432824a84b090c08b16ee3f2030659)  \n> Date: Tue, 6 Nov 2018 15:40:41 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 425983a27b432824a84b090c08b16ee3f2030659)\n[//]: # (START_SECTION 22f15c2097c4b895ebc9ee9edcd69dcf4ed055b1)\n### Update pbxs_and_endpoints.rst\n\n> Commit: [22f15c2097c4b895ebc9ee9edcd69dcf4ed055b1](https://github.com/dOpensource/dsiprouter/commit/22f15c2097c4b895ebc9ee9edcd69dcf4ed055b1)  \n> Date: Tue, 6 Nov 2018 15:39:43 -0500  \n> Author: jornsby (44816622+jornsby@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 22f15c2097c4b895ebc9ee9edcd69dcf4ed055b1)\n[//]: # (START_SECTION 456b139ff2fdd8227774fd6e49bbf2249873f9ab)\n### Update carrier_groups.rst\n\n> Commit: [456b139ff2fdd8227774fd6e49bbf2249873f9ab](https://github.com/dOpensource/dsiprouter/commit/456b139ff2fdd8227774fd6e49bbf2249873f9ab)  \n> Date: Tue, 6 Nov 2018 14:36:52 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 456b139ff2fdd8227774fd6e49bbf2249873f9ab)\n[//]: # (START_SECTION cd6fb1e206dc29f01955896415b447752adc9eed)\n### Update carrier_groups.rst\n\n> Commit: [cd6fb1e206dc29f01955896415b447752adc9eed](https://github.com/dOpensource/dsiprouter/commit/cd6fb1e206dc29f01955896415b447752adc9eed)  \n> Date: Tue, 6 Nov 2018 14:35:38 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cd6fb1e206dc29f01955896415b447752adc9eed)\n[//]: # (START_SECTION 34c97dae0dd8e3c52a4736fef455eb5c221f6bcc)\n### Update carrier_groups.rst\n\n> Commit: [34c97dae0dd8e3c52a4736fef455eb5c221f6bcc](https://github.com/dOpensource/dsiprouter/commit/34c97dae0dd8e3c52a4736fef455eb5c221f6bcc)  \n> Date: Tue, 6 Nov 2018 14:29:57 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 34c97dae0dd8e3c52a4736fef455eb5c221f6bcc)\n[//]: # (START_SECTION e4176de1db50571fe0704de7ac66ae7b33683391)\n### Delete config pic.PNG\n\n> Commit: [e4176de1db50571fe0704de7ac66ae7b33683391](https://github.com/dOpensource/dsiprouter/commit/e4176de1db50571fe0704de7ac66ae7b33683391)  \n> Date: Tue, 6 Nov 2018 14:28:38 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e4176de1db50571fe0704de7ac66ae7b33683391)\n[//]: # (START_SECTION b63f23f6797f6b07bef9c3080bcc6049785af918)\n### Update carrier_groups.rst\n\n> Commit: [b63f23f6797f6b07bef9c3080bcc6049785af918](https://github.com/dOpensource/dsiprouter/commit/b63f23f6797f6b07bef9c3080bcc6049785af918)  \n> Date: Tue, 6 Nov 2018 14:26:02 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b63f23f6797f6b07bef9c3080bcc6049785af918)\n[//]: # (START_SECTION 6db1e3b0addf8721c2afa088efe6e07550413f6b)\n### Update carrier_groups.rst\n\n> Commit: [6db1e3b0addf8721c2afa088efe6e07550413f6b](https://github.com/dOpensource/dsiprouter/commit/6db1e3b0addf8721c2afa088efe6e07550413f6b)  \n> Date: Tue, 6 Nov 2018 14:25:07 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6db1e3b0addf8721c2afa088efe6e07550413f6b)\n[//]: # (START_SECTION b578812373655cd5ad6afcae139cea32dcb3350e)\n### Add files via upload\n\n> Commit: [b578812373655cd5ad6afcae139cea32dcb3350e](https://github.com/dOpensource/dsiprouter/commit/b578812373655cd5ad6afcae139cea32dcb3350e)  \n> Date: Tue, 6 Nov 2018 14:20:07 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b578812373655cd5ad6afcae139cea32dcb3350e)\n[//]: # (START_SECTION 15452463de0bc35b8cd8ba9602221cebbaf452d2)\n### Update carrier_groups.rst\n\n> Commit: [15452463de0bc35b8cd8ba9602221cebbaf452d2](https://github.com/dOpensource/dsiprouter/commit/15452463de0bc35b8cd8ba9602221cebbaf452d2)  \n> Date: Tue, 6 Nov 2018 14:13:24 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 15452463de0bc35b8cd8ba9602221cebbaf452d2)\n[//]: # (START_SECTION 46854615c9d3487716ee21defc65158fe9139998)\n### Add files via upload\n\n> Commit: [46854615c9d3487716ee21defc65158fe9139998](https://github.com/dOpensource/dsiprouter/commit/46854615c9d3487716ee21defc65158fe9139998)  \n> Date: Tue, 6 Nov 2018 14:12:45 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 46854615c9d3487716ee21defc65158fe9139998)\n[//]: # (START_SECTION 6098310670e6198e1dc71da0c19b47402b388f4d)\n### Update carrier_groups.rst\n\n> Commit: [6098310670e6198e1dc71da0c19b47402b388f4d](https://github.com/dOpensource/dsiprouter/commit/6098310670e6198e1dc71da0c19b47402b388f4d)  \n> Date: Tue, 6 Nov 2018 14:12:18 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6098310670e6198e1dc71da0c19b47402b388f4d)\n[//]: # (START_SECTION b7026ba379540cd5dbb1cab29b626e7878d69bd0)\n### Update carrier_groups.rst\n\n> Commit: [b7026ba379540cd5dbb1cab29b626e7878d69bd0](https://github.com/dOpensource/dsiprouter/commit/b7026ba379540cd5dbb1cab29b626e7878d69bd0)  \n> Date: Tue, 6 Nov 2018 14:11:06 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b7026ba379540cd5dbb1cab29b626e7878d69bd0)\n[//]: # (START_SECTION f95551465323eb8990cddce09f0928a3f20dc9fe)\n### Update carrier_groups.rst\n\n> Commit: [f95551465323eb8990cddce09f0928a3f20dc9fe](https://github.com/dOpensource/dsiprouter/commit/f95551465323eb8990cddce09f0928a3f20dc9fe)  \n> Date: Tue, 6 Nov 2018 14:10:26 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f95551465323eb8990cddce09f0928a3f20dc9fe)\n[//]: # (START_SECTION eaedd02a8c38094878bec06cccd5820ced6d729f)\n### Update carrier_groups.rst\n\n> Commit: [eaedd02a8c38094878bec06cccd5820ced6d729f](https://github.com/dOpensource/dsiprouter/commit/eaedd02a8c38094878bec06cccd5820ced6d729f)  \n> Date: Tue, 6 Nov 2018 14:05:19 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eaedd02a8c38094878bec06cccd5820ced6d729f)\n[//]: # (START_SECTION 20cb405ed58d8ea5797ab611a06589a02ab42472)\n### Update carrier_groups.rst\n\n> Commit: [20cb405ed58d8ea5797ab611a06589a02ab42472](https://github.com/dOpensource/dsiprouter/commit/20cb405ed58d8ea5797ab611a06589a02ab42472)  \n> Date: Tue, 6 Nov 2018 13:57:07 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 20cb405ed58d8ea5797ab611a06589a02ab42472)\n[//]: # (START_SECTION 341f1bb1b6ebd2f9e2a9e5bc80ecb8e924c9a16e)\n### Update carrier_groups.rst\n\n> Commit: [341f1bb1b6ebd2f9e2a9e5bc80ecb8e924c9a16e](https://github.com/dOpensource/dsiprouter/commit/341f1bb1b6ebd2f9e2a9e5bc80ecb8e924c9a16e)  \n> Date: Tue, 6 Nov 2018 13:15:55 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 341f1bb1b6ebd2f9e2a9e5bc80ecb8e924c9a16e)\n[//]: # (START_SECTION 74b14d739ff92554a2106839daddf0acd7aa4e37)\n### Update carrier_groups.rst\n\n> Commit: [74b14d739ff92554a2106839daddf0acd7aa4e37](https://github.com/dOpensource/dsiprouter/commit/74b14d739ff92554a2106839daddf0acd7aa4e37)  \n> Date: Tue, 6 Nov 2018 13:14:48 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 74b14d739ff92554a2106839daddf0acd7aa4e37)\n[//]: # (START_SECTION 7df26ba4d81a09c01aa26660744ff0a011d38086)\n### Update carrier_groups.rst\n\n> Commit: [7df26ba4d81a09c01aa26660744ff0a011d38086](https://github.com/dOpensource/dsiprouter/commit/7df26ba4d81a09c01aa26660744ff0a011d38086)  \n> Date: Tue, 6 Nov 2018 13:13:39 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7df26ba4d81a09c01aa26660744ff0a011d38086)\n[//]: # (START_SECTION 9fdf5e42cc004a5aa42d57872456c04b94dba276)\n### Add files via upload\n\n> Commit: [9fdf5e42cc004a5aa42d57872456c04b94dba276](https://github.com/dOpensource/dsiprouter/commit/9fdf5e42cc004a5aa42d57872456c04b94dba276)  \n> Date: Tue, 6 Nov 2018 13:11:02 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9fdf5e42cc004a5aa42d57872456c04b94dba276)\n[//]: # (START_SECTION f9713fb262681100a5b9e99222af4ce02ad87295)\n### Update carrier_groups.rst\n\n> Commit: [f9713fb262681100a5b9e99222af4ce02ad87295](https://github.com/dOpensource/dsiprouter/commit/f9713fb262681100a5b9e99222af4ce02ad87295)  \n> Date: Tue, 6 Nov 2018 13:09:14 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f9713fb262681100a5b9e99222af4ce02ad87295)\n[//]: # (START_SECTION 658695c588c634cab89ec3a65054f4d89f9af18e)\n### Update carrier_groups.rst\n\n> Commit: [658695c588c634cab89ec3a65054f4d89f9af18e](https://github.com/dOpensource/dsiprouter/commit/658695c588c634cab89ec3a65054f4d89f9af18e)  \n> Date: Tue, 6 Nov 2018 13:08:39 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 658695c588c634cab89ec3a65054f4d89f9af18e)\n[//]: # (START_SECTION 9e637b7c2da277d8629ab68dab753386d116a233)\n### Update carrier_groups.rst\n\n> Commit: [9e637b7c2da277d8629ab68dab753386d116a233](https://github.com/dOpensource/dsiprouter/commit/9e637b7c2da277d8629ab68dab753386d116a233)  \n> Date: Tue, 6 Nov 2018 13:07:59 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9e637b7c2da277d8629ab68dab753386d116a233)\n[//]: # (START_SECTION 60841ddeb6f74b3691a9969c5f20d28058675287)\n### Update carrier_groups.rst\n\n> Commit: [60841ddeb6f74b3691a9969c5f20d28058675287](https://github.com/dOpensource/dsiprouter/commit/60841ddeb6f74b3691a9969c5f20d28058675287)  \n> Date: Tue, 6 Nov 2018 12:51:20 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 60841ddeb6f74b3691a9969c5f20d28058675287)\n[//]: # (START_SECTION 0c9fb3ef6d12bf27f6c034c929a30e8213caf829)\n### Update carrier_groups.rst\n\n> Commit: [0c9fb3ef6d12bf27f6c034c929a30e8213caf829](https://github.com/dOpensource/dsiprouter/commit/0c9fb3ef6d12bf27f6c034c929a30e8213caf829)  \n> Date: Tue, 6 Nov 2018 12:50:43 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0c9fb3ef6d12bf27f6c034c929a30e8213caf829)\n[//]: # (START_SECTION 1c0045b2078d2dba2a87185d3665e8666c5b12b5)\n### Update carrier_groups.rst\n\n> Commit: [1c0045b2078d2dba2a87185d3665e8666c5b12b5](https://github.com/dOpensource/dsiprouter/commit/1c0045b2078d2dba2a87185d3665e8666c5b12b5)  \n> Date: Tue, 6 Nov 2018 12:49:04 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1c0045b2078d2dba2a87185d3665e8666c5b12b5)\n[//]: # (START_SECTION 62a95efd8e94072d5fd7a18448e1973407d144a4)\n### Update carrier_groups.rst\n\n> Commit: [62a95efd8e94072d5fd7a18448e1973407d144a4](https://github.com/dOpensource/dsiprouter/commit/62a95efd8e94072d5fd7a18448e1973407d144a4)  \n> Date: Tue, 6 Nov 2018 12:42:07 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 62a95efd8e94072d5fd7a18448e1973407d144a4)\n[//]: # (START_SECTION 95a067a6f1cbf22c965fe6f015f1946d8247de53)\n### Update carrier_groups.rst\n\n> Commit: [95a067a6f1cbf22c965fe6f015f1946d8247de53](https://github.com/dOpensource/dsiprouter/commit/95a067a6f1cbf22c965fe6f015f1946d8247de53)  \n> Date: Tue, 6 Nov 2018 12:38:07 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 95a067a6f1cbf22c965fe6f015f1946d8247de53)\n[//]: # (START_SECTION 7c533fa92e67bf759c63705f3a2ba38bf17cade2)\n### Update carrier_groups.rst\n\n> Commit: [7c533fa92e67bf759c63705f3a2ba38bf17cade2](https://github.com/dOpensource/dsiprouter/commit/7c533fa92e67bf759c63705f3a2ba38bf17cade2)  \n> Date: Tue, 6 Nov 2018 12:35:21 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7c533fa92e67bf759c63705f3a2ba38bf17cade2)\n[//]: # (START_SECTION f60054085f8d8395cbf020a86f5aef13d172b696)\n### Update carrier_groups.rst\n\n> Commit: [f60054085f8d8395cbf020a86f5aef13d172b696](https://github.com/dOpensource/dsiprouter/commit/f60054085f8d8395cbf020a86f5aef13d172b696)  \n> Date: Tue, 6 Nov 2018 12:19:42 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f60054085f8d8395cbf020a86f5aef13d172b696)\n[//]: # (START_SECTION 067e979aff32592a34afc0daba1d4188ea9046c4)\n### Fixed a number of GUI related issues and fixed issues with sort and search\n\n> Commit: [067e979aff32592a34afc0daba1d4188ea9046c4](https://github.com/dOpensource/dsiprouter/commit/067e979aff32592a34afc0daba1d4188ea9046c4)  \n> Date: Tue, 6 Nov 2018 11:58:54 +0000  \n> Author: root (mack@dopensource.com)  \n> Committer: root (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 067e979aff32592a34afc0daba1d4188ea9046c4)\n[//]: # (START_SECTION 55020672413646463728c5abf4716302161896f9)\n### Update configuring.rst\n\n> Commit: [55020672413646463728c5abf4716302161896f9](https://github.com/dOpensource/dsiprouter/commit/55020672413646463728c5abf4716302161896f9)  \n> Date: Tue, 6 Nov 2018 06:45:33 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 55020672413646463728c5abf4716302161896f9)\n[//]: # (START_SECTION f282dfac1f383786c9e60c4c43974a0dba9c2a5c)\n### Update carrier_groups.rst\n\n> Commit: [f282dfac1f383786c9e60c4c43974a0dba9c2a5c](https://github.com/dOpensource/dsiprouter/commit/f282dfac1f383786c9e60c4c43974a0dba9c2a5c)  \n> Date: Tue, 6 Nov 2018 06:43:23 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f282dfac1f383786c9e60c4c43974a0dba9c2a5c)\n[//]: # (START_SECTION 09c22d175e78ea1bb8c08e55fd6b5c23cf798bd7)\n### Update carrier_groups.rst\n\n> Commit: [09c22d175e78ea1bb8c08e55fd6b5c23cf798bd7](https://github.com/dOpensource/dsiprouter/commit/09c22d175e78ea1bb8c08e55fd6b5c23cf798bd7)  \n> Date: Tue, 6 Nov 2018 06:42:56 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 09c22d175e78ea1bb8c08e55fd6b5c23cf798bd7)\n[//]: # (START_SECTION ce2273f8e4556060f2a349a657ab6e78874b45b9)\n### Add files via upload\n\n> Commit: [ce2273f8e4556060f2a349a657ab6e78874b45b9](https://github.com/dOpensource/dsiprouter/commit/ce2273f8e4556060f2a349a657ab6e78874b45b9)  \n> Date: Tue, 6 Nov 2018 06:42:16 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ce2273f8e4556060f2a349a657ab6e78874b45b9)\n[//]: # (START_SECTION ba3328fc3e2c5d0fe0aca3ead4d7abe4bcae0520)\n### Update carrier_groups.rst\n\n> Commit: [ba3328fc3e2c5d0fe0aca3ead4d7abe4bcae0520](https://github.com/dOpensource/dsiprouter/commit/ba3328fc3e2c5d0fe0aca3ead4d7abe4bcae0520)  \n> Date: Tue, 6 Nov 2018 06:34:47 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ba3328fc3e2c5d0fe0aca3ead4d7abe4bcae0520)\n[//]: # (START_SECTION c7ea4c6bfec6a648a34525726f804cdf018d8cb0)\n### Create pbxs_and_endpoints.rst\n\n> Commit: [c7ea4c6bfec6a648a34525726f804cdf018d8cb0](https://github.com/dOpensource/dsiprouter/commit/c7ea4c6bfec6a648a34525726f804cdf018d8cb0)  \n> Date: Tue, 6 Nov 2018 06:32:36 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c7ea4c6bfec6a648a34525726f804cdf018d8cb0)\n[//]: # (START_SECTION 6a21a1dd7725ba3f129d20028e5cd79453eb7f6f)\n### Update configuring.rst\n\n> Commit: [6a21a1dd7725ba3f129d20028e5cd79453eb7f6f](https://github.com/dOpensource/dsiprouter/commit/6a21a1dd7725ba3f129d20028e5cd79453eb7f6f)  \n> Date: Tue, 6 Nov 2018 06:29:25 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6a21a1dd7725ba3f129d20028e5cd79453eb7f6f)\n[//]: # (START_SECTION 0e82f5cf587a8ca2fac5229af6e1da04fbb3c458)\n### Update configuring.rst\n\n> Commit: [0e82f5cf587a8ca2fac5229af6e1da04fbb3c458](https://github.com/dOpensource/dsiprouter/commit/0e82f5cf587a8ca2fac5229af6e1da04fbb3c458)  \n> Date: Tue, 6 Nov 2018 06:27:25 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0e82f5cf587a8ca2fac5229af6e1da04fbb3c458)\n[//]: # (START_SECTION 99c76f0bd6098aa3172bdabc7ac0b4210c683d39)\n### Create configuring.rst\n\n> Commit: [99c76f0bd6098aa3172bdabc7ac0b4210c683d39](https://github.com/dOpensource/dsiprouter/commit/99c76f0bd6098aa3172bdabc7ac0b4210c683d39)  \n> Date: Tue, 6 Nov 2018 06:24:48 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 99c76f0bd6098aa3172bdabc7ac0b4210c683d39)\n[//]: # (START_SECTION 82b7b1a39a16fdfdf891096aac36bd6ea7a22849)\n### Rename configuring.rst to carrier_groups.rst\n\n> Commit: [82b7b1a39a16fdfdf891096aac36bd6ea7a22849](https://github.com/dOpensource/dsiprouter/commit/82b7b1a39a16fdfdf891096aac36bd6ea7a22849)  \n> Date: Tue, 6 Nov 2018 06:24:06 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 82b7b1a39a16fdfdf891096aac36bd6ea7a22849)\n[//]: # (START_SECTION d68e56df12b70ecf354177549c837deff39a10e4)\n### Update configuring.rst\n\n> Commit: [d68e56df12b70ecf354177549c837deff39a10e4](https://github.com/dOpensource/dsiprouter/commit/d68e56df12b70ecf354177549c837deff39a10e4)  \n> Date: Tue, 6 Nov 2018 06:20:11 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d68e56df12b70ecf354177549c837deff39a10e4)\n[//]: # (START_SECTION 61a9dccb2b41ea89350d8c9a7e361952207e5d47)\n### Add files via upload\n\n> Commit: [61a9dccb2b41ea89350d8c9a7e361952207e5d47](https://github.com/dOpensource/dsiprouter/commit/61a9dccb2b41ea89350d8c9a7e361952207e5d47)  \n> Date: Tue, 6 Nov 2018 06:17:50 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 61a9dccb2b41ea89350d8c9a7e361952207e5d47)\n[//]: # (START_SECTION cdd0d8a4c465e0abea94f38d4c2cf78326a3845f)\n### Create configuring.rst\n\n> Commit: [cdd0d8a4c465e0abea94f38d4c2cf78326a3845f](https://github.com/dOpensource/dsiprouter/commit/cdd0d8a4c465e0abea94f38d4c2cf78326a3845f)  \n> Date: Tue, 6 Nov 2018 06:12:18 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cdd0d8a4c465e0abea94f38d4c2cf78326a3845f)\n[//]: # (START_SECTION 05dfa018657d794d8678403fac08f7ac841eba8a)\n### Update installing.rst\n\n> Commit: [05dfa018657d794d8678403fac08f7ac841eba8a](https://github.com/dOpensource/dsiprouter/commit/05dfa018657d794d8678403fac08f7ac841eba8a)  \n> Date: Tue, 6 Nov 2018 06:01:51 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 05dfa018657d794d8678403fac08f7ac841eba8a)\n[//]: # (START_SECTION 852d06bbf8e641b8090d645d5744335f51ae8e13)\n### Update installing.rst\n\n> Commit: [852d06bbf8e641b8090d645d5744335f51ae8e13](https://github.com/dOpensource/dsiprouter/commit/852d06bbf8e641b8090d645d5744335f51ae8e13)  \n> Date: Tue, 6 Nov 2018 06:00:54 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 852d06bbf8e641b8090d645d5744335f51ae8e13)\n[//]: # (START_SECTION d3cf85d9f3ed1f2d51c557097800b01735e058f0)\n### Update installing.rst\n\n> Commit: [d3cf85d9f3ed1f2d51c557097800b01735e058f0](https://github.com/dOpensource/dsiprouter/commit/d3cf85d9f3ed1f2d51c557097800b01735e058f0)  \n> Date: Tue, 6 Nov 2018 05:59:05 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d3cf85d9f3ed1f2d51c557097800b01735e058f0)\n[//]: # (START_SECTION bf9af20b132e0e6c51db41fec889aa501601d9a9)\n### Update installing.rst\n\n> Commit: [bf9af20b132e0e6c51db41fec889aa501601d9a9](https://github.com/dOpensource/dsiprouter/commit/bf9af20b132e0e6c51db41fec889aa501601d9a9)  \n> Date: Tue, 6 Nov 2018 05:57:19 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bf9af20b132e0e6c51db41fec889aa501601d9a9)\n[//]: # (START_SECTION 3388a9d66fa6f1f3a6f6528c4b862c82c53c7f5d)\n### Update installing.rst\n\n> Commit: [3388a9d66fa6f1f3a6f6528c4b862c82c53c7f5d](https://github.com/dOpensource/dsiprouter/commit/3388a9d66fa6f1f3a6f6528c4b862c82c53c7f5d)  \n> Date: Tue, 6 Nov 2018 05:54:18 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3388a9d66fa6f1f3a6f6528c4b862c82c53c7f5d)\n[//]: # (START_SECTION 1b4f27e33f6f20e60ee1604935509db03caa03e3)\n### Update index.rst\n\n> Commit: [1b4f27e33f6f20e60ee1604935509db03caa03e3](https://github.com/dOpensource/dsiprouter/commit/1b4f27e33f6f20e60ee1604935509db03caa03e3)  \n> Date: Tue, 6 Nov 2018 05:52:57 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1b4f27e33f6f20e60ee1604935509db03caa03e3)\n[//]: # (START_SECTION 42592fea9dbd6d08eecfb82d5b88954ddfe4e31d)\n### Update installing.rst\n\n> Commit: [42592fea9dbd6d08eecfb82d5b88954ddfe4e31d](https://github.com/dOpensource/dsiprouter/commit/42592fea9dbd6d08eecfb82d5b88954ddfe4e31d)  \n> Date: Tue, 6 Nov 2018 05:46:33 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 42592fea9dbd6d08eecfb82d5b88954ddfe4e31d)\n[//]: # (START_SECTION 66c07ef48148c9a3ac21b1c34b6cee146dce63f4)\n### Update installing.rst\n\n> Commit: [66c07ef48148c9a3ac21b1c34b6cee146dce63f4](https://github.com/dOpensource/dsiprouter/commit/66c07ef48148c9a3ac21b1c34b6cee146dce63f4)  \n> Date: Tue, 6 Nov 2018 05:43:39 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 66c07ef48148c9a3ac21b1c34b6cee146dce63f4)\n[//]: # (START_SECTION 91b954a9ea9cb06507ac7eead1ff24349000bc1d)\n### Update installing.rst\n\n> Commit: [91b954a9ea9cb06507ac7eead1ff24349000bc1d](https://github.com/dOpensource/dsiprouter/commit/91b954a9ea9cb06507ac7eead1ff24349000bc1d)  \n> Date: Tue, 6 Nov 2018 05:37:44 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 91b954a9ea9cb06507ac7eead1ff24349000bc1d)\n[//]: # (START_SECTION 210b71f1b902564a4a6d869d9610a580b916c41e)\n### Update installing.rst\n\n> Commit: [210b71f1b902564a4a6d869d9610a580b916c41e](https://github.com/dOpensource/dsiprouter/commit/210b71f1b902564a4a6d869d9610a580b916c41e)  \n> Date: Tue, 6 Nov 2018 05:36:12 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 210b71f1b902564a4a6d869d9610a580b916c41e)\n[//]: # (START_SECTION 6e1a4b67aea0c2422a412a481bc999aa29352103)\n### Update index.rst\n\n> Commit: [6e1a4b67aea0c2422a412a481bc999aa29352103](https://github.com/dOpensource/dsiprouter/commit/6e1a4b67aea0c2422a412a481bc999aa29352103)  \n> Date: Tue, 6 Nov 2018 05:33:26 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6e1a4b67aea0c2422a412a481bc999aa29352103)\n[//]: # (START_SECTION 225f723393d2840c8de81236bc248ffb9d0bf2ef)\n### Update installing.rst\n\n> Commit: [225f723393d2840c8de81236bc248ffb9d0bf2ef](https://github.com/dOpensource/dsiprouter/commit/225f723393d2840c8de81236bc248ffb9d0bf2ef)  \n> Date: Tue, 6 Nov 2018 05:30:21 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 225f723393d2840c8de81236bc248ffb9d0bf2ef)\n[//]: # (START_SECTION c53130200a6ef06497df2296aad1e6c9ed27f292)\n### Update installing.rst\n\n> Commit: [c53130200a6ef06497df2296aad1e6c9ed27f292](https://github.com/dOpensource/dsiprouter/commit/c53130200a6ef06497df2296aad1e6c9ed27f292)  \n> Date: Tue, 6 Nov 2018 05:26:09 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c53130200a6ef06497df2296aad1e6c9ed27f292)\n[//]: # (START_SECTION 5050ffda7bc5977df6e071fe881a163387002a74)\n### Update installing.rst\n\n> Commit: [5050ffda7bc5977df6e071fe881a163387002a74](https://github.com/dOpensource/dsiprouter/commit/5050ffda7bc5977df6e071fe881a163387002a74)  \n> Date: Tue, 6 Nov 2018 05:16:24 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5050ffda7bc5977df6e071fe881a163387002a74)\n[//]: # (START_SECTION 47dcb3be33f32f85f442300fd434f2cb2b19aa88)\n### Update index.rst\n\n> Commit: [47dcb3be33f32f85f442300fd434f2cb2b19aa88](https://github.com/dOpensource/dsiprouter/commit/47dcb3be33f32f85f442300fd434f2cb2b19aa88)  \n> Date: Tue, 6 Nov 2018 05:16:01 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 47dcb3be33f32f85f442300fd434f2cb2b19aa88)\n[//]: # (START_SECTION b90b65e99ad68133c7b426eeb0972e77b55401d5)\n### Create installing.rst\n\n> Commit: [b90b65e99ad68133c7b426eeb0972e77b55401d5](https://github.com/dOpensource/dsiprouter/commit/b90b65e99ad68133c7b426eeb0972e77b55401d5)  \n> Date: Tue, 6 Nov 2018 05:15:20 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b90b65e99ad68133c7b426eeb0972e77b55401d5)\n[//]: # (START_SECTION a306484ce89e49fc17020e70f1e5485d3d77f07a)\n### Update index.rst\n\n> Commit: [a306484ce89e49fc17020e70f1e5485d3d77f07a](https://github.com/dOpensource/dsiprouter/commit/a306484ce89e49fc17020e70f1e5485d3d77f07a)  \n> Date: Mon, 5 Nov 2018 15:09:34 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a306484ce89e49fc17020e70f1e5485d3d77f07a)\n[//]: # (START_SECTION a4cd1c7a7b945a9b347e7e7b09518703ad5170cf)\n### Update index.rst\n\n> Commit: [a4cd1c7a7b945a9b347e7e7b09518703ad5170cf](https://github.com/dOpensource/dsiprouter/commit/a4cd1c7a7b945a9b347e7e7b09518703ad5170cf)  \n> Date: Mon, 5 Nov 2018 15:07:35 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a4cd1c7a7b945a9b347e7e7b09518703ad5170cf)\n[//]: # (START_SECTION ae2bd8ea022973b1d3cf74bffc0e420435aa0084)\n### Update index.rst\n\n> Commit: [ae2bd8ea022973b1d3cf74bffc0e420435aa0084](https://github.com/dOpensource/dsiprouter/commit/ae2bd8ea022973b1d3cf74bffc0e420435aa0084)  \n> Date: Mon, 5 Nov 2018 14:54:25 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ae2bd8ea022973b1d3cf74bffc0e420435aa0084)\n[//]: # (START_SECTION 65d9b412c8e628d5a59d6227f865e171d4d6af02)\n### Update index.rst\n\n> Commit: [65d9b412c8e628d5a59d6227f865e171d4d6af02](https://github.com/dOpensource/dsiprouter/commit/65d9b412c8e628d5a59d6227f865e171d4d6af02)  \n> Date: Mon, 5 Nov 2018 14:49:08 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 65d9b412c8e628d5a59d6227f865e171d4d6af02)\n[//]: # (START_SECTION a17c6206d8f41cfe18d7fdf53c9394f46a2d7549)\n### Update index.rst\n\n> Commit: [a17c6206d8f41cfe18d7fdf53c9394f46a2d7549](https://github.com/dOpensource/dsiprouter/commit/a17c6206d8f41cfe18d7fdf53c9394f46a2d7549)  \n> Date: Mon, 5 Nov 2018 14:41:10 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a17c6206d8f41cfe18d7fdf53c9394f46a2d7549)\n[//]: # (START_SECTION d9136035e52196f9a9443248085a7aaa2ea07177)\n### Update index.rst\n\n> Commit: [d9136035e52196f9a9443248085a7aaa2ea07177](https://github.com/dOpensource/dsiprouter/commit/d9136035e52196f9a9443248085a7aaa2ea07177)  \n> Date: Mon, 5 Nov 2018 14:39:42 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d9136035e52196f9a9443248085a7aaa2ea07177)\n[//]: # (START_SECTION a1aa7b630ccb45487d329623e182c7bcc41a8149)\n### Update index.rst\n\n> Commit: [a1aa7b630ccb45487d329623e182c7bcc41a8149](https://github.com/dOpensource/dsiprouter/commit/a1aa7b630ccb45487d329623e182c7bcc41a8149)  \n> Date: Mon, 5 Nov 2018 14:38:31 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a1aa7b630ccb45487d329623e182c7bcc41a8149)\n[//]: # (START_SECTION cd97478cba66bed7cdb99c7788b29f492acddef3)\n### Update index.rst\n\n> Commit: [cd97478cba66bed7cdb99c7788b29f492acddef3](https://github.com/dOpensource/dsiprouter/commit/cd97478cba66bed7cdb99c7788b29f492acddef3)  \n> Date: Mon, 5 Nov 2018 14:36:31 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cd97478cba66bed7cdb99c7788b29f492acddef3)\n[//]: # (START_SECTION 89aba6b54851b992020db17c3ab237a5d46ee27d)\n### Update index.rst\n\n> Commit: [89aba6b54851b992020db17c3ab237a5d46ee27d](https://github.com/dOpensource/dsiprouter/commit/89aba6b54851b992020db17c3ab237a5d46ee27d)  \n> Date: Mon, 5 Nov 2018 14:28:04 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 89aba6b54851b992020db17c3ab237a5d46ee27d)\n[//]: # (START_SECTION 656429533dd95fab33de88f043d0cc6480a4883d)\n### Update index.rst\n\n> Commit: [656429533dd95fab33de88f043d0cc6480a4883d](https://github.com/dOpensource/dsiprouter/commit/656429533dd95fab33de88f043d0cc6480a4883d)  \n> Date: Mon, 5 Nov 2018 14:26:00 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 656429533dd95fab33de88f043d0cc6480a4883d)\n[//]: # (START_SECTION eb68428a3b6993fda4f8570e8e5c1866ab7b6df7)\n### Update index.rst\n\n> Commit: [eb68428a3b6993fda4f8570e8e5c1866ab7b6df7](https://github.com/dOpensource/dsiprouter/commit/eb68428a3b6993fda4f8570e8e5c1866ab7b6df7)  \n> Date: Mon, 5 Nov 2018 14:23:52 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eb68428a3b6993fda4f8570e8e5c1866ab7b6df7)\n[//]: # (START_SECTION 050707569fb1e92d9d90913feca2c5926791fe9f)\n### Update index.rst\n\n> Commit: [050707569fb1e92d9d90913feca2c5926791fe9f](https://github.com/dOpensource/dsiprouter/commit/050707569fb1e92d9d90913feca2c5926791fe9f)  \n> Date: Mon, 5 Nov 2018 14:20:36 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 050707569fb1e92d9d90913feca2c5926791fe9f)\n[//]: # (START_SECTION 4280b0277339b2dfc07456f7e8eb95df01825223)\n### Update index.rst\n\n> Commit: [4280b0277339b2dfc07456f7e8eb95df01825223](https://github.com/dOpensource/dsiprouter/commit/4280b0277339b2dfc07456f7e8eb95df01825223)  \n> Date: Mon, 5 Nov 2018 14:19:02 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4280b0277339b2dfc07456f7e8eb95df01825223)\n[//]: # (START_SECTION 3f8df7b2e0f6f44d385e36c9efa955cf1d313a5b)\n### Update index.rst\n\n> Commit: [3f8df7b2e0f6f44d385e36c9efa955cf1d313a5b](https://github.com/dOpensource/dsiprouter/commit/3f8df7b2e0f6f44d385e36c9efa955cf1d313a5b)  \n> Date: Mon, 5 Nov 2018 14:14:48 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3f8df7b2e0f6f44d385e36c9efa955cf1d313a5b)\n[//]: # (START_SECTION eb2a30656434bd2f8c8689980e595c0662215a14)\n### Update index.rst\n\n> Commit: [eb2a30656434bd2f8c8689980e595c0662215a14](https://github.com/dOpensource/dsiprouter/commit/eb2a30656434bd2f8c8689980e595c0662215a14)  \n> Date: Mon, 5 Nov 2018 13:58:16 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eb2a30656434bd2f8c8689980e595c0662215a14)\n[//]: # (START_SECTION bec7289e3e52786ebd365ab8dcce90c37535df78)\n### Update index.rst\n\n> Commit: [bec7289e3e52786ebd365ab8dcce90c37535df78](https://github.com/dOpensource/dsiprouter/commit/bec7289e3e52786ebd365ab8dcce90c37535df78)  \n> Date: Mon, 5 Nov 2018 13:53:52 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bec7289e3e52786ebd365ab8dcce90c37535df78)\n[//]: # (START_SECTION 5f8de2d823a05755459bc1ae151f2be58439e757)\n### Update index.rst\n\n> Commit: [5f8de2d823a05755459bc1ae151f2be58439e757](https://github.com/dOpensource/dsiprouter/commit/5f8de2d823a05755459bc1ae151f2be58439e757)  \n> Date: Mon, 5 Nov 2018 13:51:01 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5f8de2d823a05755459bc1ae151f2be58439e757)\n[//]: # (START_SECTION 52d2339f02db3402a12198733055efc0b833cc09)\n### Update index.rst\n\n> Commit: [52d2339f02db3402a12198733055efc0b833cc09](https://github.com/dOpensource/dsiprouter/commit/52d2339f02db3402a12198733055efc0b833cc09)  \n> Date: Mon, 5 Nov 2018 13:44:45 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 52d2339f02db3402a12198733055efc0b833cc09)\n[//]: # (START_SECTION d3e1601c9e5d02c76f65997773a66f69e4c18ac6)\n### Update index.rst\n\n> Commit: [d3e1601c9e5d02c76f65997773a66f69e4c18ac6](https://github.com/dOpensource/dsiprouter/commit/d3e1601c9e5d02c76f65997773a66f69e4c18ac6)  \n> Date: Mon, 5 Nov 2018 13:33:58 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d3e1601c9e5d02c76f65997773a66f69e4c18ac6)\n[//]: # (START_SECTION 0367725739cf98cdf7fd954656b5f0e5f7dc498b)\n### Update index.rst\n\n> Commit: [0367725739cf98cdf7fd954656b5f0e5f7dc498b](https://github.com/dOpensource/dsiprouter/commit/0367725739cf98cdf7fd954656b5f0e5f7dc498b)  \n> Date: Mon, 5 Nov 2018 13:27:06 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0367725739cf98cdf7fd954656b5f0e5f7dc498b)\n[//]: # (START_SECTION d5555b5a88904146f037b1777b1ccbc3fd585976)\n### Update index.rst\n\n> Commit: [d5555b5a88904146f037b1777b1ccbc3fd585976](https://github.com/dOpensource/dsiprouter/commit/d5555b5a88904146f037b1777b1ccbc3fd585976)  \n> Date: Mon, 5 Nov 2018 12:45:23 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d5555b5a88904146f037b1777b1ccbc3fd585976)\n[//]: # (START_SECTION df6c9add73dde8126c0ac98cf21ff569c5a602e0)\n### Update index.rst\n\n> Commit: [df6c9add73dde8126c0ac98cf21ff569c5a602e0](https://github.com/dOpensource/dsiprouter/commit/df6c9add73dde8126c0ac98cf21ff569c5a602e0)  \n> Date: Mon, 5 Nov 2018 12:44:05 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION df6c9add73dde8126c0ac98cf21ff569c5a602e0)\n[//]: # (START_SECTION 7f9fb2325771005478fbf8c10f487149ac28e895)\n### Update index.rst\n\n> Commit: [7f9fb2325771005478fbf8c10f487149ac28e895](https://github.com/dOpensource/dsiprouter/commit/7f9fb2325771005478fbf8c10f487149ac28e895)  \n> Date: Mon, 5 Nov 2018 12:41:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7f9fb2325771005478fbf8c10f487149ac28e895)\n[//]: # (START_SECTION ac93fb29e458a49f2265b0a146165515df4f971a)\n### Update index.rst\n\n> Commit: [ac93fb29e458a49f2265b0a146165515df4f971a](https://github.com/dOpensource/dsiprouter/commit/ac93fb29e458a49f2265b0a146165515df4f971a)  \n> Date: Mon, 5 Nov 2018 12:38:42 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ac93fb29e458a49f2265b0a146165515df4f971a)\n[//]: # (START_SECTION ae06eeb651ac5ee3f36554102feb02d605726da5)\n### Update index.rst\n\n> Commit: [ae06eeb651ac5ee3f36554102feb02d605726da5](https://github.com/dOpensource/dsiprouter/commit/ae06eeb651ac5ee3f36554102feb02d605726da5)  \n> Date: Mon, 5 Nov 2018 12:35:32 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ae06eeb651ac5ee3f36554102feb02d605726da5)\n[//]: # (START_SECTION 7cfe90077c726788d79f663143bf1c7dc55d6cb7)\n### Update index.rst\n\n> Commit: [7cfe90077c726788d79f663143bf1c7dc55d6cb7](https://github.com/dOpensource/dsiprouter/commit/7cfe90077c726788d79f663143bf1c7dc55d6cb7)  \n> Date: Mon, 5 Nov 2018 12:34:57 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7cfe90077c726788d79f663143bf1c7dc55d6cb7)\n[//]: # (START_SECTION d614b77f9c7b1f712cd2e599688dee294a9bee55)\n### Update index.rst\n\n> Commit: [d614b77f9c7b1f712cd2e599688dee294a9bee55](https://github.com/dOpensource/dsiprouter/commit/d614b77f9c7b1f712cd2e599688dee294a9bee55)  \n> Date: Mon, 5 Nov 2018 12:32:51 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d614b77f9c7b1f712cd2e599688dee294a9bee55)\n[//]: # (START_SECTION 61074d151d4dd157290d7e7b0210570807a499be)\n### Update index.rst\n\n> Commit: [61074d151d4dd157290d7e7b0210570807a499be](https://github.com/dOpensource/dsiprouter/commit/61074d151d4dd157290d7e7b0210570807a499be)  \n> Date: Mon, 5 Nov 2018 12:30:00 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 61074d151d4dd157290d7e7b0210570807a499be)\n[//]: # (START_SECTION 27c00857bda1ec3dcd1d73d52fed8156be102825)\n### Update index.rst\n\n> Commit: [27c00857bda1ec3dcd1d73d52fed8156be102825](https://github.com/dOpensource/dsiprouter/commit/27c00857bda1ec3dcd1d73d52fed8156be102825)  \n> Date: Mon, 5 Nov 2018 12:13:41 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 27c00857bda1ec3dcd1d73d52fed8156be102825)\n[//]: # (START_SECTION fbf822d73f3a09e9588cf05c1c79e1deb8b03f3e)\n### Update index.rst\n\n> Commit: [fbf822d73f3a09e9588cf05c1c79e1deb8b03f3e](https://github.com/dOpensource/dsiprouter/commit/fbf822d73f3a09e9588cf05c1c79e1deb8b03f3e)  \n> Date: Mon, 5 Nov 2018 12:09:45 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fbf822d73f3a09e9588cf05c1c79e1deb8b03f3e)\n[//]: # (START_SECTION 31f9a5d9a334c0af187ddb8fedf3d6613b029351)\n### Update index.rst\n\n> Commit: [31f9a5d9a334c0af187ddb8fedf3d6613b029351](https://github.com/dOpensource/dsiprouter/commit/31f9a5d9a334c0af187ddb8fedf3d6613b029351)  \n> Date: Mon, 5 Nov 2018 12:08:05 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 31f9a5d9a334c0af187ddb8fedf3d6613b029351)\n[//]: # (START_SECTION 8d387c5bc356376b84696ec825e914a8e1eba605)\n### Update index.rst\n\n> Commit: [8d387c5bc356376b84696ec825e914a8e1eba605](https://github.com/dOpensource/dsiprouter/commit/8d387c5bc356376b84696ec825e914a8e1eba605)  \n> Date: Mon, 5 Nov 2018 12:05:52 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8d387c5bc356376b84696ec825e914a8e1eba605)\n[//]: # (START_SECTION d06c51b6a55177e102e796aca34750d9f042ffea)\n### Update README.md\n\n> Commit: [d06c51b6a55177e102e796aca34750d9f042ffea](https://github.com/dOpensource/dsiprouter/commit/d06c51b6a55177e102e796aca34750d9f042ffea)  \n> Date: Mon, 5 Nov 2018 11:59:29 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d06c51b6a55177e102e796aca34750d9f042ffea)\n[//]: # (START_SECTION 69831efae2d540363e6685adce5599aeffd17e30)\n### Update index.rst\n\n> Commit: [69831efae2d540363e6685adce5599aeffd17e30](https://github.com/dOpensource/dsiprouter/commit/69831efae2d540363e6685adce5599aeffd17e30)  \n> Date: Mon, 5 Nov 2018 11:57:01 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 69831efae2d540363e6685adce5599aeffd17e30)\n[//]: # (START_SECTION a3d252949c0a867abdcdbc2fbeace5813875d50c)\n### Update index.rst\n\n> Commit: [a3d252949c0a867abdcdbc2fbeace5813875d50c](https://github.com/dOpensource/dsiprouter/commit/a3d252949c0a867abdcdbc2fbeace5813875d50c)  \n> Date: Mon, 5 Nov 2018 11:55:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a3d252949c0a867abdcdbc2fbeace5813875d50c)\n[//]: # (START_SECTION 2c7d51a7e874388f3b62f3000b57decc1d906703)\n### Update index.rst\n\n> Commit: [2c7d51a7e874388f3b62f3000b57decc1d906703](https://github.com/dOpensource/dsiprouter/commit/2c7d51a7e874388f3b62f3000b57decc1d906703)  \n> Date: Mon, 5 Nov 2018 11:54:48 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2c7d51a7e874388f3b62f3000b57decc1d906703)\n[//]: # (START_SECTION 08d10e0e628dda014c9fb1331e2b9700096ea2ca)\n### Update index.rst\n\n> Commit: [08d10e0e628dda014c9fb1331e2b9700096ea2ca](https://github.com/dOpensource/dsiprouter/commit/08d10e0e628dda014c9fb1331e2b9700096ea2ca)  \n> Date: Mon, 5 Nov 2018 11:51:58 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 08d10e0e628dda014c9fb1331e2b9700096ea2ca)\n[//]: # (START_SECTION 525b1f44557c90cbd98440092fb53321c3c0ff91)\n### Update index.rst\n\n> Commit: [525b1f44557c90cbd98440092fb53321c3c0ff91](https://github.com/dOpensource/dsiprouter/commit/525b1f44557c90cbd98440092fb53321c3c0ff91)  \n> Date: Mon, 5 Nov 2018 11:46:49 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 525b1f44557c90cbd98440092fb53321c3c0ff91)\n[//]: # (START_SECTION fdd79140e22e4802f865460df3a75a0ad50e66b3)\n### Update index.rst\n\n> Commit: [fdd79140e22e4802f865460df3a75a0ad50e66b3](https://github.com/dOpensource/dsiprouter/commit/fdd79140e22e4802f865460df3a75a0ad50e66b3)  \n> Date: Mon, 5 Nov 2018 11:33:19 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fdd79140e22e4802f865460df3a75a0ad50e66b3)\n[//]: # (START_SECTION 210a2ddc4420e829cf6a2d9c135724e05c1219c4)\n### Update index.rst\n\n> Commit: [210a2ddc4420e829cf6a2d9c135724e05c1219c4](https://github.com/dOpensource/dsiprouter/commit/210a2ddc4420e829cf6a2d9c135724e05c1219c4)  \n> Date: Mon, 5 Nov 2018 11:30:35 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 210a2ddc4420e829cf6a2d9c135724e05c1219c4)\n[//]: # (START_SECTION 25c9393d1160c74c3681b7588eb4f5391da2b806)\n### Update index.rst\n\n> Commit: [25c9393d1160c74c3681b7588eb4f5391da2b806](https://github.com/dOpensource/dsiprouter/commit/25c9393d1160c74c3681b7588eb4f5391da2b806)  \n> Date: Mon, 5 Nov 2018 11:27:45 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 25c9393d1160c74c3681b7588eb4f5391da2b806)\n[//]: # (START_SECTION 25fc7e99bb85bad0970c9d40d3c061c95f4fa04d)\n### Update index.rst\n\n> Commit: [25fc7e99bb85bad0970c9d40d3c061c95f4fa04d](https://github.com/dOpensource/dsiprouter/commit/25fc7e99bb85bad0970c9d40d3c061c95f4fa04d)  \n> Date: Mon, 5 Nov 2018 11:25:48 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 25fc7e99bb85bad0970c9d40d3c061c95f4fa04d)\n[//]: # (START_SECTION 32d177564f89e3267d6ee9f88b32128dc6f66435)\n### Update index.rst\n\n> Commit: [32d177564f89e3267d6ee9f88b32128dc6f66435](https://github.com/dOpensource/dsiprouter/commit/32d177564f89e3267d6ee9f88b32128dc6f66435)  \n> Date: Mon, 5 Nov 2018 11:23:05 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 32d177564f89e3267d6ee9f88b32128dc6f66435)\n[//]: # (START_SECTION 31dd5677af4d9640a92061f0fdf039adb647994d)\n### Update index.rst\n\n> Commit: [31dd5677af4d9640a92061f0fdf039adb647994d](https://github.com/dOpensource/dsiprouter/commit/31dd5677af4d9640a92061f0fdf039adb647994d)  \n> Date: Mon, 5 Nov 2018 10:38:56 -0500  \n> Author: ncannon01 (44709249+ncannon01@users.noreply.github.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 31dd5677af4d9640a92061f0fdf039adb647994d)\n[//]: # (START_SECTION ea93010271a1bef8480a93a87ea75e3e068957ea)\n### Update index.rst\n\n> Commit: [ea93010271a1bef8480a93a87ea75e3e068957ea](https://github.com/dOpensource/dsiprouter/commit/ea93010271a1bef8480a93a87ea75e3e068957ea)  \n> Date: Mon, 5 Nov 2018 09:33:09 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ea93010271a1bef8480a93a87ea75e3e068957ea)\n[//]: # (START_SECTION 332c0bb6b79b29fd56a283f6762e72e8133b755b)\n### Update index.rst\n\n> Commit: [332c0bb6b79b29fd56a283f6762e72e8133b755b](https://github.com/dOpensource/dsiprouter/commit/332c0bb6b79b29fd56a283f6762e72e8133b755b)  \n> Date: Mon, 5 Nov 2018 09:31:49 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 332c0bb6b79b29fd56a283f6762e72e8133b755b)\n[//]: # (START_SECTION e12ab1b48d452f1554142b63c5832e7be1058837)\n### Update index.rst\n\n> Commit: [e12ab1b48d452f1554142b63c5832e7be1058837](https://github.com/dOpensource/dsiprouter/commit/e12ab1b48d452f1554142b63c5832e7be1058837)  \n> Date: Mon, 5 Nov 2018 09:30:16 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e12ab1b48d452f1554142b63c5832e7be1058837)\n[//]: # (START_SECTION 3983711e773b9f6b41e4ed6df96f881c66a0586e)\n### Update index.rst\n\n> Commit: [3983711e773b9f6b41e4ed6df96f881c66a0586e](https://github.com/dOpensource/dsiprouter/commit/3983711e773b9f6b41e4ed6df96f881c66a0586e)  \n> Date: Mon, 5 Nov 2018 09:26:13 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3983711e773b9f6b41e4ed6df96f881c66a0586e)\n[//]: # (START_SECTION 2207add74d482c604cf70b68f7e61f72d235a96e)\n### Update index.rst\n\n> Commit: [2207add74d482c604cf70b68f7e61f72d235a96e](https://github.com/dOpensource/dsiprouter/commit/2207add74d482c604cf70b68f7e61f72d235a96e)  \n> Date: Mon, 5 Nov 2018 09:24:31 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2207add74d482c604cf70b68f7e61f72d235a96e)\n[//]: # (START_SECTION 860ab8ee1a68078c7f5d871fd27ac1348e3d47a6)\n### Update index.rst\n\n> Commit: [860ab8ee1a68078c7f5d871fd27ac1348e3d47a6](https://github.com/dOpensource/dsiprouter/commit/860ab8ee1a68078c7f5d871fd27ac1348e3d47a6)  \n> Date: Mon, 5 Nov 2018 09:24:17 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 860ab8ee1a68078c7f5d871fd27ac1348e3d47a6)\n[//]: # (START_SECTION a1b39356ab30428b96f3815adfc6b6592430dca0)\n### Update index.rst\n\n> Commit: [a1b39356ab30428b96f3815adfc6b6592430dca0](https://github.com/dOpensource/dsiprouter/commit/a1b39356ab30428b96f3815adfc6b6592430dca0)  \n> Date: Mon, 5 Nov 2018 09:20:39 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a1b39356ab30428b96f3815adfc6b6592430dca0)\n[//]: # (START_SECTION f50a31c8df66b3ac8cfb46c5fe8f291db60ebc87)\n### Update index.rst\n\n> Commit: [f50a31c8df66b3ac8cfb46c5fe8f291db60ebc87](https://github.com/dOpensource/dsiprouter/commit/f50a31c8df66b3ac8cfb46c5fe8f291db60ebc87)  \n> Date: Mon, 5 Nov 2018 09:16:01 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f50a31c8df66b3ac8cfb46c5fe8f291db60ebc87)\n[//]: # (START_SECTION 30d36d670bd96ace7dd33132387e01e73750021a)\n### Update index.rst\n\n> Commit: [30d36d670bd96ace7dd33132387e01e73750021a](https://github.com/dOpensource/dsiprouter/commit/30d36d670bd96ace7dd33132387e01e73750021a)  \n> Date: Fri, 2 Nov 2018 14:37:21 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 30d36d670bd96ace7dd33132387e01e73750021a)\n[//]: # (START_SECTION 84cafa37fe15f86334ad6c156aaff6b5fdbd196b)\n### Update index.rst\n\n> Commit: [84cafa37fe15f86334ad6c156aaff6b5fdbd196b](https://github.com/dOpensource/dsiprouter/commit/84cafa37fe15f86334ad6c156aaff6b5fdbd196b)  \n> Date: Fri, 2 Nov 2018 14:34:45 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 84cafa37fe15f86334ad6c156aaff6b5fdbd196b)\n[//]: # (START_SECTION 14d3c26782a92adf62dc26276f43e3b30839fce0)\n### Update index.rst\n\n> Commit: [14d3c26782a92adf62dc26276f43e3b30839fce0](https://github.com/dOpensource/dsiprouter/commit/14d3c26782a92adf62dc26276f43e3b30839fce0)  \n> Date: Fri, 2 Nov 2018 14:31:30 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 14d3c26782a92adf62dc26276f43e3b30839fce0)\n[//]: # (START_SECTION 2b9d89de6ee285b749028ffc6fd2544118ff484c)\n### Update index.rst\n\n> Commit: [2b9d89de6ee285b749028ffc6fd2544118ff484c](https://github.com/dOpensource/dsiprouter/commit/2b9d89de6ee285b749028ffc6fd2544118ff484c)  \n> Date: Fri, 2 Nov 2018 14:26:43 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2b9d89de6ee285b749028ffc6fd2544118ff484c)\n[//]: # (START_SECTION ce9f450ea71a35b9e08a1b936431d7750c8d0ea3)\n### Update index.rst\n\n> Commit: [ce9f450ea71a35b9e08a1b936431d7750c8d0ea3](https://github.com/dOpensource/dsiprouter/commit/ce9f450ea71a35b9e08a1b936431d7750c8d0ea3)  \n> Date: Fri, 2 Nov 2018 14:26:03 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ce9f450ea71a35b9e08a1b936431d7750c8d0ea3)\n[//]: # (START_SECTION 79823053f92679a78799d946af4f76021e3d4884)\n### Update index.rst\n\n> Commit: [79823053f92679a78799d946af4f76021e3d4884](https://github.com/dOpensource/dsiprouter/commit/79823053f92679a78799d946af4f76021e3d4884)  \n> Date: Fri, 2 Nov 2018 14:24:07 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 79823053f92679a78799d946af4f76021e3d4884)\n[//]: # (START_SECTION c02abd219f4f5eaf295fe0da9b552190c68b62c4)\n### Update index.rst\n\n> Commit: [c02abd219f4f5eaf295fe0da9b552190c68b62c4](https://github.com/dOpensource/dsiprouter/commit/c02abd219f4f5eaf295fe0da9b552190c68b62c4)  \n> Date: Fri, 2 Nov 2018 14:18:50 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c02abd219f4f5eaf295fe0da9b552190c68b62c4)\n[//]: # (START_SECTION 179cae55486547db2fa5efe8101a9820437adc41)\n### Create index.rst\n\n> Commit: [179cae55486547db2fa5efe8101a9820437adc41](https://github.com/dOpensource/dsiprouter/commit/179cae55486547db2fa5efe8101a9820437adc41)  \n> Date: Fri, 2 Nov 2018 13:29:48 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 179cae55486547db2fa5efe8101a9820437adc41)\n[//]: # (START_SECTION 92e804b0f004a91c0e47d88b954b8bc1d882998b)\n### Create index.rst\n\n> Commit: [92e804b0f004a91c0e47d88b954b8bc1d882998b](https://github.com/dOpensource/dsiprouter/commit/92e804b0f004a91c0e47d88b954b8bc1d882998b)  \n> Date: Fri, 2 Nov 2018 13:10:31 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 92e804b0f004a91c0e47d88b954b8bc1d882998b)\n[//]: # (START_SECTION 17680f44b08e8891659c7bc09bdfe637cf632127)\n### Added the notes field to the add and edit modal's for Inbound Mappings\n\n> Commit: [17680f44b08e8891659c7bc09bdfe637cf632127](https://github.com/dOpensource/dsiprouter/commit/17680f44b08e8891659c7bc09bdfe637cf632127)  \n> Date: Thu, 1 Nov 2018 11:55:07 +0000  \n> Author: root (mack@dopensource.com)  \n> Committer: root (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 17680f44b08e8891659c7bc09bdfe637cf632127)\n[//]: # (START_SECTION f96c161f8bab289573e1d4a6cf9dc0fbf3212276)\n### Added support for importing one of more DID's Issue #84\n\n> Commit: [f96c161f8bab289573e1d4a6cf9dc0fbf3212276](https://github.com/dOpensource/dsiprouter/commit/f96c161f8bab289573e1d4a6cf9dc0fbf3212276)  \n> Date: Thu, 1 Nov 2018 04:31:47 +0000  \n> Author: root (mack@dopensource.com)  \n> Committer: root (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f96c161f8bab289573e1d4a6cf9dc0fbf3212276)\n[//]: # (START_SECTION 25b110bef1346ae693673afaa2a04553faf99952)\n### Added support for sorting, searching and pagination to the domain page.  This sort can also be added to other pages as well since the library is now added Issue #84\n\n> Commit: [25b110bef1346ae693673afaa2a04553faf99952](https://github.com/dOpensource/dsiprouter/commit/25b110bef1346ae693673afaa2a04553faf99952)  \n> Date: Tue, 30 Oct 2018 04:07:50 +0000  \n> Author: root (mack@dopensource.com)  \n> Committer: root (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 25b110bef1346ae693673afaa2a04553faf99952)\n[//]: # (START_SECTION 244bda0fd487d67b3d5e42c2b1b6a9f8e53dae97)\n### Update CONTRIBUTING.md\n\n> Commit: [244bda0fd487d67b3d5e42c2b1b6a9f8e53dae97](https://github.com/dOpensource/dsiprouter/commit/244bda0fd487d67b3d5e42c2b1b6a9f8e53dae97)  \n> Date: Wed, 24 Oct 2018 16:00:59 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 244bda0fd487d67b3d5e42c2b1b6a9f8e53dae97)\n[//]: # (START_SECTION 194beeaf4cb72efefce02a69c6c48f5e9463419e)\n### Update CONTRIBUTING.md\n\n> Commit: [194beeaf4cb72efefce02a69c6c48f5e9463419e](https://github.com/dOpensource/dsiprouter/commit/194beeaf4cb72efefce02a69c6c48f5e9463419e)  \n> Date: Wed, 24 Oct 2018 15:59:39 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 194beeaf4cb72efefce02a69c6c48f5e9463419e)\n[//]: # (START_SECTION bbe0919f659d081ddaf07890b41a56854bd8660d)\n### Added Domain Management features and added a new approach to adding modules to dSIPRouter, which will be documented in the Contribution Guide.\n\n> Commit: [bbe0919f659d081ddaf07890b41a56854bd8660d](https://github.com/dOpensource/dsiprouter/commit/bbe0919f659d081ddaf07890b41a56854bd8660d)  \n> Date: Mon, 22 Oct 2018 09:26:48 +0000  \n> Author: root (mack@dopensource.com)  \n> Committer: root (mack@dopensource.com)  \n> Signed:   \n\n\n- - Static and Dynamic domains can be displayed in the GUI.\n- - Added a UnitTest to validate the Domain Management Services\n\n\n---\n\n[//]: # (END_SECTION bbe0919f659d081ddaf07890b41a56854bd8660d)\n[//]: # (START_SECTION 789683f2bedd0a502e76e19ee2f7dce42023dcae)\n### Merge asterisk-realtime and latest updates\n\n> Commit: [789683f2bedd0a502e76e19ee2f7dce42023dcae](https://github.com/dOpensource/dsiprouter/commit/789683f2bedd0a502e76e19ee2f7dce42023dcae)  \n> Date: Sun, 30 Sep 2018 20:14:59 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- allow cluster db installation config\n- auto update cluster params in kamconfig\n- condense kamconfig files (using ifdefs)\n- merge current SIPWISE kamconfig\n- merge asterisk-realtime branch\n- fix NAT issues\n- auto update kamversion in kamconfig\n- upgrade version / release functions\n- improve install script util functions\n- add import library to install script\n- section out user configurable settings\n- make install script vars easier to use\n- remove placeholder in docs\n- add TODO statements\n- add current asterisk-realtime resources\n- add possible rtpengine fix in resources\n- add module install echo statements\n- allow SSL keys on debug\n- update OS dependencies\n- Signed-off-by: Tyler Moore <tmoore@goflyball.com>\n\n\n---\n\n[//]: # (END_SECTION 789683f2bedd0a502e76e19ee2f7dce42023dcae)\n[//]: # (START_SECTION e39f4cb5aa9266b913b79089b6166a1c586a726d)\n### Create CONTRIBUTING.md\n\n> Commit: [e39f4cb5aa9266b913b79089b6166a1c586a726d](https://github.com/dOpensource/dsiprouter/commit/e39f4cb5aa9266b913b79089b6166a1c586a726d)  \n> Date: Sun, 30 Sep 2018 00:10:20 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n- initial guide\n\n\n---\n\n[//]: # (END_SECTION e39f4cb5aa9266b913b79089b6166a1c586a726d)\n[//]: # (START_SECTION 1fd189ee59969884f6d0872df1aa30d3f39ce5b2)\n### Added support for working with a Kamailio subscriber table and tested it against FreePBX\n\n> Commit: [1fd189ee59969884f6d0872df1aa30d3f39ce5b2](https://github.com/dOpensource/dsiprouter/commit/1fd189ee59969884f6d0872df1aa30d3f39ce5b2)  \n> Date: Wed, 26 Sep 2018 14:17:05 -0400  \n> Author: root (root@kamailio3.kamailo3@lhsip.com)  \n> Committer: root (root@kamailio3.kamailo3@lhsip.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1fd189ee59969884f6d0872df1aa30d3f39ce5b2)\n[//]: # (START_SECTION 43246b5426badd72a5b2ac690f2d78ca14271666)\n### Added support for enriching sip headers and added record_route support\n\n> Commit: [43246b5426badd72a5b2ac690f2d78ca14271666](https://github.com/dOpensource/dsiprouter/commit/43246b5426badd72a5b2ac690f2d78ca14271666)  \n> Date: Mon, 24 Sep 2018 12:46:52 +0200  \n> Author: root (root@reg-01.voipmuch.com)  \n> Committer: root (root@reg-01.voipmuch.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 43246b5426badd72a5b2ac690f2d78ca14271666)\n[//]: # (START_SECTION 4556d480be5c0c11d7c91152bbbbe08958b6f02d)\n### Using sippasswd field within Asterisk Realtime to validate user passwords\n\n> Commit: [4556d480be5c0c11d7c91152bbbbe08958b6f02d](https://github.com/dOpensource/dsiprouter/commit/4556d480be5c0c11d7c91152bbbbe08958b6f02d)  \n> Date: Mon, 24 Sep 2018 09:59:23 +0200  \n> Author: root (root@reg-01.voipmuch.com)  \n> Committer: root (root@reg-01.voipmuch.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4556d480be5c0c11d7c91152bbbbe08958b6f02d)\n[//]: # (START_SECTION 3027ebd0011ebac64e8d8665ae5078500c72e921)\n### weezy was specified instead of stretch\n\n> Commit: [3027ebd0011ebac64e8d8665ae5078500c72e921](https://github.com/dOpensource/dsiprouter/commit/3027ebd0011ebac64e8d8665ae5078500c72e921)  \n> Date: Sun, 23 Sep 2018 18:50:20 +0200  \n> Author: root (root@reg-01.voipmuch.com)  \n> Committer: root (root@reg-01.voipmuch.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3027ebd0011ebac64e8d8665ae5078500c72e921)\n[//]: # (START_SECTION 9e9755bde5835ae4d842741b6b4071b8e1c43799)\n### Initial commit for Asterisk Realtime Support\n\n> Commit: [9e9755bde5835ae4d842741b6b4071b8e1c43799](https://github.com/dOpensource/dsiprouter/commit/9e9755bde5835ae4d842741b6b4071b8e1c43799)  \n> Date: Sun, 23 Sep 2018 15:27:06 +0000  \n> Author: root (root@dsiprouter-dev.localdomain)  \n> Committer: root (root@dsiprouter-dev.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9e9755bde5835ae4d842741b6b4071b8e1c43799)\n[//]: # (START_SECTION 83936bb0cbfff957a846d55acac27b2a2c1d8cd9)\n### Add CentOS support v0.51\n\n> Commit: [83936bb0cbfff957a846d55acac27b2a2c1d8cd9](https://github.com/dOpensource/dsiprouter/commit/83936bb0cbfff957a846d55acac27b2a2c1d8cd9)  \n> Date: Mon, 10 Sep 2018 20:15:22 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Resolves #30\n- Resolves #69\n- merge changes with v5.1\n- remove python prompt\n- change validate to allow centos v7\n- add centos checks in install functions\n- fix distro / OS version checks\n- fix centos uninstall funcs\n- automate kamdbctl password prompt (edit config)\n- add module install support for centos\n- unset exported vars/funcs on exit (cleanup)\n- get rid of kamailio db prompt\n- allow debug in all commands (1st param only)\n- various fixes for install scripts\n- add util / library script\n- improve distro version checks\n- fixes for db error handling in dsiprouter.py\n- re-add serving https through ssl certs (settings.py)\n- improve file parsing in updateConfig()\n- fix default mysql csv's (broken from last commit)\n- enable cdrs by default\n- Signed-off-by: Tyler Moore <tmoore@goflyball.com>\n\n\n---\n\n[//]: # (END_SECTION 83936bb0cbfff957a846d55acac27b2a2c1d8cd9)\n[//]: # (START_SECTION 0cb7af60b073f8ecd78beafdb13cc540e90a2a6d)\n### Changed the default role in Kamailio to '' for all\n\n> Commit: [0cb7af60b073f8ecd78beafdb13cc540e90a2a6d](https://github.com/dOpensource/dsiprouter/commit/0cb7af60b073f8ecd78beafdb13cc540e90a2a6d)  \n> Date: Fri, 7 Sep 2018 01:03:43 -0500  \n> Author: root (root@969092-extapp1.inemsoft.com)  \n> Committer: root (root@969092-extapp1.inemsoft.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0cb7af60b073f8ecd78beafdb13cc540e90a2a6d)\n[//]: # (START_SECTION 1c9d98e912dcd04069c0fed2d7680153a97dc42b)\n### Raw fixes for centos 7 support\n\n> Commit: [1c9d98e912dcd04069c0fed2d7680153a97dc42b](https://github.com/dOpensource/dsiprouter/commit/1c9d98e912dcd04069c0fed2d7680153a97dc42b)  \n> Date: Fri, 7 Sep 2018 00:05:37 -0500  \n> Author: root (root@969092-extapp1.inemsoft.com)  \n> Committer: root (root@969092-extapp1.inemsoft.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1c9d98e912dcd04069c0fed2d7680153a97dc42b)\n[//]: # (START_SECTION d0b5c970a246391e175cd1762138a35d35dc74da)\n### Adding support for centos 7\n\n> Commit: [d0b5c970a246391e175cd1762138a35d35dc74da](https://github.com/dOpensource/dsiprouter/commit/d0b5c970a246391e175cd1762138a35d35dc74da)  \n> Date: Thu, 6 Sep 2018 17:54:35 -0500  \n> Author: root (root@969092-extapp1.inemsoft.com)  \n> Committer: root (root@969092-extapp1.inemsoft.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d0b5c970a246391e175cd1762138a35d35dc74da)\n[//]: # (START_SECTION f06d26c7ec49e4cb3e44c3f4ce2696562cea8612)\n### Added support for centos 7\n\n> Commit: [f06d26c7ec49e4cb3e44c3f4ce2696562cea8612](https://github.com/dOpensource/dsiprouter/commit/f06d26c7ec49e4cb3e44c3f4ce2696562cea8612)  \n> Date: Thu, 6 Sep 2018 17:20:57 -0500  \n> Author: root (root@969092-extapp1.inemsoft.com)  \n> Committer: root (root@969092-extapp1.inemsoft.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f06d26c7ec49e4cb3e44c3f4ce2696562cea8612)\n[//]: # (START_SECTION 0aea024cd5bb0fdfbfc9a757c7a95e53977d27d7)\n### Adding support back for centOS 7\n\n> Commit: [0aea024cd5bb0fdfbfc9a757c7a95e53977d27d7](https://github.com/dOpensource/dsiprouter/commit/0aea024cd5bb0fdfbfc9a757c7a95e53977d27d7)  \n> Date: Thu, 6 Sep 2018 17:03:58 -0500  \n> Author: root (root@969092-extapp1.inemsoft.com)  \n> Committer: root (root@969092-extapp1.inemsoft.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0aea024cd5bb0fdfbfc9a757c7a95e53977d27d7)\n[//]: # (START_SECTION 21818f07eadc647bf227fa3ba2ccad0fe81195f5)\n### Provided comments in settings.py and added support for giving dSIPRouter roles\n\n> Commit: [21818f07eadc647bf227fa3ba2ccad0fe81195f5](https://github.com/dOpensource/dsiprouter/commit/21818f07eadc647bf227fa3ba2ccad0fe81195f5)  \n> Date: Wed, 5 Sep 2018 06:54:27 -0400  \n> Author: root (root@kamailio3.kamailo3@lhsip.com)  \n> Committer: root (root@kamailio3.kamailo3@lhsip.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 21818f07eadc647bf227fa3ba2ccad0fe81195f5)\n[//]: # (START_SECTION 0df96f063ed8583a16434c58a09761d65ab1aa45)\n### Added support for Roles.  Now a dSIPRouter instance can have a Role in the tolopology\n\n> Commit: [0df96f063ed8583a16434c58a09761d65ab1aa45](https://github.com/dOpensource/dsiprouter/commit/0df96f063ed8583a16434c58a09761d65ab1aa45)  \n> Date: Tue, 4 Sep 2018 04:40:53 -0400  \n> Author: root (root@kamailio2.lhsip.com)  \n> Committer: root (root@kamailio2.lhsip.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0df96f063ed8583a16434c58a09761d65ab1aa45)\n[//]: # (START_SECTION 74fbcd830f062b37579daedb439cd29f8fc3426e)\n### Fixed an issue with SSL properties not being pulled corrected from the settings.py file\n\n> Commit: [74fbcd830f062b37579daedb439cd29f8fc3426e](https://github.com/dOpensource/dsiprouter/commit/74fbcd830f062b37579daedb439cd29f8fc3426e)  \n> Date: Tue, 4 Sep 2018 03:14:59 -0400  \n> Author: root (root@kamailio3.kamailo3@lhsip.com)  \n> Committer: root (root@kamailio3.kamailo3@lhsip.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 74fbcd830f062b37579daedb439cd29f8fc3426e)\n[//]: # (START_SECTION 5d43060eae83e900f29ed87ec7ecc7c0970ddf36)\n### Fixed an issue with SSL properties not being pulled corrected from the settings.py file\n\n> Commit: [5d43060eae83e900f29ed87ec7ecc7c0970ddf36](https://github.com/dOpensource/dsiprouter/commit/5d43060eae83e900f29ed87ec7ecc7c0970ddf36)  \n> Date: Tue, 4 Sep 2018 03:11:02 -0400  \n> Author: root (root@kamailio3.kamailo3@lhsip.com)  \n> Committer: root (root@kamailio3.kamailo3@lhsip.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5d43060eae83e900f29ed87ec7ecc7c0970ddf36)\n[//]: # (START_SECTION e8ca9ef732a81609097a40e66cf4595607dc01ad)\n### Changes to support single tenant\n\n> Commit: [e8ca9ef732a81609097a40e66cf4595607dc01ad](https://github.com/dOpensource/dsiprouter/commit/e8ca9ef732a81609097a40e66cf4595607dc01ad)  \n> Date: Tue, 4 Sep 2018 03:00:25 -0400  \n> Author: root (root@kamailio3.kamailo3@lhsip.com)  \n> Committer: root (root@kamailio3.kamailo3@lhsip.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e8ca9ef732a81609097a40e66cf4595607dc01ad)\n[//]: # (START_SECTION a227c1647381c5c70524fa225454e67bbf0eab51)\n### Fixed #71 - Added support for GUI Session timeout activity Fixed #72 - Cleaned up exception code around database connection\n\n> Commit: [a227c1647381c5c70524fa225454e67bbf0eab51](https://github.com/dOpensource/dsiprouter/commit/a227c1647381c5c70524fa225454e67bbf0eab51)  \n> Date: Sun, 2 Sep 2018 14:20:45 +0000  \n> Author: root (root@demo-dsiprouter.localdomain)  \n> Committer: root (root@demo-dsiprouter.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a227c1647381c5c70524fa225454e67bbf0eab51)\n[//]: # (START_SECTION 4482f38fa394bff1d040d7af6ffc3197c8cc2fe8)\n### Freepbx & Flowroute Feature Release v0.51\n\n> Commit: [4482f38fa394bff1d040d7af6ffc3197c8cc2fe8](https://github.com/dOpensource/dsiprouter/commit/4482f38fa394bff1d040d7af6ffc3197c8cc2fe8)  \n> Date: Tue, 28 Aug 2018 23:59:18 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- Resolves #1\n- Resolves #65\n- -- add pbx registration forwarding\n- -- add single tenant pbx domain routing\n- -- add flowroute did importing\n- -- fix fusionpbx conflicts with domain table\n- -- add generic multidomain pbx support\n- -- add syntax highliting in <code> blocks\n- -- add / optimize css vendor prefixes (autoprefix)\n- -- add combobox widget to inboundmapping view\n- -- add new icons for combobox widget\n- -- improved element disable functions\n- -- improved error view allowing debug messages\n- -- add custom domain tables to install script\n- -- fix nat reply bug in kamailio configs\n- -- merge new updates into kam44 config file\n- Signed-off-by: Tyler Moore <tmoore@goflyball.com>\n\n\n---\n\n[//]: # (END_SECTION 4482f38fa394bff1d040d7af6ffc3197c8cc2fe8)\n[//]: # (START_SECTION ae9c8c4c1620295fefa2f6ed6c9204f64e796a5f)\n### Updated the logo's\n\n> Commit: [ae9c8c4c1620295fefa2f6ed6c9204f64e796a5f](https://github.com/dOpensource/dsiprouter/commit/ae9c8c4c1620295fefa2f6ed6c9204f64e796a5f)  \n> Date: Tue, 28 Aug 2018 14:00:15 +0000  \n> Author: root (root@dsiprouter-v50-final.localdomain)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ae9c8c4c1620295fefa2f6ed6c9204f64e796a5f)\n[//]: # (START_SECTION 34119ac35b82766fbb66f4a3fad07773c20ee08e)\n### Fixed the PBX screen to ensure that ip auth is working, added fusionpbx as the default fusionpbx database username\n\n> Commit: [34119ac35b82766fbb66f4a3fad07773c20ee08e](https://github.com/dOpensource/dsiprouter/commit/34119ac35b82766fbb66f4a3fad07773c20ee08e)  \n> Date: Tue, 28 Aug 2018 12:50:03 +0000  \n> Author: root (root@dsiprouter-v050.localdomain)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 34119ac35b82766fbb66f4a3fad07773c20ee08e)\n[//]: # (START_SECTION ac97aa65f331a0007146d31343605d7cff480947)\n### Fixed issue with main navigation not showing the the proper color when a navigation button is not clicked\n\n> Commit: [ac97aa65f331a0007146d31343605d7cff480947](https://github.com/dOpensource/dsiprouter/commit/ac97aa65f331a0007146d31343605d7cff480947)  \n> Date: Mon, 27 Aug 2018 12:03:09 +0000  \n> Author: root (root@dsiprouter-v050.localdomain)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ac97aa65f331a0007146d31343605d7cff480947)\n[//]: # (START_SECTION 32eb94145b2c89bbcf1ac4059578d99a1905c64b)\n### Updated the login screen\n\n> Commit: [32eb94145b2c89bbcf1ac4059578d99a1905c64b](https://github.com/dOpensource/dsiprouter/commit/32eb94145b2c89bbcf1ac4059578d99a1905c64b)  \n> Date: Mon, 27 Aug 2018 11:15:03 +0000  \n> Author: root (root@dsiprouter-v050.localdomain)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 32eb94145b2c89bbcf1ac4059578d99a1905c64b)\n[//]: # (START_SECTION 8e1cdf4240c9f196cc69263c247ee7815810b5e8)\n### Fixed the issue with curl not returning the external ip address.  I changed out the URL that was being used to get the external ip address\n\n> Commit: [8e1cdf4240c9f196cc69263c247ee7815810b5e8](https://github.com/dOpensource/dsiprouter/commit/8e1cdf4240c9f196cc69263c247ee7815810b5e8)  \n> Date: Sun, 26 Aug 2018 02:50:35 +0000  \n> Author: root (root@dsiprouter-v050.localdomain)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8e1cdf4240c9f196cc69263c247ee7815810b5e8)\n[//]: # (START_SECTION 159d6005413754434b117140ab19ca173a115ab5)\n### Cleaned up a duplicate install function\n\n> Commit: [159d6005413754434b117140ab19ca173a115ab5](https://github.com/dOpensource/dsiprouter/commit/159d6005413754434b117140ab19ca173a115ab5)  \n> Date: Fri, 24 Aug 2018 11:56:03 +0000  \n> Author: root (root@dsiprouter-v050.localdomain)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 159d6005413754434b117140ab19ca173a115ab5)\n[//]: # (START_SECTION 9db51f765cdf90024c8ecabba4031d8e5142717f)\n### Revert \"Revert \"Add UI bug fix commits to v0.50\"\"\n\n> Commit: [9db51f765cdf90024c8ecabba4031d8e5142717f](https://github.com/dOpensource/dsiprouter/commit/9db51f765cdf90024c8ecabba4031d8e5142717f)  \n> Date: Thu, 23 Aug 2018 17:00:25 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9db51f765cdf90024c8ecabba4031d8e5142717f)\n[//]: # (START_SECTION f0c36be712f69718f3757c8bf3a349cc75f0b61d)\n### Revert \"Add UI bug fix commits to v0.50\"\n\n> Commit: [f0c36be712f69718f3757c8bf3a349cc75f0b61d](https://github.com/dOpensource/dsiprouter/commit/f0c36be712f69718f3757c8bf3a349cc75f0b61d)  \n> Date: Thu, 23 Aug 2018 10:50:30 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f0c36be712f69718f3757c8bf3a349cc75f0b61d)\n[//]: # (START_SECTION d40bb4dea60fcbfae7e84a1e8b2669ab31d04944)\n### Updated the logo's\n\n> Commit: [d40bb4dea60fcbfae7e84a1e8b2669ab31d04944](https://github.com/dOpensource/dsiprouter/commit/d40bb4dea60fcbfae7e84a1e8b2669ab31d04944)  \n> Date: Tue, 28 Aug 2018 14:00:15 +0000  \n> Author: root (root@dsiprouter-v50-final.localdomain)  \n> Committer: root (root@dsiprouter-v50-final.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d40bb4dea60fcbfae7e84a1e8b2669ab31d04944)\n[//]: # (START_SECTION 2043e95408976a879bc153e9f72725fa43e79f71)\n### Fixed the PBX screen to ensure that ip auth is working, added fusionpbx as the default fusionpbx database username\n\n> Commit: [2043e95408976a879bc153e9f72725fa43e79f71](https://github.com/dOpensource/dsiprouter/commit/2043e95408976a879bc153e9f72725fa43e79f71)  \n> Date: Tue, 28 Aug 2018 12:50:03 +0000  \n> Author: root (root@dsiprouter-v050.localdomain)  \n> Committer: root (root@dsiprouter-v050.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2043e95408976a879bc153e9f72725fa43e79f71)\n[//]: # (START_SECTION 8a86749753e83b9516beb9224629cba4c142bf9e)\n### Fixed issue with main navigation not showing the the proper color when a navigation button is not clicked\n\n> Commit: [8a86749753e83b9516beb9224629cba4c142bf9e](https://github.com/dOpensource/dsiprouter/commit/8a86749753e83b9516beb9224629cba4c142bf9e)  \n> Date: Mon, 27 Aug 2018 12:03:09 +0000  \n> Author: root (root@dsiprouter-v050.localdomain)  \n> Committer: root (root@dsiprouter-v050.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8a86749753e83b9516beb9224629cba4c142bf9e)\n[//]: # (START_SECTION ce9ef24adf7c44c087c49b85cd1fb2c1f6fd074b)\n### Updated the login screen\n\n> Commit: [ce9ef24adf7c44c087c49b85cd1fb2c1f6fd074b](https://github.com/dOpensource/dsiprouter/commit/ce9ef24adf7c44c087c49b85cd1fb2c1f6fd074b)  \n> Date: Mon, 27 Aug 2018 11:15:03 +0000  \n> Author: root (root@dsiprouter-v050.localdomain)  \n> Committer: root (root@dsiprouter-v050.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ce9ef24adf7c44c087c49b85cd1fb2c1f6fd074b)\n[//]: # (START_SECTION 34b15687a0ee6fb6e529d3d652b714be9d53c230)\n### Fixed the issue with curl not returning the external ip address.  I changed out the URL that was being used to get the external ip address\n\n> Commit: [34b15687a0ee6fb6e529d3d652b714be9d53c230](https://github.com/dOpensource/dsiprouter/commit/34b15687a0ee6fb6e529d3d652b714be9d53c230)  \n> Date: Sun, 26 Aug 2018 02:50:35 +0000  \n> Author: root (root@dsiprouter-v050.localdomain)  \n> Committer: root (root@dsiprouter-v050.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 34b15687a0ee6fb6e529d3d652b714be9d53c230)\n[//]: # (START_SECTION a76a8ec50d485591f24f1218773edb766d9feb4e)\n### Cleaned up a duplicate install function\n\n> Commit: [a76a8ec50d485591f24f1218773edb766d9feb4e](https://github.com/dOpensource/dsiprouter/commit/a76a8ec50d485591f24f1218773edb766d9feb4e)  \n> Date: Fri, 24 Aug 2018 11:56:03 +0000  \n> Author: root (root@dsiprouter-v050.localdomain)  \n> Committer: root (root@dsiprouter-v050.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a76a8ec50d485591f24f1218773edb766d9feb4e)\n[//]: # (START_SECTION 5ecec325b90e29462174bf36aec44fb3ac57bf13)\n### Revert \"Revert \"Add UI bug fix commits to v0.50\"\"\n\n> Commit: [5ecec325b90e29462174bf36aec44fb3ac57bf13](https://github.com/dOpensource/dsiprouter/commit/5ecec325b90e29462174bf36aec44fb3ac57bf13)  \n> Date: Thu, 23 Aug 2018 17:00:25 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5ecec325b90e29462174bf36aec44fb3ac57bf13)\n[//]: # (START_SECTION 62c1ba5ad7c5b74488f9f8c4707504202ecaa498)\n### Revert \"Add UI bug fix commits to v0.50\"\n\n> Commit: [62c1ba5ad7c5b74488f9f8c4707504202ecaa498](https://github.com/dOpensource/dsiprouter/commit/62c1ba5ad7c5b74488f9f8c4707504202ecaa498)  \n> Date: Thu, 23 Aug 2018 10:50:30 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 62c1ba5ad7c5b74488f9f8c4707504202ecaa498)\n[//]: # (START_SECTION 24af07468760a7d58cafadc94ff648a8d279aa34)\n### UI Bug Fixes in v0.50 continued..\n\n> Commit: [24af07468760a7d58cafadc94ff648a8d279aa34](https://github.com/dOpensource/dsiprouter/commit/24af07468760a7d58cafadc94ff648a8d279aa34)  \n> Date: Mon, 13 Aug 2018 17:05:18 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- -- fixed login page styles\n- -- fixed table styles\n- -- fixed update on pbx endpoint\n- -- fixed auth radio toggle\n- -- fixed uacreg import csv values\n- -- fixed update on uac table to enable flag\n- Signed-off-by: Tyler Moore <tmoore@goflyball.com>\n\n\n---\n\n[//]: # (END_SECTION 24af07468760a7d58cafadc94ff648a8d279aa34)\n[//]: # (START_SECTION faeec111a401183da874d0feaaa63104da45f07a)\n### UI Bug Fixes in v0.50\n\n> Commit: [faeec111a401183da874d0feaaa63104da45f07a](https://github.com/dOpensource/dsiprouter/commit/faeec111a401183da874d0feaaa63104da45f07a)  \n> Date: Fri, 10 Aug 2018 19:23:30 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- -- fixed toggle button listeners\n- -- fixed pbx modal listener (not populating)\n- -- fixed reload button refreshing on ajax call\n- Signed-off-by: Tyler Moore <tmoore@goflyball.com>\n\n\n---\n\n[//]: # (END_SECTION faeec111a401183da874d0feaaa63104da45f07a)\n[//]: # (START_SECTION eaa5c1d1256c2c700621d7ab9fbefb692217e93c)\n### Fix runtime error\n\n> Commit: [eaa5c1d1256c2c700621d7ab9fbefb692217e93c](https://github.com/dOpensource/dsiprouter/commit/eaa5c1d1256c2c700621d7ab9fbefb692217e93c)  \n> Date: Thu, 9 Aug 2018 14:07:11 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- -- change default server to use multi-threading\n- -- fix hanging comma in dsiprouter.py\n- Signed-off-by: Tyler Moore <tmoore@goflyball.com>\n\n\n---\n\n[//]: # (END_SECTION eaa5c1d1256c2c700621d7ab9fbefb692217e93c)\n[//]: # (START_SECTION 604f8b1ed33a7c3fa93889ed77433fa8396348c2)\n### Squash Commits and Merge with Master\n\n> Commit: [604f8b1ed33a7c3fa93889ed77433fa8396348c2](https://github.com/dOpensource/dsiprouter/commit/604f8b1ed33a7c3fa93889ed77433fa8396348c2)  \n> Date: Thu, 9 Aug 2018 11:35:31 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n- commit 3eb7182214594dfbbe3701487bb959f0aec4b08d\n- Merge: 8582913 6bbc7de\n- Author: Tyler Moore <tmoore@goflyball.com>\n- Date:   Thu Aug 9 11:12:24 2018 -0400\n- Merge remote-tracking branch 'origin/carrier-modal' into carrier-modal\n- # Conflicts:\n- #\tgui/settings.py\n- commit 85829134650ee4598f3c42db063b00f2ef16b4e8\n- Author: Tyler Moore <tmoore@goflyball.com>\n- Date:   Wed Aug 8 22:33:01 2018 -0400\n- v.50 Final Commit\n- Resolves #1\n- Resolves #4\n- Resolves #6\n- Resolves #55\n- Resolves #58\n- Resolves #62\n- Resolves #63\n- -- various DB query fixes\n- -- carrier delete fix\n- -- debug defaults set for production\n- -- custom icons added\n- -- Jquery queries optimized\n- -- JS library source map files fixed\n- -- HTTP errors fixed\n- + resolving IP dynamically\n- -- HTML errors fixed\n- + toggle buttons fixed\n- + query selector shadowing id's fixed\n- + radio button listeners fixed\n- + modal nesting fixed\n- + modal scrolling fixed\n- + modal styles fixed\n- -- add exception handling throughout API\n- -- add endpoint debugging when debug enabled\n- -- finish carrier group modal features\n- -- jinja macro additions\n- -- add gateway group configuration defaults\n- + dr_gw_lists table\n- + uacreg table\n- + dr_gateways table (update)\n- + address table (update)\n- -- install script fixes for kamailio config added\n- -- added group name editing feature\n- -- add conversion functions\n- -- change version number\n- -- many more small bug fixes / tweaks.. see diff\n- Signed-off-by: Tyler Moore <tmoore@goflyball.com>\n- commit 6bbc7defae59a6da1b8c146370621bb845fb9091\n- Author: Tyler Moore <tmoore@goflyball.com>\n- Date:   Wed Aug 8 22:33:01 2018 -0400\n- v.50 Final Merge\n- Resolves #1\n- Resolves #4\n- Resolves #6\n- Resolves #55\n- Resolves #58\n- Resolves #62\n- Resolves #63\n- -- various DB query fixes\n- -- carrier delete fix\n- -- debug defaults set for production\n- -- custom icons added\n- -- Jquery queries optimized\n- -- JS library source map files fixed\n- -- HTTP errors fixed\n- + resolving IP dynamically\n- -- HTML errors fixed\n- + toggle buttons fixed\n- + query selector shadowing id's fixed\n- + radio button listeners fixed\n- + modal nesting fixed\n- + modal scrolling fixed\n- + modal styles fixed\n- -- add exception handling throughout API\n- -- add endpoint debugging when debug enabled\n- -- finish carrier group modal features\n- -- jinja macro additions\n- -- add gateway group configuration defaults\n- + dr_gw_lists table\n- + uacreg table\n- + dr_gateways table (update)\n- + address table (update)\n- -- install script fixes for kamailio config added\n- -- added group name editing feature\n- -- add conversion functions\n- -- many more small bug fixes / tweaks.. see diff\n- Signed-off-by: Tyler Moore <tmoore@goflyball.com>\n- commit e14c688bd7b11145061d913a80236da0ad509eb6\n- Author: Tyler Moore <tmoore@goflyball.com>\n- Date:   Sun Jul 29 16:31:29 2018 -0400\n- Feature Addition: Carrier Groups\n- modifiy / resolves #57\n- resolves #60\n- resolves #9\n- This commit is for v0.50\n- https://github.com/dOpensource/dsiprouter/tree/v0.50\n- Add new carrier gruop route\n- Add UAC carrier registration\n- Add Voxbone carrier to default\n- Various backend additions\n- Add support for gw_lists table in DB\n- Fix table border in GUI\n- Fix GUI hideen modal / input selection bugs\n- Fix GUI modal input field data populating\n- Add notes for frontend improvements\n- Fix update config file\n- Add dynamic ip / domain methods\n- Various other fixes in commit changes\n- commit 9e0e97755ad3f87b366b162c13761ab5fec21d38\n- Author: Tyler Moore <tmoore@goflyball.com>\n- Date:   Tue Jul 10 20:05:33 2018 -0400\n- [#57] Feature Addition: Unique Domain Name Per PBX\n- resolves #57\n- This commit is for v0.50 https://github.com/dOpensource/dsiprouter/tree/v0.50\n- See screenshots in: dsiprouter/docs/images/features/pbx_domain\n- commit b67161169bbba39abb6327c9cefb1ee28961e743\n- Author: root <root@dsiprouter-dev.localdomain>\n- Date:   Mon Jul 2 21:21:48 2018 +0000\n- Removed the uk_cfk index\n- Signed-off-by: Tyler Moore <tmoore@goflyball.com>\n\n\n---\n\n[//]: # (END_SECTION 604f8b1ed33a7c3fa93889ed77433fa8396348c2)\n[//]: # (START_SECTION 72e7c725860c2656f2d24cb850b2535aed6c1f7d)\n### Update README.md\n\n> Commit: [72e7c725860c2656f2d24cb850b2535aed6c1f7d](https://github.com/dOpensource/dsiprouter/commit/72e7c725860c2656f2d24cb850b2535aed6c1f7d)  \n> Date: Fri, 6 Jul 2018 09:09:59 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 72e7c725860c2656f2d24cb850b2535aed6c1f7d)\n[//]: # (START_SECTION 65720cf08d35968c63c7b85b67fe7ad4f7c0b07a)\n### Update README.md\n\n> Commit: [65720cf08d35968c63c7b85b67fe7ad4f7c0b07a](https://github.com/dOpensource/dsiprouter/commit/65720cf08d35968c63c7b85b67fe7ad4f7c0b07a)  \n> Date: Fri, 6 Jul 2018 09:09:05 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 65720cf08d35968c63c7b85b67fe7ad4f7c0b07a)\n[//]: # (START_SECTION 0244a9e4fd8a186d2a232abe632e75bb33d946ba)\n### Update kamailio51_dsiprouter.cfg\n\n> Commit: [0244a9e4fd8a186d2a232abe632e75bb33d946ba](https://github.com/dOpensource/dsiprouter/commit/0244a9e4fd8a186d2a232abe632e75bb33d946ba)  \n> Date: Tue, 3 Jul 2018 17:09:44 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0244a9e4fd8a186d2a232abe632e75bb33d946ba)\n[//]: # (START_SECTION b67161169bbba39abb6327c9cefb1ee28961e743)\n### Removed the uk_cfk index\n\n> Commit: [b67161169bbba39abb6327c9cefb1ee28961e743](https://github.com/dOpensource/dsiprouter/commit/b67161169bbba39abb6327c9cefb1ee28961e743)  \n> Date: Mon, 2 Jul 2018 21:21:48 +0000  \n> Author: root (root@dsiprouter-dev.localdomain)  \n> Committer: root (root@dsiprouter-dev.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b67161169bbba39abb6327c9cefb1ee28961e743)\n[//]: # (START_SECTION 057a9c10c0a2da25d237e953f34bb05bf417fc4e)\n### Removed the uk_cfk index\n\n> Commit: [057a9c10c0a2da25d237e953f34bb05bf417fc4e](https://github.com/dOpensource/dsiprouter/commit/057a9c10c0a2da25d237e953f34bb05bf417fc4e)  \n> Date: Mon, 2 Jul 2018 21:21:48 +0000  \n> Author: root (root@dsiprouter-dev.localdomain)  \n> Committer: root (root@dsiprouter-dev.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 057a9c10c0a2da25d237e953f34bb05bf417fc4e)\n[//]: # (START_SECTION 7cfe35f51f54199b21541dd5d1d171c11d1bea3b)\n### Update README.md\n\n> Commit: [7cfe35f51f54199b21541dd5d1d171c11d1bea3b](https://github.com/dOpensource/dsiprouter/commit/7cfe35f51f54199b21541dd5d1d171c11d1bea3b)  \n> Date: Tue, 26 Jun 2018 04:04:27 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7cfe35f51f54199b21541dd5d1d171c11d1bea3b)\n[//]: # (START_SECTION 90033f6fe351f1862ad13e937442c10814f22ca9)\n### Update README.md\n\n> Commit: [90033f6fe351f1862ad13e937442c10814f22ca9](https://github.com/dOpensource/dsiprouter/commit/90033f6fe351f1862ad13e937442c10814f22ca9)  \n> Date: Tue, 26 Jun 2018 04:03:13 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 90033f6fe351f1862ad13e937442c10814f22ca9)\n[//]: # (START_SECTION d98c01c94caa8fb1196ecd99a133f147a9d77e32)\n### Update README.md\n\n> Commit: [d98c01c94caa8fb1196ecd99a133f147a9d77e32](https://github.com/dOpensource/dsiprouter/commit/d98c01c94caa8fb1196ecd99a133f147a9d77e32)  \n> Date: Tue, 26 Jun 2018 03:57:51 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d98c01c94caa8fb1196ecd99a133f147a9d77e32)\n[//]: # (START_SECTION 533adb9aed63672d170d8e7c75c8ffb694249d10)\n### Fixed the dSIPRouter logo\n\n> Commit: [533adb9aed63672d170d8e7c75c8ffb694249d10](https://github.com/dOpensource/dsiprouter/commit/533adb9aed63672d170d8e7c75c8ffb694249d10)  \n> Date: Sun, 24 Jun 2018 23:47:17 +0000  \n> Author: root (root@dsiprouter-v0.41-dev)  \n> Committer: root (root@dsiprouter-v0.41-dev)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 533adb9aed63672d170d8e7c75c8ffb694249d10)\n[//]: # (START_SECTION eb69b92f8964cf16740d8a4c6cc86f7c40e2a613)\n### Removed install script logic out for right now\n\n> Commit: [eb69b92f8964cf16740d8a4c6cc86f7c40e2a613](https://github.com/dOpensource/dsiprouter/commit/eb69b92f8964cf16740d8a4c6cc86f7c40e2a613)  \n> Date: Sun, 24 Jun 2018 22:37:43 +0000  \n> Author: root (root@dsiprouter-v0.41-dev)  \n> Committer: root (root@dsiprouter-v0.41-dev)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eb69b92f8964cf16740d8a4c6cc86f7c40e2a613)\n[//]: # (START_SECTION fef2d3bf8aeffef583fc9f083e2fa5b7b2fec7b6)\n### Fixed the script\n\n> Commit: [fef2d3bf8aeffef583fc9f083e2fa5b7b2fec7b6](https://github.com/dOpensource/dsiprouter/commit/fef2d3bf8aeffef583fc9f083e2fa5b7b2fec7b6)  \n> Date: Sun, 24 Jun 2018 22:21:12 +0000  \n> Author: root (root@dsiprouter-v0.41-dev)  \n> Committer: root (root@dsiprouter-v0.41-dev)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fef2d3bf8aeffef583fc9f083e2fa5b7b2fec7b6)\n[//]: # (START_SECTION 401acdfed4df133a6a02e753def5aa6c5283b630)\n### Added dSIP ascii logo  after the installation process\n\n> Commit: [401acdfed4df133a6a02e753def5aa6c5283b630](https://github.com/dOpensource/dsiprouter/commit/401acdfed4df133a6a02e753def5aa6c5283b630)  \n> Date: Sun, 24 Jun 2018 22:19:51 +0000  \n> Author: root (root@dsiprouter-v0.41-dev)  \n> Committer: root (root@dsiprouter-v0.41-dev)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 401acdfed4df133a6a02e753def5aa6c5283b630)\n[//]: # (START_SECTION a737580886187327c2e34c73d21c14fb054f82bc)\n### Fixed an issue with the function that added the firewall rule\n\n> Commit: [a737580886187327c2e34c73d21c14fb054f82bc](https://github.com/dOpensource/dsiprouter/commit/a737580886187327c2e34c73d21c14fb054f82bc)  \n> Date: Sat, 23 Jun 2018 00:02:24 +0000  \n> Author: root (root@p2.detroitpbx.com)  \n> Committer: root (root@p2.detroitpbx.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a737580886187327c2e34c73d21c14fb054f82bc)\n[//]: # (START_SECTION e846c2ed168aa164bae78bac7a101d79d2728c0d)\n### Fixed issues to support Domain Routing with FusionPBX and to support hosting images for endpoint devices like the Polycom\n\n> Commit: [e846c2ed168aa164bae78bac7a101d79d2728c0d](https://github.com/dOpensource/dsiprouter/commit/e846c2ed168aa164bae78bac7a101d79d2728c0d)  \n> Date: Fri, 22 Jun 2018 15:57:56 +0000  \n> Author: root (root@p1.detrotpbx.com)  \n> Committer: root (root@p1.detrotpbx.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e846c2ed168aa164bae78bac7a101d79d2728c0d)\n[//]: # (START_SECTION 1f4957ca82af70f81849d113ee848db38f37fff0)\n### Added changed to support proper BYE propagation when using Domain Routing with FusionPBX\n\n> Commit: [1f4957ca82af70f81849d113ee848db38f37fff0](https://github.com/dOpensource/dsiprouter/commit/1f4957ca82af70f81849d113ee848db38f37fff0)  \n> Date: Mon, 18 Jun 2018 01:00:15 +0000  \n> Author: root (root@p1.detrotpbx.com)  \n> Committer: root (root@p1.detrotpbx.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1f4957ca82af70f81849d113ee848db38f37fff0)\n[//]: # (START_SECTION 40b13f23a7fa7b0a9fc81002619cddc98019d1c2)\n### Fixed an issue with a missing compiler directive and support for UPDATE SIP messages\n\n> Commit: [40b13f23a7fa7b0a9fc81002619cddc98019d1c2](https://github.com/dOpensource/dsiprouter/commit/40b13f23a7fa7b0a9fc81002619cddc98019d1c2)  \n> Date: Sun, 17 Jun 2018 02:18:09 +0000  \n> Author: root (root@dsiprouter-v0.41-dev)  \n> Committer: root (root@dsiprouter-v0.41-dev)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 40b13f23a7fa7b0a9fc81002619cddc98019d1c2)\n[//]: # (START_SECTION cc021b41837dcf733b79d92a2dfb31a63af81451)\n### Disabled server NAT by default\n\n> Commit: [cc021b41837dcf733b79d92a2dfb31a63af81451](https://github.com/dOpensource/dsiprouter/commit/cc021b41837dcf733b79d92a2dfb31a63af81451)  \n> Date: Sat, 16 Jun 2018 09:39:13 +0000  \n> Author: root (root@ip-172-31-53-160.ec2.internal)  \n> Committer: root (root@ip-172-31-53-160.ec2.internal)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cc021b41837dcf733b79d92a2dfb31a63af81451)\n[//]: # (START_SECTION 4451243dad5172df0253b01c6cd4215859a4fb29)\n### Fixed issues with SERVERNAT feature\n\n> Commit: [4451243dad5172df0253b01c6cd4215859a4fb29](https://github.com/dOpensource/dsiprouter/commit/4451243dad5172df0253b01c6cd4215859a4fb29)  \n> Date: Thu, 14 Jun 2018 00:58:41 -0500  \n> Author: Mack (mack@dopensource.com)  \n> Committer: Mack (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4451243dad5172df0253b01c6cd4215859a4fb29)\n[//]: # (START_SECTION ae4bccd75b75d6bf800cfeb8444788803c3ade2a)\n### Fixed an issue with Outbound routes\n\n> Commit: [ae4bccd75b75d6bf800cfeb8444788803c3ade2a](https://github.com/dOpensource/dsiprouter/commit/ae4bccd75b75d6bf800cfeb8444788803c3ade2a)  \n> Date: Wed, 13 Jun 2018 03:42:36 -0400  \n> Author: root (root@dsiprouter.dopensource.com)  \n> Committer: root (root@dsiprouter.dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ae4bccd75b75d6bf800cfeb8444788803c3ade2a)\n[//]: # (START_SECTION c4f631d185987e98b17243f406af58941b0c0a64)\n### Adding the javasript file for bootstrap validation\n\n> Commit: [c4f631d185987e98b17243f406af58941b0c0a64](https://github.com/dOpensource/dsiprouter/commit/c4f631d185987e98b17243f406af58941b0c0a64)  \n> Date: Wed, 13 Jun 2018 07:18:43 +0000  \n> Author: root (root@dsiprouter-v0.41-dev)  \n> Committer: root (root@dsiprouter-v0.41-dev)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c4f631d185987e98b17243f406af58941b0c0a64)\n[//]: # (START_SECTION e51f3bbcc586e59b0e7712448f9e9e1587d76b64)\n### Fixed some issues with Javascript validation\n\n> Commit: [e51f3bbcc586e59b0e7712448f9e9e1587d76b64](https://github.com/dOpensource/dsiprouter/commit/e51f3bbcc586e59b0e7712448f9e9e1587d76b64)  \n> Date: Tue, 12 Jun 2018 20:16:07 +0000  \n> Author: root (root@dsiprouter-v0.41-dev)  \n> Committer: root (root@dsiprouter-v0.41-dev)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e51f3bbcc586e59b0e7712448f9e9e1587d76b64)\n[//]: # (START_SECTION 247a804eef2191cf740e6c01ef03ab7bdaaca0f7)\n### Fixed the rtpengine parameter that specifies the protocol used to communicate between Kamailio and RTPEngine\n\n> Commit: [247a804eef2191cf740e6c01ef03ab7bdaaca0f7](https://github.com/dOpensource/dsiprouter/commit/247a804eef2191cf740e6c01ef03ab7bdaaca0f7)  \n> Date: Tue, 12 Jun 2018 14:36:58 -0400  \n> Author: root (root@dsiprouter.dopensource.com)  \n> Committer: root (root@dsiprouter.dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 247a804eef2191cf740e6c01ef03ab7bdaaca0f7)\n[//]: # (START_SECTION 99eb299b51fa3a3e2d925472900e3c55ea2d545a)\n### Fixes #44  issues with installer and logrotate\n\n> Commit: [99eb299b51fa3a3e2d925472900e3c55ea2d545a](https://github.com/dOpensource/dsiprouter/commit/99eb299b51fa3a3e2d925472900e3c55ea2d545a)  \n> Date: Tue, 12 Jun 2018 14:16:22 -0400  \n> Author: root (root@dsiprouter.dopensource.com)  \n> Committer: root (root@dsiprouter.dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 99eb299b51fa3a3e2d925472900e3c55ea2d545a)\n[//]: # (START_SECTION afdbb834d28964461afeb3059b2363088692311a)\n### Fixed issue with install of SERVERNET\n\n> Commit: [afdbb834d28964461afeb3059b2363088692311a](https://github.com/dOpensource/dsiprouter/commit/afdbb834d28964461afeb3059b2363088692311a)  \n> Date: Tue, 12 Jun 2018 13:02:31 -0400  \n> Author: root (root@dsiprouter.dopensource.com)  \n> Committer: root (root@dsiprouter.dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION afdbb834d28964461afeb3059b2363088692311a)\n[//]: # (START_SECTION c087c50b9350c2a5e38107b9b37b7ea192c618c0)\n### Added the 0.41 version\n\n> Commit: [c087c50b9350c2a5e38107b9b37b7ea192c618c0](https://github.com/dOpensource/dsiprouter/commit/c087c50b9350c2a5e38107b9b37b7ea192c618c0)  \n> Date: Tue, 12 Jun 2018 12:05:33 -0400  \n> Author: root (root@dsiprouter.dopensource.com)  \n> Committer: root (root@dsiprouter.dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c087c50b9350c2a5e38107b9b37b7ea192c618c0)\n[//]: # (START_SECTION 26b951e359896d96a4ce11cc279d26de9dff854d)\n### Fixes 51 - Fixed the update logic when an existing LCR prefix is already defined, but you want to update it\n\n> Commit: [26b951e359896d96a4ce11cc279d26de9dff854d](https://github.com/dOpensource/dsiprouter/commit/26b951e359896d96a4ce11cc279d26de9dff854d)  \n> Date: Sun, 10 Jun 2018 22:54:09 +0000  \n> Author: root (root@dsiprouter-v0.41-dev)  \n> Committer: root (root@dsiprouter-v0.41-dev)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 26b951e359896d96a4ce11cc279d26de9dff854d)\n[//]: # (START_SECTION c877002e29b69b5a1e5d54318317124e8a4d66bc)\n### Added some comments and a record_route() when routing to PBX's\n\n> Commit: [c877002e29b69b5a1e5d54318317124e8a4d66bc](https://github.com/dOpensource/dsiprouter/commit/c877002e29b69b5a1e5d54318317124e8a4d66bc)  \n> Date: Sun, 10 Jun 2018 21:43:50 +0000  \n> Author: root (root@dsiprouter-v0.41-dev)  \n> Committer: root (root@dsiprouter-v0.41-dev)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c877002e29b69b5a1e5d54318317124e8a4d66bc)\n[//]: # (START_SECTION b8f206a19ca75c25914dd7f7c1a246d9404dc235)\n### Changed the URI to /provision\n\n> Commit: [b8f206a19ca75c25914dd7f7c1a246d9404dc235](https://github.com/dOpensource/dsiprouter/commit/b8f206a19ca75c25914dd7f7c1a246d9404dc235)  \n> Date: Sun, 10 Jun 2018 17:07:25 +0000  \n> Author: root (root@dsiprouter-v0.41-dev)  \n> Committer: root (root@dsiprouter-v0.41-dev)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b8f206a19ca75c25914dd7f7c1a246d9404dc235)\n[//]: # (START_SECTION e4413eee1fb2a137ff9628fe4cf7863c34d199e6)\n### Fixed an issue that was preventing the docker engine to install properly.\n\n> Commit: [e4413eee1fb2a137ff9628fe4cf7863c34d199e6](https://github.com/dOpensource/dsiprouter/commit/e4413eee1fb2a137ff9628fe4cf7863c34d199e6)  \n> Date: Fri, 8 Jun 2018 19:01:35 +0000  \n> Author: root (root@dsiprouter-v0.41-dev)  \n> Committer: root (root@dsiprouter-v0.41-dev)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e4413eee1fb2a137ff9628fe4cf7863c34d199e6)\n[//]: # (START_SECTION ce18ec722cbd8e69d08178ef594768ee10afdc68)\n### Fixed #51 - Added more exception handling to handle updates\n\n> Commit: [ce18ec722cbd8e69d08178ef594768ee10afdc68](https://github.com/dOpensource/dsiprouter/commit/ce18ec722cbd8e69d08178ef594768ee10afdc68)  \n> Date: Wed, 6 Jun 2018 18:18:13 -0400  \n> Author: root (root@siprtr-1.mercury.net)  \n> Committer: root (root@siprtr-1.mercury.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ce18ec722cbd8e69d08178ef594768ee10afdc68)\n[//]: # (START_SECTION 2efb34fed0631b19312fc14e958eb79520e3066a)\n### Fixes #52 - Added iptables-save to the list of steps needed to active FusionPBX support.  Without this option the iptables rule will not be added during the next reboot\n\n> Commit: [2efb34fed0631b19312fc14e958eb79520e3066a](https://github.com/dOpensource/dsiprouter/commit/2efb34fed0631b19312fc14e958eb79520e3066a)  \n> Date: Tue, 5 Jun 2018 11:24:11 +0000  \n> Author: root (root@dsiprouter-v0.41-dev)  \n> Committer: root (root@dsiprouter-v0.41-dev)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2efb34fed0631b19312fc14e958eb79520e3066a)\n[//]: # (START_SECTION 68eceda6b12cb556d57d9a37387be9bc566ec353)\n### Fixes #51 - The update logic for Outbound Routes was refactored\n\n> Commit: [68eceda6b12cb556d57d9a37387be9bc566ec353](https://github.com/dOpensource/dsiprouter/commit/68eceda6b12cb556d57d9a37387be9bc566ec353)  \n> Date: Tue, 5 Jun 2018 07:17:27 -0400  \n> Author: root (root@siprtr-1.mercury.net)  \n> Committer: root (root@siprtr-1.mercury.net)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 68eceda6b12cb556d57d9a37387be9bc566ec353)\n[//]: # (START_SECTION eb7bce74395a989ffa97021d37ac6876a6bf0211)\n### Update README.md\n\n> Commit: [eb7bce74395a989ffa97021d37ac6876a6bf0211](https://github.com/dOpensource/dsiprouter/commit/eb7bce74395a989ffa97021d37ac6876a6bf0211)  \n> Date: Sun, 27 May 2018 19:44:18 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eb7bce74395a989ffa97021d37ac6876a6bf0211)\n[//]: # (START_SECTION 4e481da4931c9dc31ddf420a78640910ebc88724)\n### Update README.md\n\n> Commit: [4e481da4931c9dc31ddf420a78640910ebc88724](https://github.com/dOpensource/dsiprouter/commit/4e481da4931c9dc31ddf420a78640910ebc88724)  \n> Date: Sun, 27 May 2018 19:42:49 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4e481da4931c9dc31ddf420a78640910ebc88724)\n[//]: # (START_SECTION 23d2d38febdcd7c166dbc3e0c54f6af0afe9580d)\n### Fixes #49 - SIP OPTION messages will be handled by only replying to them is the source ip address is a defined carrier or pbx/endpoint\n\n> Commit: [23d2d38febdcd7c166dbc3e0c54f6af0afe9580d](https://github.com/dOpensource/dsiprouter/commit/23d2d38febdcd7c166dbc3e0c54f6af0afe9580d)  \n> Date: Sun, 27 May 2018 07:39:59 -0400  \n> Author: root (root@dsiprouter.dopensource.com)  \n> Committer: root (root@dsiprouter.dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 23d2d38febdcd7c166dbc3e0c54f6af0afe9580d)\n[//]: # (START_SECTION ecada9a3a8c3bfb7f046a529511954787cfb374c)\n### Added configuration files for logrotate so that log files are rotated\n\n> Commit: [ecada9a3a8c3bfb7f046a529511954787cfb374c](https://github.com/dOpensource/dsiprouter/commit/ecada9a3a8c3bfb7f046a529511954787cfb374c)  \n> Date: Tue, 22 May 2018 15:14:56 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ecada9a3a8c3bfb7f046a529511954787cfb374c)\n[//]: # (START_SECTION e9557065aec29b9b749007d8ce2e554bd667e385)\n### Fixed an issue with dsiprouter command line\n\n> Commit: [e9557065aec29b9b749007d8ce2e554bd667e385](https://github.com/dOpensource/dsiprouter/commit/e9557065aec29b9b749007d8ce2e554bd667e385)  \n> Date: Mon, 21 May 2018 11:46:15 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e9557065aec29b9b749007d8ce2e554bd667e385)\n[//]: # (START_SECTION cff11fb0915558adc02688aba563ad741b48bedc)\n### Update README.md\n\n> Commit: [cff11fb0915558adc02688aba563ad741b48bedc](https://github.com/dOpensource/dsiprouter/commit/cff11fb0915558adc02688aba563ad741b48bedc)  \n> Date: Thu, 17 May 2018 10:36:24 +0200  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cff11fb0915558adc02688aba563ad741b48bedc)\n[//]: # (START_SECTION 1e82263c7d8a89158791b024d8b09385a4a14c29)\n### Fixed an error with the RTPEngine install\n\n> Commit: [1e82263c7d8a89158791b024d8b09385a4a14c29](https://github.com/dOpensource/dsiprouter/commit/1e82263c7d8a89158791b024d8b09385a4a14c29)  \n> Date: Thu, 17 May 2018 03:40:09 -0400  \n> Author: root (root@dsiprouter.dopensource.com)  \n> Committer: root (root@dsiprouter.dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1e82263c7d8a89158791b024d8b09385a4a14c29)\n[//]: # (START_SECTION 24e6db8e5157a6398a937aeef764fd3aba2eaad6)\n### Set RTPEngine to start after it's installed\n\n> Commit: [24e6db8e5157a6398a937aeef764fd3aba2eaad6](https://github.com/dOpensource/dsiprouter/commit/24e6db8e5157a6398a937aeef764fd3aba2eaad6)  \n> Date: Thu, 17 May 2018 03:29:12 -0400  \n> Author: root (root@dsiprouter.dopensource.com)  \n> Committer: root (root@dsiprouter.dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 24e6db8e5157a6398a937aeef764fd3aba2eaad6)\n[//]: # (START_SECTION 3dbe7b98c7ae4d59ca0228130a5f1628a9bad658)\n### Fixed the configuration file for setting up RTP Engine on Debian\n\n> Commit: [3dbe7b98c7ae4d59ca0228130a5f1628a9bad658](https://github.com/dOpensource/dsiprouter/commit/3dbe7b98c7ae4d59ca0228130a5f1628a9bad658)  \n> Date: Thu, 17 May 2018 03:09:46 -0400  \n> Author: root (root@dsiprouter.dopensource.com)  \n> Committer: root (root@dsiprouter.dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3dbe7b98c7ae4d59ca0228130a5f1628a9bad658)\n[//]: # (START_SECTION 0695c19827c5c792e8be0e3df05f14c23922a318)\n### Update README.md\n\n> Commit: [0695c19827c5c792e8be0e3df05f14c23922a318](https://github.com/dOpensource/dsiprouter/commit/0695c19827c5c792e8be0e3df05f14c23922a318)  \n> Date: Thu, 17 May 2018 07:11:48 +0200  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0695c19827c5c792e8be0e3df05f14c23922a318)\n[//]: # (START_SECTION eea952b11083b9a651e568605fca196bd92ebe12)\n### Update README.md\n\n> Commit: [eea952b11083b9a651e568605fca196bd92ebe12](https://github.com/dOpensource/dsiprouter/commit/eea952b11083b9a651e568605fca196bd92ebe12)  \n> Date: Thu, 17 May 2018 07:07:40 +0200  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eea952b11083b9a651e568605fca196bd92ebe12)\n[//]: # (START_SECTION df392036e356dfe86b4941f576df8d69db7f75ee)\n### Update README.md\n\n> Commit: [df392036e356dfe86b4941f576df8d69db7f75ee](https://github.com/dOpensource/dsiprouter/commit/df392036e356dfe86b4941f576df8d69db7f75ee)  \n> Date: Wed, 16 May 2018 10:40:41 +0200  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION df392036e356dfe86b4941f576df8d69db7f75ee)\n[//]: # (START_SECTION 5952b9b095b21b5e7b98375ec0ca9eba63a5c6fc)\n### Add files via upload\n\n> Commit: [5952b9b095b21b5e7b98375ec0ca9eba63a5c6fc](https://github.com/dOpensource/dsiprouter/commit/5952b9b095b21b5e7b98375ec0ca9eba63a5c6fc)  \n> Date: Wed, 16 May 2018 10:39:42 +0200  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5952b9b095b21b5e7b98375ec0ca9eba63a5c6fc)\n[//]: # (START_SECTION 92d22847128a8ce2ac8c3e0b202400cf3d4bea16)\n### Update README.md\n\n> Commit: [92d22847128a8ce2ac8c3e0b202400cf3d4bea16](https://github.com/dOpensource/dsiprouter/commit/92d22847128a8ce2ac8c3e0b202400cf3d4bea16)  \n> Date: Wed, 16 May 2018 10:37:01 +0200  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 92d22847128a8ce2ac8c3e0b202400cf3d4bea16)\n[//]: # (START_SECTION 685a01df3f8e8b610704515029fc493d11b1b3af)\n### Update README.md\n\n> Commit: [685a01df3f8e8b610704515029fc493d11b1b3af](https://github.com/dOpensource/dsiprouter/commit/685a01df3f8e8b610704515029fc493d11b1b3af)  \n> Date: Wed, 16 May 2018 10:32:24 +0200  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 685a01df3f8e8b610704515029fc493d11b1b3af)\n[//]: # (START_SECTION 7ec83e8664298ec47f8c88b4860f31e6c34b7652)\n### Update README.md\n\n> Commit: [7ec83e8664298ec47f8c88b4860f31e6c34b7652](https://github.com/dOpensource/dsiprouter/commit/7ec83e8664298ec47f8c88b4860f31e6c34b7652)  \n> Date: Wed, 16 May 2018 10:19:35 +0200  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7ec83e8664298ec47f8c88b4860f31e6c34b7652)\n[//]: # (START_SECTION f1dc57887b223f9379e831583629734dd9786eaf)\n### Fixed an issue with username/password auth Fixes #39\n\n> Commit: [f1dc57887b223f9379e831583629734dd9786eaf](https://github.com/dOpensource/dsiprouter/commit/f1dc57887b223f9379e831583629734dd9786eaf)  \n> Date: Wed, 16 May 2018 07:40:10 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f1dc57887b223f9379e831583629734dd9786eaf)\n[//]: # (START_SECTION ec42725b41311be22dc6c4834763584a88382a5c)\n### New Logo and GUI Fixes - Fixes #40\n\n> Commit: [ec42725b41311be22dc6c4834763584a88382a5c](https://github.com/dOpensource/dsiprouter/commit/ec42725b41311be22dc6c4834763584a88382a5c)  \n> Date: Wed, 16 May 2018 07:16:08 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ec42725b41311be22dc6c4834763584a88382a5c)\n[//]: # (START_SECTION 537a9c2853e72dcade24a8cacbc095a1395ab5ba)\n### Fixed the csv file so that each carrier contains a name: in the tags/notes column.  This is used to manage the Gateways Fixes #41\n\n> Commit: [537a9c2853e72dcade24a8cacbc095a1395ab5ba](https://github.com/dOpensource/dsiprouter/commit/537a9c2853e72dcade24a8cacbc095a1395ab5ba)  \n> Date: Tue, 15 May 2018 23:04:42 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 537a9c2853e72dcade24a8cacbc095a1395ab5ba)\n[//]: # (START_SECTION d9bbc6e79b13a20b7f760d337bd8bbc59ca4b7ab)\n### Added record routes when calling outbound via carriers to ensure that the BYE is routed back throught Kamailio\n\n> Commit: [d9bbc6e79b13a20b7f760d337bd8bbc59ca4b7ab](https://github.com/dOpensource/dsiprouter/commit/d9bbc6e79b13a20b7f760d337bd8bbc59ca4b7ab)  \n> Date: Tue, 15 May 2018 22:59:36 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d9bbc6e79b13a20b7f760d337bd8bbc59ca4b7ab)\n[//]: # (START_SECTION 30a055177292dcc3e3b23c99d469984b478d04d4)\n### Update address.csv\n\n> Commit: [30a055177292dcc3e3b23c99d469984b478d04d4](https://github.com/dOpensource/dsiprouter/commit/30a055177292dcc3e3b23c99d469984b478d04d4)  \n> Date: Tue, 15 May 2018 23:23:00 +0200  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 30a055177292dcc3e3b23c99d469984b478d04d4)\n[//]: # (START_SECTION 15f0f27b64f9f6bc5331945397d4b59b7fd3567c)\n### Add Support for FusionPBX Provisioning Fixes #26\n\n> Commit: [15f0f27b64f9f6bc5331945397d4b59b7fd3567c](https://github.com/dOpensource/dsiprouter/commit/15f0f27b64f9f6bc5331945397d4b59b7fd3567c)  \n> Date: Tue, 15 May 2018 20:17:39 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 15f0f27b64f9f6bc5331945397d4b59b7fd3567c)\n[//]: # (START_SECTION 2f087482ff0428518cde1258be49d247180d26a7)\n### Added the threaded option to allow the service to startup in multi-threaded mode\n\n> Commit: [2f087482ff0428518cde1258be49d247180d26a7](https://github.com/dOpensource/dsiprouter/commit/2f087482ff0428518cde1258be49d247180d26a7)  \n> Date: Sun, 13 May 2018 23:26:03 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2f087482ff0428518cde1258be49d247180d26a7)\n[//]: # (START_SECTION 5942ccef53964881d4497570c3d6c902eb906e6b)\n### Fixed an issue that prevented the PBX password from being updated\n\n> Commit: [5942ccef53964881d4497570c3d6c902eb906e6b](https://github.com/dOpensource/dsiprouter/commit/5942ccef53964881d4497570c3d6c902eb906e6b)  \n> Date: Wed, 9 May 2018 16:22:10 -0400  \n> Author: root (release@dopensource.com)  \n> Committer: root (release@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5942ccef53964881d4497570c3d6c902eb906e6b)\n[//]: # (START_SECTION bc07bb51a36a32ade8d3ffda0ddec66c04283934)\n### Added support for automatically adding the PBX ip, port and transport when it registers.  This means that it automatically gets added to the drouting.gateway table and the table is reloaded in real time\n\n> Commit: [bc07bb51a36a32ade8d3ffda0ddec66c04283934](https://github.com/dOpensource/dsiprouter/commit/bc07bb51a36a32ade8d3ffda0ddec66c04283934)  \n> Date: Sun, 29 Apr 2018 18:32:49 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bc07bb51a36a32ade8d3ffda0ddec66c04283934)\n[//]: # (START_SECTION cb161110adb2fcedcdf9ce0cce875037b1b4cabb)\n### Update settings.py\n\n> Commit: [cb161110adb2fcedcdf9ce0cce875037b1b4cabb](https://github.com/dOpensource/dsiprouter/commit/cb161110adb2fcedcdf9ce0cce875037b1b4cabb)  \n> Date: Tue, 24 Apr 2018 16:38:47 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cb161110adb2fcedcdf9ce0cce875037b1b4cabb)\n[//]: # (START_SECTION 89ff2191ca0782707302f6978f66261be38e8847)\n### Change the description of the default outbound routes\n\n> Commit: [89ff2191ca0782707302f6978f66261be38e8847](https://github.com/dOpensource/dsiprouter/commit/89ff2191ca0782707302f6978f66261be38e8847)  \n> Date: Tue, 24 Apr 2018 15:59:53 -0400  \n> Author: root (root@dsiprouter.dopensource.com)  \n> Committer: root (root@dsiprouter.dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 89ff2191ca0782707302f6978f66261be38e8847)\n[//]: # (START_SECTION bdfc4db674b9b4524bf91fcf84ff97f98e7472c6)\n### Fixed an issue with reloading the htable that support the new outbound route logic\n\n> Commit: [bdfc4db674b9b4524bf91fcf84ff97f98e7472c6](https://github.com/dOpensource/dsiprouter/commit/bdfc4db674b9b4524bf91fcf84ff97f98e7472c6)  \n> Date: Mon, 23 Apr 2018 07:10:18 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bdfc4db674b9b4524bf91fcf84ff97f98e7472c6)\n[//]: # (START_SECTION 77ac196b2a3270f7ad259f1aa5602b02695d3e07)\n### Added a flag to make te built-in web server multi-threaded\n\n> Commit: [77ac196b2a3270f7ad259f1aa5602b02695d3e07](https://github.com/dOpensource/dsiprouter/commit/77ac196b2a3270f7ad259f1aa5602b02695d3e07)  \n> Date: Sat, 14 Apr 2018 08:06:26 -0400  \n> Author: root (release@dopensource.com)  \n> Committer: root (release@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 77ac196b2a3270f7ad259f1aa5602b02695d3e07)\n[//]: # (START_SECTION 24a9407b2a713ba31bc76c8958cc92cf993ee98e)\n### Fixed issue with update and save for LCR\n\n> Commit: [24a9407b2a713ba31bc76c8958cc92cf993ee98e](https://github.com/dOpensource/dsiprouter/commit/24a9407b2a713ba31bc76c8958cc92cf993ee98e)  \n> Date: Fri, 6 Apr 2018 11:48:41 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 24a9407b2a713ba31bc76c8958cc92cf993ee98e)\n[//]: # (START_SECTION 8fe5ea6b04a20ebadbb3be2f439c63a5257c0c07)\n### Completed the development of some light weight LCR funcationality\n\n> Commit: [8fe5ea6b04a20ebadbb3be2f439c63a5257c0c07](https://github.com/dOpensource/dsiprouter/commit/8fe5ea6b04a20ebadbb3be2f439c63a5257c0c07)  \n> Date: Fri, 6 Apr 2018 03:34:32 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8fe5ea6b04a20ebadbb3be2f439c63a5257c0c07)\n[//]: # (START_SECTION b634c0cc239500f8420d84c2751db1b1a44b6fb8)\n### Added support for support LCR from a Kamailio prespective\n\n> Commit: [b634c0cc239500f8420d84c2751db1b1a44b6fb8](https://github.com/dOpensource/dsiprouter/commit/b634c0cc239500f8420d84c2751db1b1a44b6fb8)  \n> Date: Thu, 5 Apr 2018 05:01:00 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b634c0cc239500f8420d84c2751db1b1a44b6fb8)\n[//]: # (START_SECTION 63da09ec9d97a23a789d99601de00f8445a6ece6)\n### add header check feature in teleblock route\n\n> Commit: [63da09ec9d97a23a789d99601de00f8445a6ece6](https://github.com/dOpensource/dsiprouter/commit/63da09ec9d97a23a789d99601de00f8445a6ece6)  \n> Date: Sun, 1 Apr 2018 21:32:16 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 63da09ec9d97a23a789d99601de00f8445a6ece6)\n[//]: # (START_SECTION 321e790f57468ae335d2a0fd321c441f3100f38a)\n### add current work on dynamic routing and LCR features\n\n> Commit: [321e790f57468ae335d2a0fd321c441f3100f38a](https://github.com/dOpensource/dsiprouter/commit/321e790f57468ae335d2a0fd321c441f3100f38a)  \n> Date: Sun, 1 Apr 2018 21:03:13 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 321e790f57468ae335d2a0fd321c441f3100f38a)\n[//]: # (START_SECTION 87742ca2d658a60048d1c61f8d410b625021af7d)\n### reformat messy code, fix html errors throughout, complete overhaul of front-end, add multiple outbound routes feature added, started adding backend capablities for dynamic routing, fixed 200 reply bug (endpoint now waits for 200 from carrier)\n\n> Commit: [87742ca2d658a60048d1c61f8d410b625021af7d](https://github.com/dOpensource/dsiprouter/commit/87742ca2d658a60048d1c61f8d410b625021af7d)  \n> Date: Tue, 27 Mar 2018 20:01:23 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 87742ca2d658a60048d1c61f8d410b625021af7d)\n[//]: # (START_SECTION fbaa02fd24237c9c4d809f64581696b35784943b)\n### Fixed issue with rtpengine not starting after installation\n\n> Commit: [fbaa02fd24237c9c4d809f64581696b35784943b](https://github.com/dOpensource/dsiprouter/commit/fbaa02fd24237c9c4d809f64581696b35784943b)  \n> Date: Sat, 24 Mar 2018 22:43:13 -0400  \n> Author: root (root@dsiprouter.dopensource.com)  \n> Committer: root (root@dsiprouter.dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fbaa02fd24237c9c4d809f64581696b35784943b)\n[//]: # (START_SECTION bc813f49869bc621d16585d7648ae5e2c4bfe81a)\n### Fixed typo with VI carriers\n\n> Commit: [bc813f49869bc621d16585d7648ae5e2c4bfe81a](https://github.com/dOpensource/dsiprouter/commit/bc813f49869bc621d16585d7648ae5e2c4bfe81a)  \n> Date: Sat, 24 Mar 2018 20:03:01 -0400  \n> Author: root (root@dsiprouter.dopensource.com)  \n> Committer: root (root@dsiprouter.dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bc813f49869bc621d16585d7648ae5e2c4bfe81a)\n[//]: # (START_SECTION 1a6728d9a5a2bc8e6f3febf66414c4dd6562df11)\n### Added a fix to resolve firewall issues\n\n> Commit: [1a6728d9a5a2bc8e6f3febf66414c4dd6562df11](https://github.com/dOpensource/dsiprouter/commit/1a6728d9a5a2bc8e6f3febf66414c4dd6562df11)  \n> Date: Sat, 24 Mar 2018 19:59:13 -0400  \n> Author: root (root@dsiprouter.dopensource.com)  \n> Committer: root (root@dsiprouter.dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1a6728d9a5a2bc8e6f3febf66414c4dd6562df11)\n[//]: # (START_SECTION 4389700a37b7c41741ad9e6f25743072c3b41889)\n### Fixed an issue that prevented port 5060 from being added and removed during the install and uninstall process, respectively\n\n> Commit: [4389700a37b7c41741ad9e6f25743072c3b41889](https://github.com/dOpensource/dsiprouter/commit/4389700a37b7c41741ad9e6f25743072c3b41889)  \n> Date: Sat, 24 Mar 2018 18:52:49 -0400  \n> Author: root (root@dsiprouter.dopensource.com)  \n> Committer: root (root@dsiprouter.dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4389700a37b7c41741ad9e6f25743072c3b41889)\n[//]: # (START_SECTION 3c3ba20ad9f923cfd9be6fec74a5100fcee432ed)\n### fixed uninstall cmd, add support for debian jessie dsiprouter installation\n\n> Commit: [3c3ba20ad9f923cfd9be6fec74a5100fcee432ed](https://github.com/dOpensource/dsiprouter/commit/3c3ba20ad9f923cfd9be6fec74a5100fcee432ed)  \n> Date: Sat, 24 Mar 2018 02:17:16 +0000  \n> Author: root (root@packer-debian-8-amd64.droplet.local)  \n> Committer: root (root@packer-debian-8-amd64.droplet.local)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3c3ba20ad9f923cfd9be6fec74a5100fcee432ed)\n[//]: # (START_SECTION 0cebcae69d28faebb58e1f37f9c9c16ee8b097b1)\n### Fixed issues with Deb 8.9 installer\n\n> Commit: [0cebcae69d28faebb58e1f37f9c9c16ee8b097b1](https://github.com/dOpensource/dsiprouter/commit/0cebcae69d28faebb58e1f37f9c9c16ee8b097b1)  \n> Date: Sat, 24 Mar 2018 12:55:35 +1100  \n> Author: root (root@debian.vixtel.com.au)  \n> Committer: root (root@debian.vixtel.com.au)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0cebcae69d28faebb58e1f37f9c9c16ee8b097b1)\n[//]: # (START_SECTION f3bd49a0c980055d373007a7eca60aa24aa1464c)\n### Fixed issues with Deb 8.9 installer\n\n> Commit: [f3bd49a0c980055d373007a7eca60aa24aa1464c](https://github.com/dOpensource/dsiprouter/commit/f3bd49a0c980055d373007a7eca60aa24aa1464c)  \n> Date: Sat, 24 Mar 2018 12:54:22 +1100  \n> Author: root (root@debian.vixtel.com.au)  \n> Committer: root (root@debian.vixtel.com.au)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f3bd49a0c980055d373007a7eca60aa24aa1464c)\n[//]: # (START_SECTION a72d5ace87979b2b1647ef627be4ea8cc40d562d)\n### fix broken debian jessie installation issues\n\n> Commit: [a72d5ace87979b2b1647ef627be4ea8cc40d562d](https://github.com/dOpensource/dsiprouter/commit/a72d5ace87979b2b1647ef627be4ea8cc40d562d)  \n> Date: Sat, 24 Mar 2018 01:16:53 +0000  \n> Author: root (root@packer-debian-8-amd64.droplet.local)  \n> Committer: root (root@packer-debian-8-amd64.droplet.local)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a72d5ace87979b2b1647ef627be4ea8cc40d562d)\n[//]: # (START_SECTION 3f6ea6ed37ee0948cfa574897261f181ab0af0f8)\n### Update README.md\n\n> Commit: [3f6ea6ed37ee0948cfa574897261f181ab0af0f8](https://github.com/dOpensource/dsiprouter/commit/3f6ea6ed37ee0948cfa574897261f181ab0af0f8)  \n> Date: Fri, 23 Mar 2018 06:31:35 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3f6ea6ed37ee0948cfa574897261f181ab0af0f8)\n[//]: # (START_SECTION 3f8691614b484f0060b72cf67442bc1a1b79844b)\n### Update README.md\n\n> Commit: [3f8691614b484f0060b72cf67442bc1a1b79844b](https://github.com/dOpensource/dsiprouter/commit/3f8691614b484f0060b72cf67442bc1a1b79844b)  \n> Date: Fri, 23 Mar 2018 06:30:27 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3f8691614b484f0060b72cf67442bc1a1b79844b)\n[//]: # (START_SECTION f19f87d7d4325eedb4a266db47cf9a495f6e1ca2)\n### Update README.md\n\n> Commit: [f19f87d7d4325eedb4a266db47cf9a495f6e1ca2](https://github.com/dOpensource/dsiprouter/commit/f19f87d7d4325eedb4a266db47cf9a495f6e1ca2)  \n> Date: Fri, 23 Mar 2018 06:30:03 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f19f87d7d4325eedb4a266db47cf9a495f6e1ca2)\n[//]: # (START_SECTION fa9806ab3573a9c353a222da85b041fcc1d87a7c)\n### Update README.md\n\n> Commit: [fa9806ab3573a9c353a222da85b041fcc1d87a7c](https://github.com/dOpensource/dsiprouter/commit/fa9806ab3573a9c353a222da85b041fcc1d87a7c)  \n> Date: Fri, 23 Mar 2018 06:25:38 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fa9806ab3573a9c353a222da85b041fcc1d87a7c)\n[//]: # (START_SECTION 7f96b230c12f91c3f83238608fb07ff53a32c08c)\n### Updated README and validated the install on Debian 9.4 (Stretch)\n\n> Commit: [7f96b230c12f91c3f83238608fb07ff53a32c08c](https://github.com/dOpensource/dsiprouter/commit/7f96b230c12f91c3f83238608fb07ff53a32c08c)  \n> Date: Fri, 23 Mar 2018 06:15:44 -0400  \n> Author: root (root@dsiprouter.dopensource.com)  \n> Committer: root (root@dsiprouter.dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7f96b230c12f91c3f83238608fb07ff53a32c08c)\n[//]: # (START_SECTION 461c71c97f1e017f0f8827570d5f95f2c741bcef)\n### Fixed the installer issues for Debian 9.x (stretch)\n\n> Commit: [461c71c97f1e017f0f8827570d5f95f2c741bcef](https://github.com/dOpensource/dsiprouter/commit/461c71c97f1e017f0f8827570d5f95f2c741bcef)  \n> Date: Fri, 23 Mar 2018 05:19:52 -0400  \n> Author: root (root@dsiprouter-kam5.dopensource.com)  \n> Committer: root (root@dsiprouter-kam5.dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 461c71c97f1e017f0f8827570d5f95f2c741bcef)\n[//]: # (START_SECTION 51f8a8cad4f6faae2f98ca47f47de6670b765786)\n### Fixed RTPProxy issue with Debian\n\n> Commit: [51f8a8cad4f6faae2f98ca47f47de6670b765786](https://github.com/dOpensource/dsiprouter/commit/51f8a8cad4f6faae2f98ca47f47de6670b765786)  \n> Date: Thu, 22 Mar 2018 00:00:53 -0400  \n> Author: root (release@dopensource.com)  \n> Committer: root (release@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 51f8a8cad4f6faae2f98ca47f47de6670b765786)\n[//]: # (START_SECTION 6b4608c72aa90e30910087cff27b192f18e7c9d1)\n### Fixed a missing curly brackets\n\n> Commit: [6b4608c72aa90e30910087cff27b192f18e7c9d1](https://github.com/dOpensource/dsiprouter/commit/6b4608c72aa90e30910087cff27b192f18e7c9d1)  \n> Date: Tue, 20 Mar 2018 22:40:20 -0400  \n> Author: root (release@dopensource.com)  \n> Committer: root (release@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6b4608c72aa90e30910087cff27b192f18e7c9d1)\n[//]: # (START_SECTION e682af00fb9e952212cb0eaf63dd5d9b7d2a420b)\n### Fixed a bug with teleblock media enablement\n\n> Commit: [e682af00fb9e952212cb0eaf63dd5d9b7d2a420b](https://github.com/dOpensource/dsiprouter/commit/e682af00fb9e952212cb0eaf63dd5d9b7d2a420b)  \n> Date: Tue, 20 Mar 2018 17:42:13 -0600  \n> Author: root (mack@dopensource.com)  \n> Committer: root (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e682af00fb9e952212cb0eaf63dd5d9b7d2a420b)\n[//]: # (START_SECTION 17b89eea8920d9feec83e8ebbcad6c5063a6a608)\n### Fixed a bug that prevented the media server from being enabled\n\n> Commit: [17b89eea8920d9feec83e8ebbcad6c5063a6a608](https://github.com/dOpensource/dsiprouter/commit/17b89eea8920d9feec83e8ebbcad6c5063a6a608)  \n> Date: Tue, 20 Mar 2018 16:44:30 -0600  \n> Author: root (mack@dopensource.com)  \n> Committer: root (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 17b89eea8920d9feec83e8ebbcad6c5063a6a608)\n[//]: # (START_SECTION 864cb5b4b1b86e05dce86410deb6971af9ec9d53)\n### Fixed the default settings in the Kam 4.4 version of the configuration file\n\n> Commit: [864cb5b4b1b86e05dce86410deb6971af9ec9d53](https://github.com/dOpensource/dsiprouter/commit/864cb5b4b1b86e05dce86410deb6971af9ec9d53)  \n> Date: Tue, 20 Mar 2018 04:48:01 -0600  \n> Author: root (mack@dopensource.com)  \n> Committer: root (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 864cb5b4b1b86e05dce86410deb6971af9ec9d53)\n[//]: # (START_SECTION 093f4b1b000707456dd95dca2d21b34aa0090af4)\n### Changed the port back to the default 5000\n\n> Commit: [093f4b1b000707456dd95dca2d21b34aa0090af4](https://github.com/dOpensource/dsiprouter/commit/093f4b1b000707456dd95dca2d21b34aa0090af4)  \n> Date: Tue, 20 Mar 2018 04:23:55 -0600  \n> Author: root (mack@dopensource.com)  \n> Committer: root (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 093f4b1b000707456dd95dca2d21b34aa0090af4)\n[//]: # (START_SECTION 574f0d60efa09a5f736ab0cc096ba30114ce5b7d)\n### Update settings.py\n\n> Commit: [574f0d60efa09a5f736ab0cc096ba30114ce5b7d](https://github.com/dOpensource/dsiprouter/commit/574f0d60efa09a5f736ab0cc096ba30114ce5b7d)  \n> Date: Mon, 19 Mar 2018 06:01:04 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 574f0d60efa09a5f736ab0cc096ba30114ce5b7d)\n[//]: # (START_SECTION ee9b1c68d5c3770a80c178d0c967ce55f299d096)\n### Update README.md\n\n> Commit: [ee9b1c68d5c3770a80c178d0c967ce55f299d096](https://github.com/dOpensource/dsiprouter/commit/ee9b1c68d5c3770a80c178d0c967ce55f299d096)  \n> Date: Mon, 19 Mar 2018 06:00:25 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ee9b1c68d5c3770a80c178d0c967ce55f299d096)\n[//]: # (START_SECTION a5a92ac439283a2876d39bce2e38348441451c99)\n### Update README.md\n\n> Commit: [a5a92ac439283a2876d39bce2e38348441451c99](https://github.com/dOpensource/dsiprouter/commit/a5a92ac439283a2876d39bce2e38348441451c99)  \n> Date: Mon, 19 Mar 2018 05:57:05 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a5a92ac439283a2876d39bce2e38348441451c99)\n[//]: # (START_SECTION 6f7ec9398b602574396786567bb735ff00d954f8)\n### Add files via upload\n\n> Commit: [6f7ec9398b602574396786567bb735ff00d954f8](https://github.com/dOpensource/dsiprouter/commit/6f7ec9398b602574396786567bb735ff00d954f8)  \n> Date: Mon, 19 Mar 2018 05:53:56 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6f7ec9398b602574396786567bb735ff00d954f8)\n[//]: # (START_SECTION 270505741eaf8f7325be7ab3277c76abf58e9045)\n### Completed support for Teleblock Service\n\n> Commit: [270505741eaf8f7325be7ab3277c76abf58e9045](https://github.com/dOpensource/dsiprouter/commit/270505741eaf8f7325be7ab3277c76abf58e9045)  \n> Date: Mon, 19 Mar 2018 09:51:06 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 270505741eaf8f7325be7ab3277c76abf58e9045)\n[//]: # (START_SECTION a8b26db33bfb995c2f8298654c183eac11174b17)\n### Added GUI Support for Gryphon Teleblock Support\n\n> Commit: [a8b26db33bfb995c2f8298654c183eac11174b17](https://github.com/dOpensource/dsiprouter/commit/a8b26db33bfb995c2f8298654c183eac11174b17)  \n> Date: Sun, 18 Mar 2018 13:30:25 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a8b26db33bfb995c2f8298654c183eac11174b17)\n[//]: # (START_SECTION bac4034ad784e796bc013d874a874e0c38777bcd)\n### Create CNAME\n\n> Commit: [bac4034ad784e796bc013d874a874e0c38777bcd](https://github.com/dOpensource/dsiprouter/commit/bac4034ad784e796bc013d874a874e0c38777bcd)  \n> Date: Sat, 17 Mar 2018 20:04:06 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bac4034ad784e796bc013d874a874e0c38777bcd)\n[//]: # (START_SECTION e36449722cb15fbf56b65179c8ea6793ec8be906)\n### Set theme jekyll-theme-architect\n\n> Commit: [e36449722cb15fbf56b65179c8ea6793ec8be906](https://github.com/dOpensource/dsiprouter/commit/e36449722cb15fbf56b65179c8ea6793ec8be906)  \n> Date: Sat, 17 Mar 2018 19:51:32 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e36449722cb15fbf56b65179c8ea6793ec8be906)\n[//]: # (START_SECTION 47333b5cde16838ba7c4a8c64e99445f53c4e7f3)\n### Removed a legacy script for stopping dsiprouter\n\n> Commit: [47333b5cde16838ba7c4a8c64e99445f53c4e7f3](https://github.com/dOpensource/dsiprouter/commit/47333b5cde16838ba7c4a8c64e99445f53c4e7f3)  \n> Date: Sat, 17 Mar 2018 14:50:44 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 47333b5cde16838ba7c4a8c64e99445f53c4e7f3)\n[//]: # (START_SECTION 4d7d4e84888eeeefd12f3692320badb19040fe4d)\n### Added support for Teleblock\n\n> Commit: [4d7d4e84888eeeefd12f3692320badb19040fe4d](https://github.com/dOpensource/dsiprouter/commit/4d7d4e84888eeeefd12f3692320badb19040fe4d)  \n> Date: Sat, 17 Mar 2018 14:48:59 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4d7d4e84888eeeefd12f3692320badb19040fe4d)\n[//]: # (START_SECTION 6117b3fc5b30dcec0e3a162d6df5f976db6d1bdb)\n### got rid of uneeded replies, fixed formatting\n\n> Commit: [6117b3fc5b30dcec0e3a162d6df5f976db6d1bdb](https://github.com/dOpensource/dsiprouter/commit/6117b3fc5b30dcec0e3a162d6df5f976db6d1bdb)  \n> Date: Wed, 14 Mar 2018 14:48:09 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6117b3fc5b30dcec0e3a162d6df5f976db6d1bdb)\n[//]: # (START_SECTION 0ad302940d9e883763df68fe40fbb560aaaaccd6)\n### fixed the \"500\" reply bug and check status bug\n\n> Commit: [0ad302940d9e883763df68fe40fbb560aaaaccd6](https://github.com/dOpensource/dsiprouter/commit/0ad302940d9e883763df68fe40fbb560aaaaccd6)  \n> Date: Tue, 13 Mar 2018 15:30:54 -0400  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0ad302940d9e883763df68fe40fbb560aaaaccd6)\n[//]: # (START_SECTION c3f8fe6d4d980230dc9a32b6d20079c86bcfdd51)\n### Update kamailio51_dsiprouter.cfg\n\n> Commit: [c3f8fe6d4d980230dc9a32b6d20079c86bcfdd51](https://github.com/dOpensource/dsiprouter/commit/c3f8fe6d4d980230dc9a32b6d20079c86bcfdd51)  \n> Date: Mon, 12 Mar 2018 21:04:00 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c3f8fe6d4d980230dc9a32b6d20079c86bcfdd51)\n[//]: # (START_SECTION eec8c7ae6a3c49b1fd7a58fa1edb4e0a4fbdaef8)\n### Update kamailio51_dsiprouter.cfg\n\n> Commit: [eec8c7ae6a3c49b1fd7a58fa1edb4e0a4fbdaef8](https://github.com/dOpensource/dsiprouter/commit/eec8c7ae6a3c49b1fd7a58fa1edb4e0a4fbdaef8)  \n> Date: Mon, 12 Mar 2018 21:03:30 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION eec8c7ae6a3c49b1fd7a58fa1edb4e0a4fbdaef8)\n[//]: # (START_SECTION 97e3ea8d6762476226e00e110d8a33e5630a3a6f)\n### Update stretch.sh\n\n> Commit: [97e3ea8d6762476226e00e110d8a33e5630a3a6f](https://github.com/dOpensource/dsiprouter/commit/97e3ea8d6762476226e00e110d8a33e5630a3a6f)  \n> Date: Sun, 11 Mar 2018 21:55:28 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 97e3ea8d6762476226e00e110d8a33e5630a3a6f)\n[//]: # (START_SECTION 5f36971c01d9a3bc52ee999454c0199eb27a7ff8)\n### Update README.md\n\n> Commit: [5f36971c01d9a3bc52ee999454c0199eb27a7ff8](https://github.com/dOpensource/dsiprouter/commit/5f36971c01d9a3bc52ee999454c0199eb27a7ff8)  \n> Date: Sun, 11 Mar 2018 21:43:45 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5f36971c01d9a3bc52ee999454c0199eb27a7ff8)\n[//]: # (START_SECTION b07a5e065a16755cba48d638d72b28c2bd111dac)\n### Update README.md\n\n> Commit: [b07a5e065a16755cba48d638d72b28c2bd111dac](https://github.com/dOpensource/dsiprouter/commit/b07a5e065a16755cba48d638d72b28c2bd111dac)  \n> Date: Sun, 11 Mar 2018 21:34:51 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b07a5e065a16755cba48d638d72b28c2bd111dac)\n[//]: # (START_SECTION dbec72854ac57ee4f2937b09eedf373bd49e6e19)\n### Update README.md\n\n> Commit: [dbec72854ac57ee4f2937b09eedf373bd49e6e19](https://github.com/dOpensource/dsiprouter/commit/dbec72854ac57ee4f2937b09eedf373bd49e6e19)  \n> Date: Sun, 11 Mar 2018 21:29:02 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dbec72854ac57ee4f2937b09eedf373bd49e6e19)\n[//]: # (START_SECTION 4b5a7621be2f99cb6ba75476ea9299e2155b9d16)\n### Update README.md\n\n> Commit: [4b5a7621be2f99cb6ba75476ea9299e2155b9d16](https://github.com/dOpensource/dsiprouter/commit/4b5a7621be2f99cb6ba75476ea9299e2155b9d16)  \n> Date: Sun, 11 Mar 2018 21:27:56 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4b5a7621be2f99cb6ba75476ea9299e2155b9d16)\n[//]: # (START_SECTION 4278d5b249e8f0302866777e10d57ceff32945cc)\n### Add files via upload\n\n> Commit: [4278d5b249e8f0302866777e10d57ceff32945cc](https://github.com/dOpensource/dsiprouter/commit/4278d5b249e8f0302866777e10d57ceff32945cc)  \n> Date: Sun, 11 Mar 2018 21:26:05 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4278d5b249e8f0302866777e10d57ceff32945cc)\n[//]: # (START_SECTION 5d6cbfff711d6721af44702d9baee487078d77e3)\n### Updated the README\n\n> Commit: [5d6cbfff711d6721af44702d9baee487078d77e3](https://github.com/dOpensource/dsiprouter/commit/5d6cbfff711d6721af44702d9baee487078d77e3)  \n> Date: Mon, 12 Mar 2018 01:20:38 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5d6cbfff711d6721af44702d9baee487078d77e3)\n[//]: # (START_SECTION a64b491d710f1258e6b1fd58772726a459ac8e91)\n### Prevent the DBROOTPW from being prompted during an install on a fresh machine\n\n> Commit: [a64b491d710f1258e6b1fd58772726a459ac8e91](https://github.com/dOpensource/dsiprouter/commit/a64b491d710f1258e6b1fd58772726a459ac8e91)  \n> Date: Mon, 12 Mar 2018 00:53:19 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a64b491d710f1258e6b1fd58772726a459ac8e91)\n[//]: # (START_SECTION 14f87963c736094ad35c8e0908514670b2111774)\n### Completed GUI support for PBX Registration\n\n> Commit: [14f87963c736094ad35c8e0908514670b2111774](https://github.com/dOpensource/dsiprouter/commit/14f87963c736094ad35c8e0908514670b2111774)  \n> Date: Mon, 12 Mar 2018 00:29:14 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 14f87963c736094ad35c8e0908514670b2111774)\n[//]: # (START_SECTION a8f0df2a88064d31ed445a05e032c69ca490fc82)\n### Fixed the Add PBX with subscriber support\n\n> Commit: [a8f0df2a88064d31ed445a05e032c69ca490fc82](https://github.com/dOpensource/dsiprouter/commit/a8f0df2a88064d31ed445a05e032c69ca490fc82)  \n> Date: Sun, 11 Mar 2018 14:30:30 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a8f0df2a88064d31ed445a05e032c69ca490fc82)\n[//]: # (START_SECTION bb9adc17826e2b4df40255f3c7efd366541e1795)\n### added teleblock blacklisting feature\n\n> Commit: [bb9adc17826e2b4df40255f3c7efd366541e1795](https://github.com/dOpensource/dsiprouter/commit/bb9adc17826e2b4df40255f3c7efd366541e1795)  \n> Date: Fri, 9 Mar 2018 22:03:46 -0500  \n> Author: Tyler Moore (tmoore@goflyball.com)  \n> Committer: Tyler Moore (tmoore@goflyball.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bb9adc17826e2b4df40255f3c7efd366541e1795)\n[//]: # (START_SECTION 088ef02a18ffd0c57fbcc5565358905c624a3dda)\n### Added GUI support for allowing a PBX/Endpoint to register\n\n> Commit: [088ef02a18ffd0c57fbcc5565358905c624a3dda](https://github.com/dOpensource/dsiprouter/commit/088ef02a18ffd0c57fbcc5565358905c624a3dda)  \n> Date: Wed, 7 Mar 2018 05:41:00 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 088ef02a18ffd0c57fbcc5565358905c624a3dda)\n[//]: # (START_SECTION 0c689b9e7d08a64c320d4769ff6d312ec61f1b00)\n### Completed Kamailio support to allow PBX's to register to dSIPRouter\n\n> Commit: [0c689b9e7d08a64c320d4769ff6d312ec61f1b00](https://github.com/dOpensource/dsiprouter/commit/0c689b9e7d08a64c320d4769ff6d312ec61f1b00)  \n> Date: Mon, 5 Mar 2018 03:25:26 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0c689b9e7d08a64c320d4769ff6d312ec61f1b00)\n[//]: # (START_SECTION 94fc56e69265f0d61d0cb7e896335d017b77747b)\n### Added support to allow PBX's to register\n\n> Commit: [94fc56e69265f0d61d0cb7e896335d017b77747b](https://github.com/dOpensource/dsiprouter/commit/94fc56e69265f0d61d0cb7e896335d017b77747b)  \n> Date: Sat, 3 Mar 2018 16:56:11 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 94fc56e69265f0d61d0cb7e896335d017b77747b)\n[//]: # (START_SECTION 0c5292d265bb33dd3be8b6d217fdfbdb0ad7bcd9)\n### Added curl to the packages that needs to tbe downloaded.  Also fixed issue with the dSIPRouter port not being added\n\n> Commit: [0c5292d265bb33dd3be8b6d217fdfbdb0ad7bcd9](https://github.com/dOpensource/dsiprouter/commit/0c5292d265bb33dd3be8b6d217fdfbdb0ad7bcd9)  \n> Date: Fri, 2 Mar 2018 05:08:18 +0000  \n> Author: root (root@disrouter-kam5-dev2.localdomain)  \n> Committer: root (root@disrouter-kam5-dev2.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0c5292d265bb33dd3be8b6d217fdfbdb0ad7bcd9)\n[//]: # (START_SECTION 35989c8bb8226c0d15bf02cd07ab23a5237d6eff)\n### Fixed issues with install script\n\n> Commit: [35989c8bb8226c0d15bf02cd07ab23a5237d6eff](https://github.com/dOpensource/dsiprouter/commit/35989c8bb8226c0d15bf02cd07ab23a5237d6eff)  \n> Date: Fri, 2 Mar 2018 04:40:16 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 35989c8bb8226c0d15bf02cd07ab23a5237d6eff)\n[//]: # (START_SECTION fb1385b87914ddc040630bbe0d10c5a40bdc8b99)\n### Fixed and validated the debian stretch install\n\n> Commit: [fb1385b87914ddc040630bbe0d10c5a40bdc8b99](https://github.com/dOpensource/dsiprouter/commit/fb1385b87914ddc040630bbe0d10c5a40bdc8b99)  \n> Date: Fri, 2 Mar 2018 01:43:22 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fb1385b87914ddc040630bbe0d10c5a40bdc8b99)\n[//]: # (START_SECTION d9a3d2dc612cd1c5956ae98bb9393c4b58f94653)\n### Refactoring the install script into more maintainable and testable units\n\n> Commit: [d9a3d2dc612cd1c5956ae98bb9393c4b58f94653](https://github.com/dOpensource/dsiprouter/commit/d9a3d2dc612cd1c5956ae98bb9393c4b58f94653)  \n> Date: Sun, 25 Feb 2018 07:58:28 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d9a3d2dc612cd1c5956ae98bb9393c4b58f94653)\n[//]: # (START_SECTION 9caa328dcaa35b751c48f2e2197ce92104cdf132)\n### Fixed issues with the Stretch install\n\n> Commit: [9caa328dcaa35b751c48f2e2197ce92104cdf132](https://github.com/dOpensource/dsiprouter/commit/9caa328dcaa35b751c48f2e2197ce92104cdf132)  \n> Date: Sat, 24 Feb 2018 22:06:10 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9caa328dcaa35b751c48f2e2197ce92104cdf132)\n[//]: # (START_SECTION 3fb6f45c7a48a148cfc94afd4475e90a8d56f15f)\n### Adding support for Debian Stretch release\n\n> Commit: [3fb6f45c7a48a148cfc94afd4475e90a8d56f15f](https://github.com/dOpensource/dsiprouter/commit/3fb6f45c7a48a148cfc94afd4475e90a8d56f15f)  \n> Date: Sat, 24 Feb 2018 20:40:50 +0000  \n> Author: root (root@dsiprouter-kam5.localdomain)  \n> Committer: root (root@dsiprouter-kam5.localdomain)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3fb6f45c7a48a148cfc94afd4475e90a8d56f15f)\n[//]: # (START_SECTION 1bbb6616fbf5393a984bda57e2d7423d15e06d7f)\n### Update README.md\n\n> Commit: [1bbb6616fbf5393a984bda57e2d7423d15e06d7f](https://github.com/dOpensource/dsiprouter/commit/1bbb6616fbf5393a984bda57e2d7423d15e06d7f)  \n> Date: Sat, 24 Feb 2018 11:56:30 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1bbb6616fbf5393a984bda57e2d7423d15e06d7f)\n[//]: # (START_SECTION 8c00bda7dbc94eae694acb5e0204c8b12eec633a)\n### Fixed an issue that prevented Kamailio 4.4 from being installed\n\n> Commit: [8c00bda7dbc94eae694acb5e0204c8b12eec633a](https://github.com/dOpensource/dsiprouter/commit/8c00bda7dbc94eae694acb5e0204c8b12eec633a)  \n> Date: Tue, 19 Dec 2017 14:50:24 -0500  \n> Author: root (root@debian89)  \n> Committer: root (root@debian89)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8c00bda7dbc94eae694acb5e0204c8b12eec633a)\n[//]: # (START_SECTION c49f1656c78bfe96ea2c562f3bba3f80e294157f)\n### Update README.md\n\n> Commit: [c49f1656c78bfe96ea2c562f3bba3f80e294157f](https://github.com/dOpensource/dsiprouter/commit/c49f1656c78bfe96ea2c562f3bba3f80e294157f)  \n> Date: Mon, 18 Dec 2017 20:48:57 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c49f1656c78bfe96ea2c562f3bba3f80e294157f)\n[//]: # (START_SECTION 9da44b990faf882f0988bb419456fa7a466e3d67)\n### Removed debugging statements from bash scripts and made kamailio restart after the dSIPRouter install\n\n> Commit: [9da44b990faf882f0988bb419456fa7a466e3d67](https://github.com/dOpensource/dsiprouter/commit/9da44b990faf882f0988bb419456fa7a466e3d67)  \n> Date: Tue, 19 Dec 2017 01:41:58 +0000  \n> Author: root (root@packer-debian-8-amd64.droplet.local)  \n> Committer: root (root@packer-debian-8-amd64.droplet.local)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9da44b990faf882f0988bb419456fa7a466e3d67)\n[//]: # (START_SECTION 882cb7a3eac6de74cc913ce9fdc898c8fe0728b2)\n### Added logic to handle different versios of Kamailio\n\n> Commit: [882cb7a3eac6de74cc913ce9fdc898c8fe0728b2](https://github.com/dOpensource/dsiprouter/commit/882cb7a3eac6de74cc913ce9fdc898c8fe0728b2)  \n> Date: Tue, 19 Dec 2017 01:26:04 +0000  \n> Author: root (root@packer-debian-8-amd64.droplet.local)  \n> Committer: root (root@packer-debian-8-amd64.droplet.local)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 882cb7a3eac6de74cc913ce9fdc898c8fe0728b2)\n[//]: # (START_SECTION 2da11a5f987d00e235ec6b7d7ea5d072b6a36198)\n### fixed the install the uninstall scripts\n\n> Commit: [2da11a5f987d00e235ec6b7d7ea5d072b6a36198](https://github.com/dOpensource/dsiprouter/commit/2da11a5f987d00e235ec6b7d7ea5d072b6a36198)  \n> Date: Tue, 19 Dec 2017 00:28:13 +0000  \n> Author: root (root@packer-debian-8-amd64.droplet.local)  \n> Committer: root (root@packer-debian-8-amd64.droplet.local)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2da11a5f987d00e235ec6b7d7ea5d072b6a36198)\n[//]: # (START_SECTION afb668cb39cf623379989209a3f6cf4584eae71b)\n### Update README.md\n\n> Commit: [afb668cb39cf623379989209a3f6cf4584eae71b](https://github.com/dOpensource/dsiprouter/commit/afb668cb39cf623379989209a3f6cf4584eae71b)  \n> Date: Mon, 18 Dec 2017 19:00:16 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION afb668cb39cf623379989209a3f6cf4584eae71b)\n[//]: # (START_SECTION 224b098a0dfc5adf8da010713ae55588d411c011)\n### added support for installing kamailio on debian\n\n> Commit: [224b098a0dfc5adf8da010713ae55588d411c011](https://github.com/dOpensource/dsiprouter/commit/224b098a0dfc5adf8da010713ae55588d411c011)  \n> Date: Mon, 18 Dec 2017 23:56:06 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 224b098a0dfc5adf8da010713ae55588d411c011)\n[//]: # (START_SECTION b7c3abf5004ccda474507dd39c0c52d579186584)\n### Correct reference to REQ_PYTHON_MAJOR_VER\n\n> Commit: [b7c3abf5004ccda474507dd39c0c52d579186584](https://github.com/dOpensource/dsiprouter/commit/b7c3abf5004ccda474507dd39c0c52d579186584)  \n> Date: Sun, 10 Dec 2017 09:55:02 -0500  \n> Author: hailthemelody (rainman@hailthemelody.com)  \n> Committer: hailthemelody (rainman@hailthemelody.com)  \n> Signed:   \n\n\n- Was pointing to REQ_PYTHON_VER, which presumable was the previous name of the variable\n\n\n---\n\n[//]: # (END_SECTION b7c3abf5004ccda474507dd39c0c52d579186584)\n[//]: # (START_SECTION 63496f31a39d0bbfc35460259d90e4c5053c1db5)\n### Correct reference to variable\n\n> Commit: [63496f31a39d0bbfc35460259d90e4c5053c1db5](https://github.com/dOpensource/dsiprouter/commit/63496f31a39d0bbfc35460259d90e4c5053c1db5)  \n> Date: Sun, 10 Dec 2017 08:44:47 -0500  \n> Author: hailthemelody (rainman@hailthemelody.com)  \n> Committer: hailthemelody (rainman@hailthemelody.com)  \n> Signed:   \n\n\n- Was missing \"$\" and being displayed as text. Now resolves to variable\n\n\n---\n\n[//]: # (END_SECTION 63496f31a39d0bbfc35460259d90e4c5053c1db5)\n[//]: # (START_SECTION 52189aced1a306c35cd2ad4af6db8826aa879837)\n### update the version from 0.30 to 0.31\n\n> Commit: [52189aced1a306c35cd2ad4af6db8826aa879837](https://github.com/dOpensource/dsiprouter/commit/52189aced1a306c35cd2ad4af6db8826aa879837)  \n> Date: Mon, 4 Dec 2017 12:12:24 +0000  \n> Author: root (root@packer-debian-8-amd64.droplet.local)  \n> Committer: root (root@packer-debian-8-amd64.droplet.local)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 52189aced1a306c35cd2ad4af6db8826aa879837)\n[//]: # (START_SECTION 51e91ee29e272e8d6af2854b46ff54096b942108)\n### Update README.md\n\n> Commit: [51e91ee29e272e8d6af2854b46ff54096b942108](https://github.com/dOpensource/dsiprouter/commit/51e91ee29e272e8d6af2854b46ff54096b942108)  \n> Date: Mon, 4 Dec 2017 07:09:50 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 51e91ee29e272e8d6af2854b46ff54096b942108)\n[//]: # (START_SECTION 669cf794d179668c1176d0f7b3e9af0cc266187a)\n### Update README.md\n\n> Commit: [669cf794d179668c1176d0f7b3e9af0cc266187a](https://github.com/dOpensource/dsiprouter/commit/669cf794d179668c1176d0f7b3e9af0cc266187a)  \n> Date: Mon, 4 Dec 2017 07:07:36 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 669cf794d179668c1176d0f7b3e9af0cc266187a)\n[//]: # (START_SECTION 9c8444ae737cab7a94bd62edb0e06a70699cdad1)\n### Fixed some minor bugs and formatting issues\n\n> Commit: [9c8444ae737cab7a94bd62edb0e06a70699cdad1](https://github.com/dOpensource/dsiprouter/commit/9c8444ae737cab7a94bd62edb0e06a70699cdad1)  \n> Date: Mon, 4 Dec 2017 01:19:21 +0000  \n> Author: root (root@packer-debian-8-amd64.droplet.local)  \n> Committer: root (root@packer-debian-8-amd64.droplet.local)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9c8444ae737cab7a94bd62edb0e06a70699cdad1)\n[//]: # (START_SECTION 03c315ebbbfa321d8494f2ee5d32ad09ed2eb4a5)\n### Update README.md\n\n> Commit: [03c315ebbbfa321d8494f2ee5d32ad09ed2eb4a5](https://github.com/dOpensource/dsiprouter/commit/03c315ebbbfa321d8494f2ee5d32ad09ed2eb4a5)  \n> Date: Sun, 3 Dec 2017 17:06:14 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 03c315ebbbfa321d8494f2ee5d32ad09ed2eb4a5)\n[//]: # (START_SECTION 7bd1de71a7e6a8a4e2104fe8a92255c957f2094e)\n### Generate unique password during install\n\n> Commit: [7bd1de71a7e6a8a4e2104fe8a92255c957f2094e](https://github.com/dOpensource/dsiprouter/commit/7bd1de71a7e6a8a4e2104fe8a92255c957f2094e)  \n> Date: Sun, 3 Dec 2017 22:03:42 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7bd1de71a7e6a8a4e2104fe8a92255c957f2094e)\n[//]: # (START_SECTION 82917923a05f944ff8e283dc3846e06fb4a97b7c)\n### Added support for generating a unique password during the installation process\n\n> Commit: [82917923a05f944ff8e283dc3846e06fb4a97b7c](https://github.com/dOpensource/dsiprouter/commit/82917923a05f944ff8e283dc3846e06fb4a97b7c)  \n> Date: Sun, 3 Dec 2017 21:59:15 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 82917923a05f944ff8e283dc3846e06fb4a97b7c)\n[//]: # (START_SECTION f1c1f30dd2b9a0720f3c2821c11a65f6908e50c0)\n### restored the format of the file\n\n> Commit: [f1c1f30dd2b9a0720f3c2821c11a65f6908e50c0](https://github.com/dOpensource/dsiprouter/commit/f1c1f30dd2b9a0720f3c2821c11a65f6908e50c0)  \n> Date: Sat, 2 Dec 2017 11:58:52 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f1c1f30dd2b9a0720f3c2821c11a65f6908e50c0)\n[//]: # (START_SECTION f6912be94e6e4cf913c639398cffac55c5c96fa9)\n### restored the format of the file\n\n> Commit: [f6912be94e6e4cf913c639398cffac55c5c96fa9](https://github.com/dOpensource/dsiprouter/commit/f6912be94e6e4cf913c639398cffac55c5c96fa9)  \n> Date: Sat, 2 Dec 2017 11:56:20 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f6912be94e6e4cf913c639398cffac55c5c96fa9)\n[//]: # (START_SECTION 00538e7cc48ade86b72f17a41c4e5dd28ad6051d)\n### Fixed the reloadcmd file, but forgot to commit. Fixes #17\n\n> Commit: [00538e7cc48ade86b72f17a41c4e5dd28ad6051d](https://github.com/dOpensource/dsiprouter/commit/00538e7cc48ade86b72f17a41c4e5dd28ad6051d)  \n> Date: Sat, 2 Dec 2017 11:02:45 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 00538e7cc48ade86b72f17a41c4e5dd28ad6051d)\n[//]: # (START_SECTION 659dac168fc571efe6872ed61abfce4a5800fb2c)\n### Fixed the container padding to remove the padding on the left and right. Fixes #12\n\n> Commit: [659dac168fc571efe6872ed61abfce4a5800fb2c](https://github.com/dOpensource/dsiprouter/commit/659dac168fc571efe6872ed61abfce4a5800fb2c)  \n> Date: Sat, 2 Dec 2017 10:28:12 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 659dac168fc571efe6872ed61abfce4a5800fb2c)\n[//]: # (START_SECTION f7e26d7590574085fe389c8f8a8abfd28cb20499)\n### Enhanced the logic around reloading Kamailio from the GUI.  Thanks to @khorsmann  Fixes #17\n\n> Commit: [f7e26d7590574085fe389c8f8a8abfd28cb20499](https://github.com/dOpensource/dsiprouter/commit/f7e26d7590574085fe389c8f8a8abfd28cb20499)  \n> Date: Sat, 2 Dec 2017 09:43:40 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f7e26d7590574085fe389c8f8a8abfd28cb20499)\n[//]: # (START_SECTION 89a2de5b935125ab917af7f8107d7f93e80f7cb6)\n### Fixed an issue with the Kamailio module path not being populated properly during install.  Close #18 in release 0.31\n\n> Commit: [89a2de5b935125ab917af7f8107d7f93e80f7cb6](https://github.com/dOpensource/dsiprouter/commit/89a2de5b935125ab917af7f8107d7f93e80f7cb6)  \n> Date: Fri, 1 Dec 2017 11:36:39 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 89a2de5b935125ab917af7f8107d7f93e80f7cb6)\n[//]: # (START_SECTION cdbc3d9be43d180f90d02ba4dce6fb76c6d25774)\n### Added logic that would distinguish between local dialing and external dialing through a carrier when registering endpoints through the SIPProxy.  It's hardcoded so that extensions has to contain 5 or more digits.  Otherwise, it will try to route the call to a carrier\n\n> Commit: [cdbc3d9be43d180f90d02ba4dce6fb76c6d25774](https://github.com/dOpensource/dsiprouter/commit/cdbc3d9be43d180f90d02ba4dce6fb76c6d25774)  \n> Date: Sat, 25 Nov 2017 06:27:05 -0800  \n> Author: root (root@noc-lcb-spxy1.garlic.com)  \n> Committer: root (root@noc-lcb-spxy1.garlic.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cdbc3d9be43d180f90d02ba4dce6fb76c6d25774)\n[//]: # (START_SECTION e7a7d4b282560f435be995fe0a08f992d944bee8)\n### Fixed issue with ACK's not propagating thru the Kamailio correctedly.  Also, set the retranmission timeout to 10sec when trying to initial a call to an endpoint.\n\n> Commit: [e7a7d4b282560f435be995fe0a08f992d944bee8](https://github.com/dOpensource/dsiprouter/commit/e7a7d4b282560f435be995fe0a08f992d944bee8)  \n> Date: Wed, 22 Nov 2017 21:48:45 -0800  \n> Author: root (root@noc-lcb-spxy1.garlic.com)  \n> Committer: root (root@noc-lcb-spxy1.garlic.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e7a7d4b282560f435be995fe0a08f992d944bee8)\n[//]: # (START_SECTION 7b958fda8aa5befdb950b226214cc89a982f696b)\n### Fixed an issue with endpoints being able to receive calls once registered\n\n> Commit: [7b958fda8aa5befdb950b226214cc89a982f696b](https://github.com/dOpensource/dsiprouter/commit/7b958fda8aa5befdb950b226214cc89a982f696b)  \n> Date: Tue, 21 Nov 2017 20:57:26 -0800  \n> Author: dopensource (dopensource@noc-lcb-spxy1.garlic.com)  \n> Committer: dopensource (dopensource@noc-lcb-spxy1.garlic.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7b958fda8aa5befdb950b226214cc89a982f696b)\n[//]: # (START_SECTION ac333d0828cb9e772799d33a99ee2931fa6800d1)\n### close 23\n\n> Commit: [ac333d0828cb9e772799d33a99ee2931fa6800d1](https://github.com/dOpensource/dsiprouter/commit/ac333d0828cb9e772799d33a99ee2931fa6800d1)  \n> Date: Tue, 21 Nov 2017 09:22:21 -0800  \n> Author: dopensource (dopensource@noc-lcb-spxy1.garlic.com)  \n> Committer: dopensource (dopensource@noc-lcb-spxy1.garlic.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ac333d0828cb9e772799d33a99ee2931fa6800d1)\n[//]: # (START_SECTION f8e6dac639b6437f73ee607da87f737911aa9c63)\n### Fixed an issue with a quote not being specified correctly\n\n> Commit: [f8e6dac639b6437f73ee607da87f737911aa9c63](https://github.com/dOpensource/dsiprouter/commit/f8e6dac639b6437f73ee607da87f737911aa9c63)  \n> Date: Tue, 21 Nov 2017 02:49:41 -0800  \n> Author: dopensource (dopensource@noc-lcb-spxy1.garlic.com)  \n> Committer: dopensource (dopensource@noc-lcb-spxy1.garlic.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f8e6dac639b6437f73ee607da87f737911aa9c63)\n[//]: # (START_SECTION 5152e54cd53dda2261269fe0ecbd4e73b1d54e79)\n### Will run apt-get update before installing\n\n> Commit: [5152e54cd53dda2261269fe0ecbd4e73b1d54e79](https://github.com/dOpensource/dsiprouter/commit/5152e54cd53dda2261269fe0ecbd4e73b1d54e79)  \n> Date: Tue, 21 Nov 2017 02:45:56 -0800  \n> Author: dopensource (dopensource@noc-lcb-spxy1.garlic.com)  \n> Committer: dopensource (dopensource@noc-lcb-spxy1.garlic.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5152e54cd53dda2261269fe0ecbd4e73b1d54e79)\n[//]: # (START_SECTION 6a58a375b667a3447bf28527937ddd2cf8ec3a95)\n### Added a parameter to the save function in the registrar module.  Close #23\n\n> Commit: [6a58a375b667a3447bf28527937ddd2cf8ec3a95](https://github.com/dOpensource/dsiprouter/commit/6a58a375b667a3447bf28527937ddd2cf8ec3a95)  \n> Date: Tue, 21 Nov 2017 16:37:15 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6a58a375b667a3447bf28527937ddd2cf8ec3a95)\n[//]: # (START_SECTION f98711e799efc4ce3799b4ffde9e021a0f2ffded)\n### Fixed a bug with the commands to enable dSIPRouter to access the FusionPBX DB\n\n> Commit: [f98711e799efc4ce3799b4ffde9e021a0f2ffded](https://github.com/dOpensource/dsiprouter/commit/f98711e799efc4ce3799b4ffde9e021a0f2ffded)  \n> Date: Tue, 14 Nov 2017 23:30:37 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f98711e799efc4ce3799b4ffde9e021a0f2ffded)\n[//]: # (START_SECTION 0cac2e6e1a0642f65963b2aceb34a38dce332956)\n### Update README.md\n\n> Commit: [0cac2e6e1a0642f65963b2aceb34a38dce332956](https://github.com/dOpensource/dsiprouter/commit/0cac2e6e1a0642f65963b2aceb34a38dce332956)  \n> Date: Mon, 13 Nov 2017 15:02:31 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0cac2e6e1a0642f65963b2aceb34a38dce332956)\n[//]: # (START_SECTION c6d6a174294ac9e6598f4eb7e44114419e083474)\n### Updated the release version\n\n> Commit: [c6d6a174294ac9e6598f4eb7e44114419e083474](https://github.com/dOpensource/dsiprouter/commit/c6d6a174294ac9e6598f4eb7e44114419e083474)  \n> Date: Mon, 13 Nov 2017 17:50:18 +0000  \n> Author: root (root@packer-debian-8-amd64.droplet.local)  \n> Committer: root (root@packer-debian-8-amd64.droplet.local)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION c6d6a174294ac9e6598f4eb7e44114419e083474)\n[//]: # (START_SECTION 81f0bcc195e06efeee1e1c1f38a5e857aebdbc27)\n### Fixed the issue with overwriting the original Kamailio configuration files when installing the product multiple times. Closes #19\n\n> Commit: [81f0bcc195e06efeee1e1c1f38a5e857aebdbc27](https://github.com/dOpensource/dsiprouter/commit/81f0bcc195e06efeee1e1c1f38a5e857aebdbc27)  \n> Date: Mon, 13 Nov 2017 17:47:30 +0000  \n> Author: root (root@packer-debian-8-amd64.droplet.local)  \n> Committer: root (root@packer-debian-8-amd64.droplet.local)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 81f0bcc195e06efeee1e1c1f38a5e857aebdbc27)\n[//]: # (START_SECTION 6ab256bc23ca59ebca0e282732e9bd3e1e8eb41f)\n### Commented out database mapping for the fusionpbx_db_mapping table\n\n> Commit: [6ab256bc23ca59ebca0e282732e9bd3e1e8eb41f](https://github.com/dOpensource/dsiprouter/commit/6ab256bc23ca59ebca0e282732e9bd3e1e8eb41f)  \n> Date: Mon, 13 Nov 2017 16:23:29 +0000  \n> Author: root (root@packer-debian-8-amd64.droplet.local)  \n> Committer: root (root@packer-debian-8-amd64.droplet.local)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6ab256bc23ca59ebca0e282732e9bd3e1e8eb41f)\n[//]: # (START_SECTION 30a0d5b5f6ed421bff78e92320c91841aa7fc5e1)\n### Added a library to the install script and fixed an issue with the mysql script\n\n> Commit: [30a0d5b5f6ed421bff78e92320c91841aa7fc5e1](https://github.com/dOpensource/dsiprouter/commit/30a0d5b5f6ed421bff78e92320c91841aa7fc5e1)  \n> Date: Mon, 13 Nov 2017 16:19:11 +0000  \n> Author: root (root@packer-debian-8-amd64.droplet.local)  \n> Committer: root (root@packer-debian-8-amd64.droplet.local)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 30a0d5b5f6ed421bff78e92320c91841aa7fc5e1)\n[//]: # (START_SECTION 1970e0617e9671f155b866f7b27c70d91975f153)\n### Update README.md\n\n> Commit: [1970e0617e9671f155b866f7b27c70d91975f153](https://github.com/dOpensource/dsiprouter/commit/1970e0617e9671f155b866f7b27c70d91975f153)  \n> Date: Mon, 13 Nov 2017 10:37:12 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1970e0617e9671f155b866f7b27c70d91975f153)\n[//]: # (START_SECTION 0fbfa1e96b069d605d5cb6fe91aa7e88baeb46dd)\n### Fixed an issue with stopping the server\n\n> Commit: [0fbfa1e96b069d605d5cb6fe91aa7e88baeb46dd](https://github.com/dOpensource/dsiprouter/commit/0fbfa1e96b069d605d5cb6fe91aa7e88baeb46dd)  \n> Date: Mon, 13 Nov 2017 15:27:52 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0fbfa1e96b069d605d5cb6fe91aa7e88baeb46dd)\n[//]: # (START_SECTION 9b981ea6e8cbc1c9bab4b23576bb58dd3026ea42)\n### Update README.md\n\n> Commit: [9b981ea6e8cbc1c9bab4b23576bb58dd3026ea42](https://github.com/dOpensource/dsiprouter/commit/9b981ea6e8cbc1c9bab4b23576bb58dd3026ea42)  \n> Date: Mon, 13 Nov 2017 09:40:24 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9b981ea6e8cbc1c9bab4b23576bb58dd3026ea42)\n[//]: # (START_SECTION 7b12db3d9acd7a1198dba333387b37aaf7db2ff8)\n### Update README.md\n\n> Commit: [7b12db3d9acd7a1198dba333387b37aaf7db2ff8](https://github.com/dOpensource/dsiprouter/commit/7b12db3d9acd7a1198dba333387b37aaf7db2ff8)  \n> Date: Mon, 13 Nov 2017 09:40:12 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7b12db3d9acd7a1198dba333387b37aaf7db2ff8)\n[//]: # (START_SECTION 16773e75599c05f867f44fa622636031f75bd8c8)\n### Add files via upload\n\n> Commit: [16773e75599c05f867f44fa622636031f75bd8c8](https://github.com/dOpensource/dsiprouter/commit/16773e75599c05f867f44fa622636031f75bd8c8)  \n> Date: Mon, 13 Nov 2017 09:34:41 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 16773e75599c05f867f44fa622636031f75bd8c8)\n[//]: # (START_SECTION 00dc8386150c5da6207cd6f59a048ae26e38e136)\n### Update README.md\n\n> Commit: [00dc8386150c5da6207cd6f59a048ae26e38e136](https://github.com/dOpensource/dsiprouter/commit/00dc8386150c5da6207cd6f59a048ae26e38e136)  \n> Date: Mon, 13 Nov 2017 09:24:26 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 00dc8386150c5da6207cd6f59a048ae26e38e136)\n[//]: # (START_SECTION 25479f2a5fffce7ff304d6b38c979581b32dc801)\n### Update README.md\n\n> Commit: [25479f2a5fffce7ff304d6b38c979581b32dc801](https://github.com/dOpensource/dsiprouter/commit/25479f2a5fffce7ff304d6b38c979581b32dc801)  \n> Date: Mon, 13 Nov 2017 09:21:45 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 25479f2a5fffce7ff304d6b38c979581b32dc801)\n[//]: # (START_SECTION fdd69aa8e1d79ef14fb0448b3ee702982e43f9da)\n### Update README.md\n\n> Commit: [fdd69aa8e1d79ef14fb0448b3ee702982e43f9da](https://github.com/dOpensource/dsiprouter/commit/fdd69aa8e1d79ef14fb0448b3ee702982e43f9da)  \n> Date: Mon, 13 Nov 2017 09:15:09 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fdd69aa8e1d79ef14fb0448b3ee702982e43f9da)\n[//]: # (START_SECTION 1bc9419eb983a1bb2dd99645476b16670f5a565a)\n### Update README.md\n\n> Commit: [1bc9419eb983a1bb2dd99645476b16670f5a565a](https://github.com/dOpensource/dsiprouter/commit/1bc9419eb983a1bb2dd99645476b16670f5a565a)  \n> Date: Mon, 13 Nov 2017 09:00:39 -0500  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1bc9419eb983a1bb2dd99645476b16670f5a565a)\n[//]: # (START_SECTION 4ff4d11309aa6d40595ed6ec89d800c39bfcdf9c)\n### Fixed issues with the install script\n\n> Commit: [4ff4d11309aa6d40595ed6ec89d800c39bfcdf9c](https://github.com/dOpensource/dsiprouter/commit/4ff4d11309aa6d40595ed6ec89d800c39bfcdf9c)  \n> Date: Mon, 13 Nov 2017 12:39:15 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4ff4d11309aa6d40595ed6ec89d800c39bfcdf9c)\n[//]: # (START_SECTION 015fce0deaf6b2edc5b4072d0fdddd65096f61c4)\n### Chnaged to support FusionPBX Domain Support\n\n> Commit: [015fce0deaf6b2edc5b4072d0fdddd65096f61c4](https://github.com/dOpensource/dsiprouter/commit/015fce0deaf6b2edc5b4072d0fdddd65096f61c4)  \n> Date: Sun, 12 Nov 2017 15:36:54 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 015fce0deaf6b2edc5b4072d0fdddd65096f61c4)\n[//]: # (START_SECTION 2918708fb475111254a86f6303a0e0fb23ac0c6c)\n### Added logic to sync the Kamailio domain and domain_attrs tables with FusionPBX instances\n\n> Commit: [2918708fb475111254a86f6303a0e0fb23ac0c6c](https://github.com/dOpensource/dsiprouter/commit/2918708fb475111254a86f6303a0e0fb23ac0c6c)  \n> Date: Sat, 11 Nov 2017 09:40:54 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2918708fb475111254a86f6303a0e0fb23ac0c6c)\n[//]: # (START_SECTION 763434d4868607ea78d5d1d233551f41490f6c4c)\n### Added Add,Update and Delete support for FusionPBX Domain Support feature\n\n> Commit: [763434d4868607ea78d5d1d233551f41490f6c4c](https://github.com/dOpensource/dsiprouter/commit/763434d4868607ea78d5d1d233551f41490f6c4c)  \n> Date: Sun, 5 Nov 2017 08:16:48 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 763434d4868607ea78d5d1d233551f41490f6c4c)\n[//]: # (START_SECTION 9161f79fc60c3d022be68c1c919168705af970f6)\n### Add files via upload\n\n> Commit: [9161f79fc60c3d022be68c1c919168705af970f6](https://github.com/dOpensource/dsiprouter/commit/9161f79fc60c3d022be68c1c919168705af970f6)  \n> Date: Sun, 22 Oct 2017 13:13:26 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9161f79fc60c3d022be68c1c919168705af970f6)\n[//]: # (START_SECTION bdac91314d94d450695afe886069df1def8cd1af)\n### Added js to enable the FusionPBX toogle button and sytled the label for the toggle button\n\n> Commit: [bdac91314d94d450695afe886069df1def8cd1af](https://github.com/dOpensource/dsiprouter/commit/bdac91314d94d450695afe886069df1def8cd1af)  \n> Date: Sun, 22 Oct 2017 17:10:25 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION bdac91314d94d450695afe886069df1def8cd1af)\n[//]: # (START_SECTION dd125a19e527939ff5c128856283e0bc4bc0def3)\n### Initial Support for automatically syncing FusionPBX domains with Kamailio '\n\n> Commit: [dd125a19e527939ff5c128856283e0bc4bc0def3](https://github.com/dOpensource/dsiprouter/commit/dd125a19e527939ff5c128856283e0bc4bc0def3)  \n> Date: Thu, 12 Oct 2017 03:33:42 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dd125a19e527939ff5c128856283e0bc4bc0def3)\n[//]: # (START_SECTION 0ae571409ca712c0e6032ff34a662fea009fde7c)\n### Added some notes\n\n> Commit: [0ae571409ca712c0e6032ff34a662fea009fde7c](https://github.com/dOpensource/dsiprouter/commit/0ae571409ca712c0e6032ff34a662fea009fde7c)  \n> Date: Wed, 11 Oct 2017 11:24:20 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0ae571409ca712c0e6032ff34a662fea009fde7c)\n[//]: # (START_SECTION d720a14a295d5c1f73db93e8b65b9fa459464018)\n### Added an install script for configuring the CDR support within dSIPRouter\n\n> Commit: [d720a14a295d5c1f73db93e8b65b9fa459464018](https://github.com/dOpensource/dsiprouter/commit/d720a14a295d5c1f73db93e8b65b9fa459464018)  \n> Date: Wed, 11 Oct 2017 11:16:04 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d720a14a295d5c1f73db93e8b65b9fa459464018)\n[//]: # (START_SECTION cb5b188dc0b07d264bbe59f0507a56b1a2a14ea4)\n### update .gitignore fix #15\n\n> Commit: [cb5b188dc0b07d264bbe59f0507a56b1a2a14ea4](https://github.com/dOpensource/dsiprouter/commit/cb5b188dc0b07d264bbe59f0507a56b1a2a14ea4)  \n> Date: Wed, 11 Oct 2017 02:43:51 +0300  \n> Author: littleguga (fed777os@gmail.com)  \n> Committer: littleguga (fed777os@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cb5b188dc0b07d264bbe59f0507a56b1a2a14ea4)\n[//]: # (START_SECTION e7427ec396688045c49a33df0bd997c6d0baf078)\n### add info about configuring DSIProuter\n\n> Commit: [e7427ec396688045c49a33df0bd997c6d0baf078](https://github.com/dOpensource/dsiprouter/commit/e7427ec396688045c49a33df0bd997c6d0baf078)  \n> Date: Mon, 9 Oct 2017 05:33:56 +0300  \n> Author: littleguga (fed777os@gmail.com)  \n> Committer: littleguga (fed777os@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e7427ec396688045c49a33df0bd997c6d0baf078)\n[//]: # (START_SECTION 3f1075a79528d14ea34c921fab1a79791af034be)\n### start server on port from settings fix #14\n\n> Commit: [3f1075a79528d14ea34c921fab1a79791af034be](https://github.com/dOpensource/dsiprouter/commit/3f1075a79528d14ea34c921fab1a79791af034be)  \n> Date: Mon, 9 Oct 2017 05:28:53 +0300  \n> Author: littleguga (fed777os@gmail.com)  \n> Committer: littleguga (fed777os@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3f1075a79528d14ea34c921fab1a79791af034be)\n[//]: # (START_SECTION 88bf113c19036b381fcc477166af1c2f98bf3e8d)\n### set DSIP_PORT to variable\n\n> Commit: [88bf113c19036b381fcc477166af1c2f98bf3e8d](https://github.com/dOpensource/dsiprouter/commit/88bf113c19036b381fcc477166af1c2f98bf3e8d)  \n> Date: Mon, 9 Oct 2017 05:14:06 +0300  \n> Author: littleguga (fed777os@gmail.com)  \n> Committer: littleguga (fed777os@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 88bf113c19036b381fcc477166af1c2f98bf3e8d)\n[//]: # (START_SECTION 3d8be7ae7c28d86d733165b135781c8947e3330e)\n### add PIP_CMD for pip3 on debian/ubuntu systems fix #11\n\n> Commit: [3d8be7ae7c28d86d733165b135781c8947e3330e](https://github.com/dOpensource/dsiprouter/commit/3d8be7ae7c28d86d733165b135781c8947e3330e)  \n> Date: Mon, 9 Oct 2017 05:10:03 +0300  \n> Author: littleguga (fed777os@gmail.com)  \n> Committer: littleguga (fed777os@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3d8be7ae7c28d86d733165b135781c8947e3330e)\n[//]: # (START_SECTION 61df3f49ca44afb570b4c7508238b7ebfebaab01)\n### fix typo\n\n> Commit: [61df3f49ca44afb570b4c7508238b7ebfebaab01](https://github.com/dOpensource/dsiprouter/commit/61df3f49ca44afb570b4c7508238b7ebfebaab01)  \n> Date: Mon, 9 Oct 2017 05:02:55 +0300  \n> Author: littleguga (fed777os@gmail.com)  \n> Committer: littleguga (fed777os@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 61df3f49ca44afb570b4c7508238b7ebfebaab01)\n[//]: # (START_SECTION b95cb445d214687f20e728012f07606df5a6e9f3)\n### fix markup and typos\n\n> Commit: [b95cb445d214687f20e728012f07606df5a6e9f3](https://github.com/dOpensource/dsiprouter/commit/b95cb445d214687f20e728012f07606df5a6e9f3)  \n> Date: Mon, 9 Oct 2017 04:55:13 +0300  \n> Author: littleguga (fed777os@gmail.com)  \n> Committer: littleguga (fed777os@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b95cb445d214687f20e728012f07606df5a6e9f3)\n[//]: # (START_SECTION 6f7ebede0ca8a72d6b6f7ccf0a9106bb862cac39)\n### fix command for password change\n\n> Commit: [6f7ebede0ca8a72d6b6f7ccf0a9106bb862cac39](https://github.com/dOpensource/dsiprouter/commit/6f7ebede0ca8a72d6b6f7ccf0a9106bb862cac39)  \n> Date: Mon, 9 Oct 2017 04:53:04 +0300  \n> Author: littleguga (fed777os@gmail.com)  \n> Committer: littleguga (fed777os@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6f7ebede0ca8a72d6b6f7ccf0a9106bb862cac39)\n[//]: # (START_SECTION feb0bebc498f8bf64ef9f2286c5c1cebb5127c4f)\n### add info about License\n\n> Commit: [feb0bebc498f8bf64ef9f2286c5c1cebb5127c4f](https://github.com/dOpensource/dsiprouter/commit/feb0bebc498f8bf64ef9f2286c5c1cebb5127c4f)  \n> Date: Mon, 9 Oct 2017 04:48:38 +0300  \n> Author: littleguga (fed777os@gmail.com)  \n> Committer: littleguga (fed777os@gmail.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION feb0bebc498f8bf64ef9f2286c5c1cebb5127c4f)\n[//]: # (START_SECTION 493840379aa8b915916a6ae6135ed8ccc3038bea)\n### Initial commit for the fraud detection module\n\n> Commit: [493840379aa8b915916a6ae6135ed8ccc3038bea](https://github.com/dOpensource/dsiprouter/commit/493840379aa8b915916a6ae6135ed8ccc3038bea)  \n> Date: Sun, 8 Oct 2017 06:03:37 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 493840379aa8b915916a6ae6135ed8ccc3038bea)\n[//]: # (START_SECTION 2fbe8142d19ed8cf0d6399f63214adcff09c1325)\n### Add cdrs.sql\n\n> Commit: [2fbe8142d19ed8cf0d6399f63214adcff09c1325](https://github.com/dOpensource/dsiprouter/commit/2fbe8142d19ed8cf0d6399f63214adcff09c1325)  \n> Date: Sat, 7 Oct 2017 19:32:48 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2fbe8142d19ed8cf0d6399f63214adcff09c1325)\n[//]: # (START_SECTION 592e3a4b0501cfa5a0dbe85693bd33cab18e5ce9)\n### updated cdrs.sql with the new cdr sql file\n\n> Commit: [592e3a4b0501cfa5a0dbe85693bd33cab18e5ce9](https://github.com/dOpensource/dsiprouter/commit/592e3a4b0501cfa5a0dbe85693bd33cab18e5ce9)  \n> Date: Sat, 7 Oct 2017 19:22:39 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 592e3a4b0501cfa5a0dbe85693bd33cab18e5ce9)\n[//]: # (START_SECTION 4dedc70f7dcecedc52e5f8dd2532ec9666308c96)\n### Adding SQL for CDR's\n\n> Commit: [4dedc70f7dcecedc52e5f8dd2532ec9666308c96](https://github.com/dOpensource/dsiprouter/commit/4dedc70f7dcecedc52e5f8dd2532ec9666308c96)  \n> Date: Thu, 5 Oct 2017 21:45:06 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4dedc70f7dcecedc52e5f8dd2532ec9666308c96)\n[//]: # (START_SECTION 1f65b29dd8870d17100089e934c27e3774d136d1)\n### Added support for domain routing (aka multidomain support)\n\n> Commit: [1f65b29dd8870d17100089e934c27e3774d136d1](https://github.com/dOpensource/dsiprouter/commit/1f65b29dd8870d17100089e934c27e3774d136d1)  \n> Date: Fri, 29 Sep 2017 20:29:01 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1f65b29dd8870d17100089e934c27e3774d136d1)\n[//]: # (START_SECTION 5cbd3ea1b5a3b584b4fea043c9ecfec6396d9ee8)\n### Started to add support for Redhat 7.4\n\n> Commit: [5cbd3ea1b5a3b584b4fea043c9ecfec6396d9ee8](https://github.com/dOpensource/dsiprouter/commit/5cbd3ea1b5a3b584b4fea043c9ecfec6396d9ee8)  \n> Date: Wed, 27 Sep 2017 17:02:01 -0400  \n> Author: root (root@aio.kazoo.com)  \n> Committer: root (root@aio.kazoo.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5cbd3ea1b5a3b584b4fea043c9ecfec6396d9ee8)\n[//]: # (START_SECTION dbb626353928f9a130d707c057b038ff38ab9dca)\n### Fixed an issue that might cause the wrong Python executable to be ran\n\n> Commit: [dbb626353928f9a130d707c057b038ff38ab9dca](https://github.com/dOpensource/dsiprouter/commit/dbb626353928f9a130d707c057b038ff38ab9dca)  \n> Date: Fri, 15 Sep 2017 05:09:14 -0600  \n> Author: root (mack@dopensource.com)  \n> Committer: root (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION dbb626353928f9a130d707c057b038ff38ab9dca)\n[//]: # (START_SECTION 64a24f404516f0c54222749fe7e924cce6706b40)\n### Added support for CDR's to support call direction using a table column called calltype\n\n> Commit: [64a24f404516f0c54222749fe7e924cce6706b40](https://github.com/dOpensource/dsiprouter/commit/64a24f404516f0c54222749fe7e924cce6706b40)  \n> Date: Thu, 14 Sep 2017 20:46:11 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 64a24f404516f0c54222749fe7e924cce6706b40)\n[//]: # (START_SECTION 0c40065b190b1eabb3ec95eb835a2f8decac81ff)\n### Fixed it for Debian\n\n> Commit: [0c40065b190b1eabb3ec95eb835a2f8decac81ff](https://github.com/dOpensource/dsiprouter/commit/0c40065b190b1eabb3ec95eb835a2f8decac81ff)  \n> Date: Mon, 11 Sep 2017 18:47:29 -0700  \n> Author: dopensource (dopensource@noc-lcb-spxy1.garlic.com)  \n> Committer: dopensource (dopensource@noc-lcb-spxy1.garlic.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0c40065b190b1eabb3ec95eb835a2f8decac81ff)\n[//]: # (START_SECTION e029468a13c3e8c0b6030a3276a179990d1de11e)\n### Added a library that was need on Debian Jessie 8.8\n\n> Commit: [e029468a13c3e8c0b6030a3276a179990d1de11e](https://github.com/dOpensource/dsiprouter/commit/e029468a13c3e8c0b6030a3276a179990d1de11e)  \n> Date: Mon, 11 Sep 2017 14:12:50 -0700  \n> Author: dopensource (dopensource@noc-lcb-spxy1.garlic.com)  \n> Committer: dopensource (dopensource@noc-lcb-spxy1.garlic.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e029468a13c3e8c0b6030a3276a179990d1de11e)\n[//]: # (START_SECTION 8c3eb214a98cad32e9798a2082170de6fd040870)\n### Added logic to support stopping of both dsiprouter and rtpengine\n\n> Commit: [8c3eb214a98cad32e9798a2082170de6fd040870](https://github.com/dOpensource/dsiprouter/commit/8c3eb214a98cad32e9798a2082170de6fd040870)  \n> Date: Sun, 10 Sep 2017 20:08:24 +0000  \n> Author: root (root@packer-debian-8-amd64.droplet.local)  \n> Committer: root (root@packer-debian-8-amd64.droplet.local)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8c3eb214a98cad32e9798a2082170de6fd040870)\n[//]: # (START_SECTION 5a08d61c877932e39b997fff9e9980b2456dc348)\n### Added logic to the stop command\n\n> Commit: [5a08d61c877932e39b997fff9e9980b2456dc348](https://github.com/dOpensource/dsiprouter/commit/5a08d61c877932e39b997fff9e9980b2456dc348)  \n> Date: Sun, 10 Sep 2017 19:37:15 +0000  \n> Author: root (root@packer-debian-8-amd64.droplet.local)  \n> Committer: root (root@packer-debian-8-amd64.droplet.local)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5a08d61c877932e39b997fff9e9980b2456dc348)\n[//]: # (START_SECTION 1a77d09fac5c722e294a1a8f9846758caa5a7c2a)\n### Add logic to create a tmpfiles configuration for rtpengine\n\n> Commit: [1a77d09fac5c722e294a1a8f9846758caa5a7c2a](https://github.com/dOpensource/dsiprouter/commit/1a77d09fac5c722e294a1a8f9846758caa5a7c2a)  \n> Date: Sun, 10 Sep 2017 19:28:28 +0000  \n> Author: root (root@packer-debian-8-amd64.droplet.local)  \n> Committer: root (root@packer-debian-8-amd64.droplet.local)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 1a77d09fac5c722e294a1a8f9846758caa5a7c2a)\n[//]: # (START_SECTION 918b7ba206cc5e0a97dd86e82695772c25ce8347)\n### Fixed an issue with the script for installing the RTPEngine on Debian\n\n> Commit: [918b7ba206cc5e0a97dd86e82695772c25ce8347](https://github.com/dOpensource/dsiprouter/commit/918b7ba206cc5e0a97dd86e82695772c25ce8347)  \n> Date: Sun, 10 Sep 2017 19:01:24 +0000  \n> Author: root (root@packer-debian-8-amd64.droplet.local)  \n> Committer: root (root@packer-debian-8-amd64.droplet.local)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 918b7ba206cc5e0a97dd86e82695772c25ce8347)\n[//]: # (START_SECTION 7b433cd386abbb190cbde543708eaaf0dfd567bd)\n### Updated the version\n\n> Commit: [7b433cd386abbb190cbde543708eaaf0dfd567bd](https://github.com/dOpensource/dsiprouter/commit/7b433cd386abbb190cbde543708eaaf0dfd567bd)  \n> Date: Sun, 10 Sep 2017 18:46:27 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7b433cd386abbb190cbde543708eaaf0dfd567bd)\n[//]: # (START_SECTION af7d0f4b31df1bf21bf91cba6946685235ab3ece)\n### Added logic to handle NAT\n\n> Commit: [af7d0f4b31df1bf21bf91cba6946685235ab3ece](https://github.com/dOpensource/dsiprouter/commit/af7d0f4b31df1bf21bf91cba6946685235ab3ece)  \n> Date: Sun, 10 Sep 2017 17:54:42 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION af7d0f4b31df1bf21bf91cba6946685235ab3ece)\n[//]: # (START_SECTION 55e4d2a715c262dbdd5df12944e966bef9ee3d72)\n### Added support for NAT when the RTPEngine process is running\n\n> Commit: [55e4d2a715c262dbdd5df12944e966bef9ee3d72](https://github.com/dOpensource/dsiprouter/commit/55e4d2a715c262dbdd5df12944e966bef9ee3d72)  \n> Date: Sun, 10 Sep 2017 14:02:02 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 55e4d2a715c262dbdd5df12944e966bef9ee3d72)\n[//]: # (START_SECTION 25a091ed28521fea47632909651a725fc7eca153)\n### Updated the README.md\n\n> Commit: [25a091ed28521fea47632909651a725fc7eca153](https://github.com/dOpensource/dsiprouter/commit/25a091ed28521fea47632909651a725fc7eca153)  \n> Date: Sun, 10 Sep 2017 13:20:08 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 25a091ed28521fea47632909651a725fc7eca153)\n[//]: # (START_SECTION f46610f88beaa8b937097461b5fb77d2967f7016)\n### Changed the RTPEngine port from 7222 to 7722\n\n> Commit: [f46610f88beaa8b937097461b5fb77d2967f7016](https://github.com/dOpensource/dsiprouter/commit/f46610f88beaa8b937097461b5fb77d2967f7016)  \n> Date: Sun, 10 Sep 2017 00:16:04 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f46610f88beaa8b937097461b5fb77d2967f7016)\n[//]: # (START_SECTION 0f56674cf0f18f39e18918c2191417b69ed2ea82)\n### Fixed the installer command line and tested it on CentOS - fixed #8\n\n> Commit: [0f56674cf0f18f39e18918c2191417b69ed2ea82](https://github.com/dOpensource/dsiprouter/commit/0f56674cf0f18f39e18918c2191417b69ed2ea82)  \n> Date: Sat, 9 Sep 2017 23:48:26 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0f56674cf0f18f39e18918c2191417b69ed2ea82)\n[//]: # (START_SECTION b5667380011d1f940fd03d2f22ffefd380fe816e)\n### Fixed the installer command line and tested it on CentOS - Issue #8\n\n> Commit: [b5667380011d1f940fd03d2f22ffefd380fe816e](https://github.com/dOpensource/dsiprouter/commit/b5667380011d1f940fd03d2f22ffefd380fe816e)  \n> Date: Sat, 9 Sep 2017 23:45:58 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b5667380011d1f940fd03d2f22ffefd380fe816e)\n[//]: # (START_SECTION ec411e94da0c4dbcd4078483689d0f409d59d278)\n### Finsihed up the command options\n\n> Commit: [ec411e94da0c4dbcd4078483689d0f409d59d278](https://github.com/dOpensource/dsiprouter/commit/ec411e94da0c4dbcd4078483689d0f409d59d278)  \n> Date: Sat, 9 Sep 2017 22:12:38 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ec411e94da0c4dbcd4078483689d0f409d59d278)\n[//]: # (START_SECTION 8838b5495fa752aa388c09eb28be989f320269f0)\n### Added logic to store the process ID when the dsiprouter process is started\n\n> Commit: [8838b5495fa752aa388c09eb28be989f320269f0](https://github.com/dOpensource/dsiprouter/commit/8838b5495fa752aa388c09eb28be989f320269f0)  \n> Date: Sun, 27 Aug 2017 05:42:12 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 8838b5495fa752aa388c09eb28be989f320269f0)\n[//]: # (START_SECTION d0b603a0cd9ef29aaaeecd5094df91a29222a4b6)\n### Added support for installing RTPEngine on Debian\n\n> Commit: [d0b603a0cd9ef29aaaeecd5094df91a29222a4b6](https://github.com/dOpensource/dsiprouter/commit/d0b603a0cd9ef29aaaeecd5094df91a29222a4b6)  \n> Date: Tue, 22 Aug 2017 01:34:46 -0400  \n> Author: root (root@SR215)  \n> Committer: root (root@SR215)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d0b603a0cd9ef29aaaeecd5094df91a29222a4b6)\n[//]: # (START_SECTION 18b6620ad991c1e7896cf605377be10fa6569387)\n### Added support for installing RTPEngine\n\n> Commit: [18b6620ad991c1e7896cf605377be10fa6569387](https://github.com/dOpensource/dsiprouter/commit/18b6620ad991c1e7896cf605377be10fa6569387)  \n> Date: Mon, 21 Aug 2017 10:42:46 -0400  \n> Author: root (root@SR215)  \n> Committer: root (root@SR215)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 18b6620ad991c1e7896cf605377be10fa6569387)\n[//]: # (START_SECTION d92edfb0b0d787f072513b68b9aff16f2021a678)\n### will install rtpengine on CentOS7 by default\n\n> Commit: [d92edfb0b0d787f072513b68b9aff16f2021a678](https://github.com/dOpensource/dsiprouter/commit/d92edfb0b0d787f072513b68b9aff16f2021a678)  \n> Date: Mon, 21 Aug 2017 13:44:39 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION d92edfb0b0d787f072513b68b9aff16f2021a678)\n[//]: # (START_SECTION 91c0c1881b5186f479db3f1046e207291b077582)\n### Fixed an issue with carriers not being assigned to the right address type of carrier\n\n> Commit: [91c0c1881b5186f479db3f1046e207291b077582](https://github.com/dOpensource/dsiprouter/commit/91c0c1881b5186f479db3f1046e207291b077582)  \n> Date: Thu, 17 Aug 2017 17:08:32 -0400  \n> Author: root (root@SR215)  \n> Committer: root (root@SR215)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 91c0c1881b5186f479db3f1046e207291b077582)\n[//]: # (START_SECTION 68abc449f309e8b27c18f7e076721736620a1f21)\n### Update README.md\n\n> Commit: [68abc449f309e8b27c18f7e076721736620a1f21](https://github.com/dOpensource/dsiprouter/commit/68abc449f309e8b27c18f7e076721736620a1f21)  \n> Date: Wed, 16 Aug 2017 22:57:07 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 68abc449f309e8b27c18f7e076721736620a1f21)\n[//]: # (START_SECTION cce6379b13070616fd186c01dee622366ec3c517)\n### Added logic to install dSIPRouter on Debian Jesie\n\n> Commit: [cce6379b13070616fd186c01dee622366ec3c517](https://github.com/dOpensource/dsiprouter/commit/cce6379b13070616fd186c01dee622366ec3c517)  \n> Date: Wed, 16 Aug 2017 22:50:31 -0400  \n> Author: root (root@SR215)  \n> Committer: root (root@SR215)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION cce6379b13070616fd186c01dee622366ec3c517)\n[//]: # (START_SECTION 69195deb45197ffd6a6a78a72df65df3108fff6a)\n### Turned the Reload Kamailio button into an ajax query that updates a div called message\n\n> Commit: [69195deb45197ffd6a6a78a72df65df3108fff6a](https://github.com/dOpensource/dsiprouter/commit/69195deb45197ffd6a6a78a72df65df3108fff6a)  \n> Date: Sun, 30 Jul 2017 13:55:36 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 69195deb45197ffd6a6a78a72df65df3108fff6a)\n[//]: # (START_SECTION 798f3bb9f5eb16724ad4eef99967155ecd00b602)\n### Fixed issue #2 by adding a div that shows any error messages in the login form\n\n> Commit: [798f3bb9f5eb16724ad4eef99967155ecd00b602](https://github.com/dOpensource/dsiprouter/commit/798f3bb9f5eb16724ad4eef99967155ecd00b602)  \n> Date: Sun, 30 Jul 2017 00:59:38 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 798f3bb9f5eb16724ad4eef99967155ecd00b602)\n[//]: # (START_SECTION 6921ff7af74065190741e1a718f8a0551825a095)\n### Added support to deal with MySQL expiring db connections after a certain timeframe.\n\n> Commit: [6921ff7af74065190741e1a718f8a0551825a095](https://github.com/dOpensource/dsiprouter/commit/6921ff7af74065190741e1a718f8a0551825a095)  \n> Date: Thu, 20 Jul 2017 12:08:27 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6921ff7af74065190741e1a718f8a0551825a095)\n[//]: # (START_SECTION e1490bc7b17ae36c6f081b092014754ae3a8c115)\n### Update README.md\n\n> Commit: [e1490bc7b17ae36c6f081b092014754ae3a8c115](https://github.com/dOpensource/dsiprouter/commit/e1490bc7b17ae36c6f081b092014754ae3a8c115)  \n> Date: Mon, 17 Jul 2017 12:30:12 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e1490bc7b17ae36c6f081b092014754ae3a8c115)\n[//]: # (START_SECTION 3be4ee51309b570f6617403f24b1c8662f35c487)\n### Update README.md\n\n> Commit: [3be4ee51309b570f6617403f24b1c8662f35c487](https://github.com/dOpensource/dsiprouter/commit/3be4ee51309b570f6617403f24b1c8662f35c487)  \n> Date: Mon, 17 Jul 2017 12:28:06 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3be4ee51309b570f6617403f24b1c8662f35c487)\n[//]: # (START_SECTION 6546ded1458f724d33ffe527cfaf453d8979da0a)\n### Update README.md\n\n> Commit: [6546ded1458f724d33ffe527cfaf453d8979da0a](https://github.com/dOpensource/dsiprouter/commit/6546ded1458f724d33ffe527cfaf453d8979da0a)  \n> Date: Mon, 17 Jul 2017 12:26:18 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 6546ded1458f724d33ffe527cfaf453d8979da0a)\n[//]: # (START_SECTION 74dd6ca3e4d2f88db6d8205b2118a8e2f08475ed)\n### Update README.md\n\n> Commit: [74dd6ca3e4d2f88db6d8205b2118a8e2f08475ed](https://github.com/dOpensource/dsiprouter/commit/74dd6ca3e4d2f88db6d8205b2118a8e2f08475ed)  \n> Date: Mon, 17 Jul 2017 12:25:42 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 74dd6ca3e4d2f88db6d8205b2118a8e2f08475ed)\n[//]: # (START_SECTION b127757f8912661d9d9292db7567782e042d464b)\n### Add files via upload\n\n> Commit: [b127757f8912661d9d9292db7567782e042d464b](https://github.com/dOpensource/dsiprouter/commit/b127757f8912661d9d9292db7567782e042d464b)  \n> Date: Mon, 17 Jul 2017 12:14:06 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b127757f8912661d9d9292db7567782e042d464b)\n[//]: # (START_SECTION 3994d71451bc8596310945e043cf31018212d815)\n### Add files via upload\n\n> Commit: [3994d71451bc8596310945e043cf31018212d815](https://github.com/dOpensource/dsiprouter/commit/3994d71451bc8596310945e043cf31018212d815)  \n> Date: Mon, 17 Jul 2017 12:12:21 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 3994d71451bc8596310945e043cf31018212d815)\n[//]: # (START_SECTION 23e91efb4050638a050125af743862915875bc11)\n### Delete dsiprouter_outboundrouting\n\n> Commit: [23e91efb4050638a050125af743862915875bc11](https://github.com/dOpensource/dsiprouter/commit/23e91efb4050638a050125af743862915875bc11)  \n> Date: Mon, 17 Jul 2017 12:10:47 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 23e91efb4050638a050125af743862915875bc11)\n[//]: # (START_SECTION e90589481e62211a0c57fff537c20db4226c3a47)\n### Add files via upload\n\n> Commit: [e90589481e62211a0c57fff537c20db4226c3a47](https://github.com/dOpensource/dsiprouter/commit/e90589481e62211a0c57fff537c20db4226c3a47)  \n> Date: Mon, 17 Jul 2017 12:09:31 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION e90589481e62211a0c57fff537c20db4226c3a47)\n[//]: # (START_SECTION 9571c5a872379c646a04d0b2701602d3cff93521)\n### Update README.md\n\n> Commit: [9571c5a872379c646a04d0b2701602d3cff93521](https://github.com/dOpensource/dsiprouter/commit/9571c5a872379c646a04d0b2701602d3cff93521)  \n> Date: Mon, 17 Jul 2017 12:08:48 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 9571c5a872379c646a04d0b2701602d3cff93521)\n[//]: # (START_SECTION 7c71eb8fb1af846c5acb56809d0064a3e1ae25f8)\n### Add files via upload\n\n> Commit: [7c71eb8fb1af846c5acb56809d0064a3e1ae25f8](https://github.com/dOpensource/dsiprouter/commit/7c71eb8fb1af846c5acb56809d0064a3e1ae25f8)  \n> Date: Mon, 17 Jul 2017 11:54:35 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 7c71eb8fb1af846c5acb56809d0064a3e1ae25f8)\n[//]: # (START_SECTION 2e195af1554cf51d9d57ad11b63862e9bc6c64e0)\n### Adding a docs directory\n\n> Commit: [2e195af1554cf51d9d57ad11b63862e9bc6c64e0](https://github.com/dOpensource/dsiprouter/commit/2e195af1554cf51d9d57ad11b63862e9bc6c64e0)  \n> Date: Mon, 17 Jul 2017 15:49:03 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 2e195af1554cf51d9d57ad11b63862e9bc6c64e0)\n[//]: # (START_SECTION 5d8e7ca80a359c04b1e5839266647245c8b05a0d)\n### Fixed an issue with the MySQL DB closing a connection after 8 hours\n\n> Commit: [5d8e7ca80a359c04b1e5839266647245c8b05a0d](https://github.com/dOpensource/dsiprouter/commit/5d8e7ca80a359c04b1e5839266647245c8b05a0d)  \n> Date: Mon, 17 Jul 2017 06:56:54 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 5d8e7ca80a359c04b1e5839266647245c8b05a0d)\n[//]: # (START_SECTION fbdd0b7881be637f8893f8f138805e56d06aa22d)\n### added a intro screen\n\n> Commit: [fbdd0b7881be637f8893f8f138805e56d06aa22d](https://github.com/dOpensource/dsiprouter/commit/fbdd0b7881be637f8893f8f138805e56d06aa22d)  \n> Date: Mon, 17 Jul 2017 04:21:21 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION fbdd0b7881be637f8893f8f138805e56d06aa22d)\n[//]: # (START_SECTION f852135775123a7d2f4d4338038f97bfda21efd0)\n### Changed the navigation so that the left hand navigation is one level\n\n> Commit: [f852135775123a7d2f4d4338038f97bfda21efd0](https://github.com/dOpensource/dsiprouter/commit/f852135775123a7d2f4d4338038f97bfda21efd0)  \n> Date: Mon, 17 Jul 2017 01:31:50 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION f852135775123a7d2f4d4338038f97bfda21efd0)\n[//]: # (START_SECTION 17555a94a9fae749331872c0cff2da8b1aca42d4)\n### added execute permissions\n\n> Commit: [17555a94a9fae749331872c0cff2da8b1aca42d4](https://github.com/dOpensource/dsiprouter/commit/17555a94a9fae749331872c0cff2da8b1aca42d4)  \n> Date: Sun, 16 Jul 2017 13:48:36 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 17555a94a9fae749331872c0cff2da8b1aca42d4)\n[//]: # (START_SECTION 0df719c6bd8d001311418c3b4bcc31d417d184ab)\n### Made the kamailio configuration more generic\n\n> Commit: [0df719c6bd8d001311418c3b4bcc31d417d184ab](https://github.com/dOpensource/dsiprouter/commit/0df719c6bd8d001311418c3b4bcc31d417d184ab)  \n> Date: Sun, 16 Jul 2017 03:06:39 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 0df719c6bd8d001311418c3b4bcc31d417d184ab)\n[//]: # (START_SECTION 67e9f09358bfd180d71c19d12377e72b8601a7bf)\n### fixed an error with the symbolic link with the kamailio.cfg file\n\n> Commit: [67e9f09358bfd180d71c19d12377e72b8601a7bf](https://github.com/dOpensource/dsiprouter/commit/67e9f09358bfd180d71c19d12377e72b8601a7bf)  \n> Date: Sat, 15 Jul 2017 23:20:35 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 67e9f09358bfd180d71c19d12377e72b8601a7bf)\n[//]: # (START_SECTION 38f275621c9727894c80d37922023b1e82a62c13)\n### Update README.md\n\n> Commit: [38f275621c9727894c80d37922023b1e82a62c13](https://github.com/dOpensource/dsiprouter/commit/38f275621c9727894c80d37922023b1e82a62c13)  \n> Date: Sat, 15 Jul 2017 06:47:13 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 38f275621c9727894c80d37922023b1e82a62c13)\n[//]: # (START_SECTION 57f965d3c06817386be9519a1384f699d7ddf523)\n### Update README.md\n\n> Commit: [57f965d3c06817386be9519a1384f699d7ddf523](https://github.com/dOpensource/dsiprouter/commit/57f965d3c06817386be9519a1384f699d7ddf523)  \n> Date: Sat, 15 Jul 2017 06:46:28 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 57f965d3c06817386be9519a1384f699d7ddf523)\n[//]: # (START_SECTION 4b3798280cbf3b9735a54a982868e8cc91eb046f)\n### Update README.md\n\n> Commit: [4b3798280cbf3b9735a54a982868e8cc91eb046f](https://github.com/dOpensource/dsiprouter/commit/4b3798280cbf3b9735a54a982868e8cc91eb046f)  \n> Date: Sat, 15 Jul 2017 06:45:03 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 4b3798280cbf3b9735a54a982868e8cc91eb046f)\n[//]: # (START_SECTION a72121b9551921aa3dced32d943c6034ba318f82)\n### Update README.md\n\n> Commit: [a72121b9551921aa3dced32d943c6034ba318f82](https://github.com/dOpensource/dsiprouter/commit/a72121b9551921aa3dced32d943c6034ba318f82)  \n> Date: Sat, 15 Jul 2017 06:44:19 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION a72121b9551921aa3dced32d943c6034ba318f82)\n[//]: # (START_SECTION 955add0b09637e7ecdd62411272b2d0ef84d3aa3)\n### Update README.md\n\n> Commit: [955add0b09637e7ecdd62411272b2d0ef84d3aa3](https://github.com/dOpensource/dsiprouter/commit/955add0b09637e7ecdd62411272b2d0ef84d3aa3)  \n> Date: Sat, 15 Jul 2017 06:42:08 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: GitHub (noreply@github.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION 955add0b09637e7ecdd62411272b2d0ef84d3aa3)\n[//]: # (START_SECTION ce6c5aac0db5476dc496c34388e4f9ce2c4b86e5)\n### Initial commit as dsiprouter\n\n> Commit: [ce6c5aac0db5476dc496c34388e4f9ce2c4b86e5](https://github.com/dOpensource/dsiprouter/commit/ce6c5aac0db5476dc496c34388e4f9ce2c4b86e5)  \n> Date: Sat, 15 Jul 2017 10:37:01 +0000  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION ce6c5aac0db5476dc496c34388e4f9ce2c4b86e5)\n[//]: # (START_SECTION b46b1e64f06f448bde78b98e3ae8228ce5f96067)\n### Initial commit\n\n> Commit: [b46b1e64f06f448bde78b98e3ae8228ce5f96067](https://github.com/dOpensource/dsiprouter/commit/b46b1e64f06f448bde78b98e3ae8228ce5f96067)  \n> Date: Sat, 15 Jul 2017 06:30:25 -0400  \n> Author: Mack Hendricks (mack@dopensource.com)  \n> Committer: Mack Hendricks (mack@dopensource.com)  \n> Signed:   \n\n\n\n\n---\n\n[//]: # (END_SECTION b46b1e64f06f448bde78b98e3ae8228ce5f96067)\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contribution Guide\n\nThis guide will provide you with the tools to start developing on the dSIPRouter platform and contributing back to the community.\n\n## Getting Started\n\nFirst we will get our dev environment setup.\nWe recommend you create a local or cloud hosted VM for your dev environment.\n\n1. Clone the branch you would like to work on and create a feature branch for your changes.\nIn this example we want to add support for **Ubuntu 20.04** to the **master** branch.\n\n    ```bash\n    git clone -b master https://github.com/dOpensource/dsiprouter.git /opt/dsiprouter\n    cd /opt/dsiprouter\n    git checkout -b feature-ubuntu-20.04\n    ```\n\n2. Install dSIPRouter with dev options.\nYou may need different flags depending on where you deploy (servernat, etc..)\n\n    ```bash\n    ./dsiprouter.sh install -all -servernat -with_dev\n    ```\n\n    This will run through the entire install process, then configure your git environment for the dsiprouter repo.\n    For the rest of this walkthrough assume your starting location is in project root (default `/etc/dsiprouter`).\n\n3. Make your changes..\nThen prior to commit make sure you reset any defaults in `settings.py` or `kamailio.cfg`.\nYou should **not** be commiting the generated versions from `/etc/dsiprouter`, this will include changes to the defaults.\nInstead you should check which changes you need to keep by runnning diff:\n\n    ```bash\n    diff /etc/dsiprouter/gui/settings.py /opt/dsiprouter/gui/settings.py\n    ```\n\n    You should be able to pick out which changes were generated on install and which changes you need to keep.\n    You can then merge the changes you need into the project source files.\n\n4. Then commit, and push the changes to your feature branch.\n\n    ```bash\n    git add -A\n    git commit\n    git push\n    ```\n\n   This will run the git hooks setup earlier and automatically do the following:\n   - update python dependencies in requirements.txt\n   - update the changelog doc\n   - update the contributors doc\n   - resolve git references in your commit message\n\n   If your committing to a different remote (i.e. not origin), then you need to let our hooks know beforehand.\n   This is useful if you forked dsiprouter, or you have a secondary upstream/downstream remote:\n\n   ```bash\n   git commit --remote=upstream\n   git push upstream\n   ```\n\n5. Create a Pull Request on Github (or Merge Request if on gitlab).\nSee the [Github Docs](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request) for more information.\n\n## Core Architecture Principles\n\n- Installation should be less then 10 minutes\n- Someone with basic SIP knowledge should be able to configure it and place a test call within 10 minutes\n- The API structure should follow the Web UI\n\n### File Structure\n\n### Modules\n\n#### Structure\n\nOur module architecture has been loosely defined for a while, but we now want to define the structure and start moving all modules into this structure.\nEach module should have these components:\n\n| Component | Location | Purpose |\n| --------- | -------- | ------- |\n| module.js | gui/static/js/ | Contains the UI Javascript for a module |\n| module.css | gui/static/css/ | Contains the Cascading Style Sheets for a module |\n| module.py | gui/modules/module | Contains the Python scripts |\n\nFor example, this is what the \"***Domain***\" module looks like:\n\n| Component | Location | Purpose |\n| --------- | -------- | ------- |\n| domain.js | gui/static/js/ | Contains the UI Javascript for a module |\n| domain.css | gui/static/css/ | Contains the Cascading Style Sheets for a module |\n| domain*.py | gui/modules/domain/ | Contains the Python scripts |\n| domain*.sql | gui/modules/domain/ | SQL Scripts for installing the database table structure fro the module |\n\n#### Packaging\n\nModules not installed during dSIPRouter install should be packaged in a zipfile with an install script.\nThe install script should place the components defined in the [Structure](#structure) section into their proper locations.\n\n#### Auto-discovery\n\nModules should be automatically discoverable.\nThis means that a new module should become automatically available from the UI without restarting the UI\n\n### API Structure\n\nWe are in the process of refactoring most of the application so that all of the GUI components leverages the API.  Currently, the core API reside in gui/modules/api.\n\n#### Adding an API\n\nThe following steps will guide you thru the process of adding a new API to\ndSIPRouter.  We handle all of the security on your behalf.\n\n1. Add a subdirectory in gui/modules/api/new_api\n2. Copy sample_api.py to gui/modules/api/new_api/routes.py\n3. Add the following line to the imports section of gui/dsiprouter.py\n\n```\nfrom modules.api.new_api.routes import new_api\n```\n\n4. Add the following line to gui/dsiprouter to register the new API\n\n```\napp.register_blueprint(new_api)\n```\n\n5. Restart dSIPRouter\n\n6. Test the new API\n\n```\nexport DSIP_TOKEN=<api token>\nexport DSIP_HOST=<ip or hostname of dSIPRouter>\ncurl --insecure -H \"Authorization: Bearer $DSIP_TOKEN\" -X GET https://$DSIP_HOST:5000/api/v1/new_api/new_entity\n```\n\n\n### Usability\n\n - Web GUI\n - REST API\n - CLI Commands\n\n### Development Environment\n\n### dsiprouter.sh\n\n- Do not put platform specific commands in this file.  Use the component/OS distribution/version.sh file to place those commands.\n\nFor example, if we need to install the Letsencrypt OS package so that it can be used for Kamailio on debian, then you would\nplace it in the kamailio/debian/9.sh and kamailio/debian/10.sh file\n"
  },
  {
    "path": "CONTRIBUTORS.md",
    "content": "## Thank you to all contributors for your hard work\n\n### Contributors\n\n- Asiel Lara\n- chelseatcarter\n- Dan\n- Dan Ryan\n- Dean Forester\n- demonspork\n- dependabot[bot]\n- dopensource\n- hailthemelody\n- James Peru Mmbono\n- jornsby\n- littleguga\n- Mack\n- Mack Hendicks\n- Mack hendricks\n- Mack Hendricks\n- matmurdock\n- Mat Murdock\n- Maurice Rogers\n- mhendricks\n- Micah Quinn\n- ncannon01\n- Nicole\n- Omari S. King\n- reqlez\n- richard\n- Richard\n- Richard Bolaji\n- RichSosa28\n- root\n- TheGolg\n- TuxPowered\n- Tyler Moore\n- VOICE1\n"
  },
  {
    "path": "HA/consul/consul.fc",
    "content": "/usr/local/bin/consul -- gen_context(system_u:object_r:consul_exec_t,s0)\n/usr/lib/systemd/system/consul.service -- gen_context(system_u:object_r:consul_unit_file_t,s0)\n/etc/systemd/system/consul.service -- gen_context(system_u:object_r:consul_unit_file_t,s0)\n/var/lib/consul(/.*)? -- gen_context(system_u:object_r:consul_var_lib_t,s0)\n/var/run/consul.pid -- gen_context(system_u:object_r:consul_var_run_t,s0)\n/var/run/user/consul(/.*)? -- gen_context(system_u:object_r:consul_tmp_t,s0)\n/var/cache/consul(/.*)? -- gen_context(system_u:object_r:consul_cache_t,s0)\n/etc/consul.d(/.*)? -- gen_context(system_u:object_r:consul_etc_t,s0)"
  },
  {
    "path": "HA/consul/consul.hcl",
    "content": "datacenter = \"${CLUSTER_NAME}\"\ndata_dir = \"/opt/consul\"\nnode_name = \"NODE_NAME\"\nlog_level = \"INFO\"\nbind_addr = \"0.0.0.0\"\nclient_addr = \"0.0.0.0\"\nadvertise_addr = \"EXTERNAL_IP_ADDR\"\nenable_syslog = true\nsyslog_facility = \"LOCAL3\"\nencrypt = \"${KEY_CIPHER_TEXT_B64}\"\nretry_join = ${RETRY_JOIN}\nperformance = {\n  raft_multiplier = 1\n}"
  },
  {
    "path": "HA/consul/consul.service",
    "content": "[Unit]\nDescription=\"HashiCorp Consul Service Mesh\"\nDocumentation=https://www.consul.io/\nRequires=network-online.target\nAfter=network-online.target\nConditionFileNotEmpty=/etc/consul.d/consul.hcl\n\n[Service]\nUser=consul\nGroup=consul\nPIDFile=/var/run/consul/consul.pid\nExecStartPre=[ -f \"/var/run/consul/consul.pid\" ] && /bin/rm -f /var/run/consul/consul.pid\nExecStartPre=/usr/local/bin/consul validate /etc/consul.d\nExecStart=/usr/local/bin/consul agent -pid-file /var/run/consul/consul.pid -enable-local-script-checks -config-dir=/etc/consul.d\nExecReload=/usr/local/bin/consul reload\nKillMode=process\nRestart=on-failure\nLimitNOFILE=65536\n\n[Install]\nWantedBy=multi-user.target"
  },
  {
    "path": "HA/consul/consul.te",
    "content": "policy_module(consul, 1.0.0)\n\n########################################\n#\n# Declarations\n#\n\ntype consul_t;\ntype consul_exec_t;\ninit_daemon_domain(consul_t, consul_exec_t)\n\nattribute consul_domain;\n\ntype consul_etc_t;\nfiles_config_file(consul_etc_t)\n\ntype consul_cache_t;\nfiles_type(consul_cache_t)\n\ntype consul_var_lib_t;\nfiles_type(consul_var_lib_t)\n\ntype consul_var_run_t;\nfiles_pid_file(consul_var_run_t)\n\ntype consul_tmp_t;\nfiles_tmp_file(consul_tmp_t)\n\ntype consul_unit_file_t;\nsystemd_unit_file(consul_unit_file_t)\n\n#######################################\n#\n# consul local policy\n#\n\noptional_policy(`\n\tunconfined_domain(consul_t)\n')\n\n########################################\n#\n# consul domain local policy\n#\n\n\nallow consul_t self:fifo_file rw_fifo_file_perms;\nallow consul_t self:unix_stream_socket create_stream_socket_perms;\nallow consul_t self:process signal_perms;\nallow consul_t self:tcp_socket create_socket_perms;\nallow consul_t self:udp_socket create_socket_perms;\n\nmanage_dirs_pattern(consul_domain, consul_cache_t, consul_cache_t)\nmanage_files_pattern(consul_domain, consul_cache_t, consul_cache_t)\nmanage_lnk_files_pattern(consul_t, consul_cache_t, consul_cache_t)\nfiles_var_filetrans(consul_domain, consul_cache_t, { dir file })\n\nmanage_dirs_pattern(consul_domain, consul_log_t, consul_log_t)\nmanage_files_pattern(consul_domain, consul_log_t, consul_log_t)\nmanage_lnk_files_pattern(consul_t, consul_log_t, consul_log_t)\nlogging_log_filetrans(consul_domain, consul_log_t, { dir file })\n\nmanage_dirs_pattern(consul_domain, consul_var_lib_t, consul_var_lib_t)\nmanage_files_pattern(consul_domain, consul_var_lib_t, consul_var_lib_t)\nmanage_lnk_files_pattern(consul_t, consul_var_lib_t, consul_var_lib_t)\nfiles_var_lib_filetrans(consul_domain, consul_var_lib_t, { dir file })\n\nmanage_dirs_pattern(consul_domain, consul_var_run_t, consul_var_run_t)\nmanage_files_pattern(consul_domain, consul_var_run_t, consul_var_run_t)\nmanage_lnk_files_pattern(consul_t, consul_var_run_t, consul_var_run_t)\nfiles_pid_filetrans(consul_domain, consul_var_run_t, { dir file })\n\nmanage_dirs_pattern(consul_t, consul_tmp_t, consul_tmp_t)\nmanage_files_pattern(consul_t, consul_tmp_t, consul_tmp_t)\nmanage_lnk_files_pattern(consul_t, consul_tmp_t,consul_tmp_t)\nfiles_tmp_filetrans(consul_t, consul_tmp_t, { file fifo_file dir })\n\n# Stay in consul domain if consul is called by a script\ncan_exec(consul_domain, consul_exec_t)\n\nkernel_read_system_state(consul_t)\nkernel_read_network_state(consul_t)\n\ncorecmd_exec_bin(consul_domain)\ncorecmd_exec_shell(consul_domain)\n\n## Consul needs to bind to ports 8301 8302 8300 8400 8500 8600, all labeled unreserved_port_t\ncorenet_tcp_bind_all_unreserved_ports(consul_domain)\ncorenet_udp_bind_all_unreserved_ports(consul_domain)\n\n## Including read random and read urandom since Consul features TLS encryption.  Maybe we'll need those devices.\ndev_read_rand(consul_domain)\ndev_read_urand(consul_domain)\ndev_read_sysfs(consul_domain)\n\ndomain_use_interactive_fds(consul_domain)\n\nfs_getattr_all_fs(consul_domain)\nfs_read_hugetlbfs_files(consul_domain)\n\nfiles_read_etc_files(consul_domain)\nfiles_read_usr_files(consul_domain)\n\nmiscfiles_read_localization(consul_domain)\n\nsysnet_dns_name_resolve(consul_domain)"
  },
  {
    "path": "HA/consul/installConsulCluster.sh",
    "content": "#!/usr/bin/env bash\n#\n# Summary:      consul service mesh\n# Supported OS: debian, ubuntu, linuxmint, redhat, centos, amazon linux\n# Notes:        you must be able to ssh to every node in the cluster from where script is run\n#               supported ssh authentication methods: password, pubkey\n#               by default the consul agent type is set to client\n#               atleast one consul agent must be a server or installation will hault\n# Usage:        ./installConsulCluster.sh [-h|--help|-cloud] <[user1[:pass1]@]node1[:port1][?server]> <[user2[:pass2]@]node2[:port2][?server]> ...\n#\n\n# set project root, if in a git repo resolve top level dir\nPROJECT_ROOT=${PROJECT_ROOT:-$(dirname $(dirname $(dirname $(readlink -f \"$0\"))))}\n# import shared library functions\n. ${PROJECT_ROOT}/HA/shared_lib.sh\n\n\n# node configuration settings\nexport CLUSTER_NAME=\"consulcluster\"\nCLOUD_AUTO_JOIN=0\nDSIP_SYSTEM_CONFIG_DIR=\"/etc/dsiprouter\"\nPATH_UPDATE_FILE=\"/etc/profile.d/dsip_paths.sh\"\nCONSUL_PRIV_KEY=\"${DSIP_SYSTEM_CONFIG_DIR}/consulkey\"\nexport KEY_CIPHER_TEXT_B64=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64)\nSERVICES_TO_TRACK=(\"dsiprouter\" \"kamailio\" \"haproxy\" \"asterisk\" \"freeswitch\" \"rtpengine\" \"rtpproxy\" \"mysql\" \"mariadb\" \"postgresql\" \"redis\")\nSSH_DEFAULT_OPTS=\"-o StrictHostKeyChecking=no -o CheckHostIp=no -o ServerAliveInterval=5 -o ServerAliveCountMax=2\"\nCONSUL_TCP_PORTS=(8300 8301 8302 8500 8501 8600)\nCONSUL_UDP_PORTS=(8301 8302 8600)\nCONSUL_URL=\"https://releases.hashicorp.com/consul/\"\n\n\nprintUsage() {\n    pprint \"$0 [-h|--help|-cloud] <[user1[:pass1]@]node1[:port1][?server]> <[user2[:pass2]@]node2[:port2][?server]> ...\"\n}\n\nif ! isRoot; then\n    printerr \"Must be run with root privileges\" && exit 1\nfi\n\nif [[ \"$1\" == \"-h\" ]] || [[ \"$1\" == \"--help\" ]]; then\n    printUsage && exit 1\nfi\n\n# loop through args and evaluate any options\nARGS=()\nwhile (( $# > 0 )); do\n    ARG=\"$1\"\n    case $ARG in\n        -cloud)\n            # TODO: support cloud auto join\n            # https://www.consul.io/docs/agent/cloud-auto-join.html\n            printerr \"Cloud auto join is not supported at this time\" && exit 1\n            WITH_CLOUD_AUTO_JOIN=1\n            shift\n            ;;\n        *)  # add to list of args\n            ARGS+=( \"$ARG\" )\n            shift\n            ;;\n    esac\ndone\n\n# make sure required args are fulfilled\nif (( ${#ARGS[@]} < 1 )); then\n    printerr \"No nodes provided to setup consul cluster\" && printUsage && exit 1\nelif ! echo \"${ARGS[*]}\" | grep -q '?server'; then\n    printerr \"At least 1 server node is required to setup a consul cluster\" && printUsage && exit 1\nfi\n\n# install local requirements for script\nsetOSInfo\ncase \"$DISTRO\" in\n    debian|ubuntu|linuxmint)\n        apt-get install -y sshpass gawk perl\n        ;;\n    centos|redhat|amazon)\n        yum install -y epel-release\n        yum install -y sshpass gawk perl\n        ;;\n    *)\n        printerr \"Your OS Distro is currently not supported\"\n        exit 1\n        ;;\nesac\n\n# prints number of nodes in cluster\ngetClusterSize() {\n    consul members 2>/dev/null | tail -n +2 | wc -l\n}\n\nisNodeInCluster() {\n    local NODE_NAME=\"$1\"\n    if consul catalog nodes 2>/dev/null | tail -n +2 | awk '{print $1}' | grep -q \"$NODE_NAME\"; then\n        return 0\n    else\n        return 1\n    fi\n}\n\n# $1 == ipv4 persistent rules file\n# $2 == ipv6 persistent rules file\nsetFirewallRules() {\n    local IP4RESTORE_FILE=\"$1\"\n    local IP6RESTORE_FILE=\"$2\"\n\n    # use firewalld if installed\n    if cmdExists \"firewall-cmd\"; then\n        for PORT in ${CONSUL_TCP_PORTS[@]}; do\n            firewall-cmd --zone=public --add-port=${PORT}/tcp --permanent\n        done\n        for PORT in ${CONSUL_UDP_PORTS[@]}; do\n            firewall-cmd --zone=public --add-port=${PORT}/udp --permanent\n        done\n        firewall-cmd --reload\n    else\n        # set ipv4 firewall rules (on each node)\n        for PORT in ${CONSUL_TCP_PORTS[@]}; do\n            iptables -I INPUT 1 -p tcp --dport ${PORT} -j ACCEPT\n        done\n        for PORT in ${CONSUL_UDP_PORTS[@]}; do\n            iptables -I INPUT 1 -p udp --dport ${PORT} -j ACCEPT\n        done\n\n        # set ipv6 firewall rules (on each node)\n        for PORT in ${CONSUL_TCP_PORTS[@]}; do\n            ip6tables -I INPUT 1 -p tcp --dport ${PORT} -j ACCEPT\n        done\n        for PORT in ${CONSUL_UDP_PORTS[@]}; do\n            ip6tables -I INPUT 1 -p udp --dport ${PORT} -j ACCEPT\n        done\n    fi\n\n    # Remove duplicates and save\n    mkdir -p $(dirname ${IP4RESTORE_FILE})\n    iptables-save | awk '!x[$0]++' > ${IP4RESTORE_FILE}\n    mkdir -p $(dirname ${IP6RESTORE_FILE})\n    ip6tables-save | awk '!x[$0]++' > ${IP6RESTORE_FILE}\n}\n\ncreateConsulTrackedService() {\n    local NAME=\"$1\"\n    local SYSTEMCTL_CMD=$(type -p systemctl)\n\n    (cat << EOF\n{\n  \"service\": {\n    \"name\": \"${NAME}\",\n    \"tags\": [\"${NAME}\"],\n    \"check\": {\n      \"id\": \"${NAME}\",\n      \"service_id\": \"${NAME}\",\n      \"args\": [\"${SYSTEMCTL_CMD}\", \"is-active\", \"--quiet\", \"${NAME}\"],\n      \"interval\": \"10s\"\n    }\n  }\n}\nEOF\n    ) > /etc/consul.d/${NAME}.json\n}\n\n# loop through args and grab hosts\nexport NUM_NODES=0\nHOST_LIST=()\nfor NODE in ${ARGS[@]}; do\n    HOST_LIST+=( $(printf '%s' \"$NODE\" | cut -d '@' -f 2- | cut -d ':' -f -1) )\n    NUM_NODES=$((NUM_NODES+1))\ndone\n\n# set the auto join list for nodes\nexport RETRY_JOIN=\"[$(join ',' $(printf '\"%s\" ' \"${HOST_LIST[@]}\"))]\"\n\n# set config files as variables for transport\nCONSUL_SYSTEM_SERVICE=$(cat ${PROJECT_ROOT}/HA/consul/consul.service)\nCONSUL_SYSLOG_CONFIG=$(cat ${PROJECT_ROOT}/resources/syslog/consul.conf)\nCONSUL_LOGROTATE_CONFIG=$(cat ${PROJECT_ROOT}/resources/logrotate/consul)\nCONSUL_SELINUX_MODULE=$(cat ${PROJECT_ROOT}/HA/consul/consul.te)\nCONSUL_SELINUX_CONTEXT=$(cat ${PROJECT_ROOT}/HA/consul/consul.fc)\n\n# prevent quote expansion in client and server config\n# we will just scp it over because bash expands quote on assignment\nmkdir -p /tmp/consul\nenvsubst < ${PROJECT_ROOT}/HA/consul/consul.hcl > /tmp/consul/consul.hcl\nenvsubst < ${PROJECT_ROOT}/HA/consul/server.hcl > /tmp/consul/server.hcl\n\n# TODO: in an example config these dns settings were used, do we need them?\n# - https://github.com/sboily/asterisk-consul-module/blob/master/contribs/kamailio/config/kamailio/kamailio.cfg\n#\n# /etc/kamailio/kamailio.cfg\n#\n#dns_cache_init=no\n#use_dns_cache=no\n#\n\n\n# loop through args and run setup commands\ni=0\nfor NODE in ${ARGS[@]}; do\n    USER=$(printf '%s' \"$NODE\" | cut -s -d '@' -f -1 | cut -d ':' -f -1)\n    PASS=$(printf '%s' \"$NODE\" | cut -s -d '@' -f -1 | cut -s -d ':' -f 2-)\n    HOST=$(printf '%s' \"$NODE\" | cut -d '@' -f 2- | cut -d ':' -f -1)\n    PORT=$(printf '%s' \"$NODE\" | cut -d '@' -f 2- | cut -s -d ':' -f 2- | cut -d '?' -f -1)\n    TYPE=$(printf '%s' \"$NODE\" | cut -s -d '?' -f 2-)\n\n    # default user is root for ssh\n    USER=${USER:-root}\n    # default port is 22 for ssh\n    PORT=${PORT:-22}\n    # default type is client\n    [[ \"$TYPE\" != \"server\" ]] && TYPE=\"client\"\n\n    # validate host connection\n    if ! checkConn ${HOST} ${PORT}; then\n        printerr \"Could not establish connection to host [${HOST}] on port [${PORT}]\" && exit 1\n    fi\n\n    SSH_CMD=\"ssh\"\n    SCP_CMD=\"scp\"\n    if [ -z \"$HOST\" ]; then\n        printerr \"Node [${NODE}] does not contain a host\" && printUsage && exit 1\n    else\n        SSH_REMOTE_HOST=\"${HOST}\"\n    fi\n    SSH_REMOTE_HOST=\"${USER}@${SSH_REMOTE_HOST}\"\n    if [ -n \"$PASS\" ]; then\n        #SSH_CMD=\"sshpass -f <(printf '${PASS}\\n') ssh\"\n        #SSH_CMD=\"sshpass -p '${PASS}' ssh\"\n        export SSHPASS=\"${PASS}\"\n        SSH_CMD=\"sshpass -e ssh\"\n        SCP_CMD=\"sshpass -e scp\"\n    fi\n    SSH_OPTS=\"${SSH_DEFAULT_OPTS} -p ${PORT}\"\n    SCP_OPTS=\"-q ${SSH_DEFAULT_OPTS} -P ${PORT}\"\n    SSH_CMD=\"${SSH_CMD} ${SSH_REMOTE_HOST} ${SSH_OPTS}\"\n    SCP_CMD=\"${SCP_CMD} ${SCP_OPTS}\"\n\n    # validate unattended ssh connection\n    if ! checkSsh ${SSH_CMD}; then\n        printerr \"Could not establish unattended ssh connection to [${SSH_REMOTE_HOST}] on port [${PORT}]\" && exit 1\n    fi\n\n    NODE_NAME=\"node$((i+1))\"\n    # remote server will be using bash as interpreter\n    SSH_CMD=\"${SSH_CMD} bash\"\n    # DEBUG:\n    printdbg \"SSH_CMD: ${SSH_CMD}\"\n    printdbg \"SCP_CMD: ${SCP_CMD}\"\n\n    # transfer some files before we connect\n    ${SCP_CMD} -r /tmp/consul ${SSH_REMOTE_HOST}:/tmp/\n    rm -rf /tmp/consul\n\n    # run commands through ssh\n    (${SSH_CMD} <<- EOSSH\n    set -x\n\n    # re-declare functions and vars we pass to remote server\n    # note that variables in function definitions (from calling environement)\n    # lose scope unless local to function, they must be passed to remote\n    $(typeset -f printdbg)\n    $(typeset -f printerr)\n    $(typeset -f cmdExists)\n    $(typeset -f setOSInfo)\n    $(typeset -f join)\n    $(typeset -f pathCheck)\n    $(typeset -f getClusterSize)\n    $(typeset -f setFirewallRules)\n    $(typeset -f detectServiceMan)\n    $(typeset -f createConsulTrackedService)\n    $(typeset -f isNodeInCluster)\n    $(typeset -f ipv4Test)\n    $(typeset -f ipv6Test)\n    $(typeset -f getExternalIP)\n    $(typeset -f getInternalIP)\n    ESC_SEQ=\"\\033[\"\n    ANSI_NONE=\"\\${ESC_SEQ}39;49;00m\" # Reset colors\n    ANSI_RED=\"\\${ESC_SEQ}1;31m\"\n    ANSI_GREEN=\"\\${ESC_SEQ}1;32m\"\n    TYPE=\"$TYPE\"\n\n\n    # systemd is a hard requirement\n    detectServiceMan\n    if [[ \"\\$SERVICE_MANAGER\" != \"systemd\" ]]; then\n        printerr \"Systemd is required and not installed on ${NODE_NAME}\" && exit 1\n    fi\n\n    setOSInfo\n    printdbg 'installing requirements'\n    case \"\\$DISTRO\" in\n        debian|ubuntu|linuxmint)\n            # debian-based distro specific settings\n            IP4RESTORE_FILE=\"/etc/iptables/rules.v4\"\n            IP6RESTORE_FILE=\"/etc/iptables/rules.v6\"\n            export DEBIAN_FRONTEND=noninteractive\n\n            apt-get install -y curl wget sed gawk dirmngr iptables-persistent netfilter-persistent\n            ;;\n\n        centos|redhat|amazon)\n            # rhel-based distro specific settings\n            IP4RESTORE_FILE=\"/etc/sysconfig/iptables\"\n            IP6RESTORE_FILE=\"/etc/sysconfig/ip6tables\"\n\n            yum install -y curl wget sed gawk\n\n            # rhel/centos SELINUX\n            # from: https://lists.fedoraproject.org/archives/list/selinux@lists.fedoraproject.org/thread/IOKQ26N53CT7WMZZWBW2RTSD2YT7TWNQ/\n            if sestatus | head -1 | grep -qi 'enabled'; then\n                yum install -y policycoreutils-python setools-console selinux-policy-devel\n\n                semanage permissive -a consul_t\n\n                setsebool -P httpd_can_network_connect 1\n\n                mkdir -p /tmp/selinux\n\n                printf '%s' \"\\$CONSUL_SELINUX_MODULE\" > /tmp/selinux/consul.te\n                printf '%s' \"\\$CONSUL_SELINUX_CONTEXT\" > /tmp/selinux/consul.fc\n\n                checkmodule -M -m /tmp/selinux/consul.te -o /tmp/selinux/consul.mod\n                semodule_package -m /tmp/selinux/galera.mod -f /tmp/selinux/consul.fc -o /tmp/selinux/consul.pp\n                semodule -i /tmp/selinux/consul.pp\n            fi\n            ;;\n\n        *)\n            printerr \"OS Distro is currently not supported\" && exit 1\n            ;;\n    esac\n\n\n    # set firewall rules\n    setFirewallRules \"\\$IP4RESTORE_FILE\" \"\\$IP6RESTORE_FILE\"\n\n    # fix PATH if needed\n    # we are using the default install paths but these may change in the future\n    mkdir -p \\$(dirname ${PATH_UPDATE_FILE})\n    if [[ ! -e \"$PATH_UPDATE_FILE\" ]]; then\n        (cat << 'EOF'\n#export PATH=\"/usr/local/bin\\${PATH:+:\\$PATH}\"\n#export PATH=\"\\${PATH:+\\$PATH:}/usr/sbin\"\n#export PATH=\"\\${PATH:+\\$PATH:}/sbin\"\nEOF\n        ) > ${PATH_UPDATE_FILE}\n    fi\n\n    # minimalistic approach avoids growing duplicates\n    # enable (uncomment) and import only what we need\n    PATH_UPDATED=0\n\n    # - consul\n    if ! pathCheck /usr/local/bin; then\n        sed -i -r 's|^#(export PATH=\"/usr/local/bin\\\\$\\{PATH:\\+:\\\\$PATH\\}\")\\$|\\1|' ${PATH_UPDATE_FILE}\n        PATH_UPDATED=1\n    fi\n\n    # import new path definition if it was updated\n    (( \\${PATH_UPDATED} == 1 )) &&  . \\${PATH_UPDATE_FILE}\n\n    # install consul and configure cluster settings\n    if ! cmdExists \"consul\"; then\n        printdbg \"Installing consul\"\n        CONSUL_VER=\\$(curl -s \"$CONSUL_URL\" 2>/dev/null | grep -oP '<a href=\"/consul/\\K[\\d\\.]+(?=/\"\\>)' | head -1)\n        ARCH=\\$(uname -m)\n\n        # normalize architecture to download correct ver (default to x86_64)\n        case \"\\$ARCH\" in\n            x86_64)\n                ARCH=\"amd64\"\n                ;;\n            i386|i686)\n                ARCH=\"386\"\n                ;;\n            aarch64*|armv[8-9]*)\n                ARCH=\"arm64\"\n                ;;\n            arm|armv[0-7]*)\n                ARCH=\"arm\"\n                ;;\n            *)\n                ARCH=\"amd64\"\n                ;;\n        esac\n\n        wget -q -O /tmp/consul.zip \"${CONSUL_URL}\\${CONSUL_VER}/consul_\\${CONSUL_VER}_linux_\\${ARCH}.zip\"\n        unzip -f /tmp/consul.zip consul -d /usr/local/bin/ &&\n        rm -f /tmp/consul.zip &&\n        chmod +x /usr/local/bin/consul\n\n        if ! cmdExists \"consul\"; then\n            printerr \"Consul install failed on ${NODE_NAME}\" && exit 1\n        fi\n    else\n        printdbg \"consul is already installed\"\n    fi\n\n    consul -autocomplete-install\n    complete -C /usr/local/bin/consul consul\n\n    useradd --system --user-group --home /etc/consul.d --shell /bin/false --comment \"Consul Service Mesh\" consul\n    mkdir -p /opt/consul\n    mkdir -p /etc/consul.d\n    mkdir -p /var/run/consul\n    chown -R consul:consul /opt/consul\n    chown -R consul:consul /var/run/consul\n\n    # create consul agent client configs\n    sed \"s|EXTERNAL_IP_ADDR|\\$(getExternalIP)|; s|NODE_NAME|${NODE_NAME}|\" /tmp/consul/consul.hcl > /etc/consul.d/consul.hcl\n\n    # create consul agent server configs\n    if [[ \"$TYPE\" == \"server\" ]]; then\n        cp -f /tmp/consul/server.hcl /etc/consul.d/server.hcl\n    fi\n\n    # setup consul syslog configs\n    printf '%s' \"$CONSUL_SYSLOG_CONFIG\" >  /etc/rsyslog.d/consul.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # setup consul logrotate configs\n    printf '%s' \"$CONSUL_LOGROTATE_CONFIG\" > /etc/logrotate.d/consul\n\n    # create consul system service\n    printf '%s' \"$CONSUL_SYSTEM_SERVICE\" > /etc/systemd/system/consul.service\n    chmod 0644 /etc/systemd/system/consul.service\n\n    # create consul agents for services\n    SERVICES=\\$(systemctl list-units | awk '{print $1}' | grep -oP \"\\$(join '|' ${SERVICES_TO_TRACK[@]})\")\n    for SERVICE in \\${SERVICES[@]}; do\n        createConsulTrackedService \"\\$SERVICE\"\n    done\n\n    # set permissions for consul configs and cleanup tmp\n    chmod -R 0740 /etc/consul.d\n    chown -R consul:consul /etc/consul.d\n    rm -rf /tmp/consul\n\n    systemctl enable consul\n    systemctl restart consul\n    if systemctl is-active --quiet consul; then\n        printdbg \"Consul was successfully configured on [${NODE_NAME}]\"\n    else\n        printerr \"Consul configuration failed on [${NODE_NAME}]\" && exit 1\n    fi\n\n    # check if node connected to cluster\n    sleep 5\n    if (( \\$? == 0 )) && (( \\$(getClusterSize) > 0 )) && isNodeInCluster \"${NODE_NAME}\"; then\n        printdbg \"Adding Node to cluster success\"\n    else\n        printerr \"Adding Node to cluster failed [${NODE_NAME}]\" && exit 1\n    fi\n\n    exit 0\nEOSSH\n    ) 2>&1\n\n    if (( $? != 0 )); then\n        printerr \"consul configuration failed on ${HOST}\" && exit 1\n    fi\n\n    i=$((i+1))\ndone\n\nunset CLUSTER_NAME KEY_CIPHER_TEXT_B64 NUM_NODES RETRY_JOIN\nexit 0"
  },
  {
    "path": "HA/consul/server.hcl",
    "content": "server = true\nbootstrap_expect = ${NUM_NODES}\nui = true"
  },
  {
    "path": "HA/mysql/installAAGaleraReplication.sh",
    "content": "#!/usr/bin/env bash\n#\n# Summary:      mysql active active galera replication\n#\n# Supported OS: debian, centos, amzn\n#\n# Notes:        uses mariadb\n#               you must be able to ssh to every node in the cluster from where script is run\n#               supported ssh authentication methods: password, pubkey\n#               if quorum is lost between 2-node cluster you must reset the quorum, bootstrap the non-primary:\n#               mysql -e \"SET GLOBAL wsrep_provider_options='pc.bootstrap=YES';\"\n#               ref: <http://galeracluster.com/documentation-webpages/quorumreset.html>\n##\n# TODO:         support active/passive galera replication\n#               https://medium.com/mr-dops/mariadb-with-galera-cluster-8ded2e83721b\n#\n\n# set project root, if in a git repo resolve top level dir\nPROJECT_ROOT=${PROJECT_ROOT:-$(dirname $(dirname $(dirname $(readlink -f \"$0\"))))}\n# import shared library functions\n. ${PROJECT_ROOT}/HA/shared_lib.sh\n\n\n# node configuration settings\nCLUSTER_NAME=\"mysqlcluster\"\nMYSQL_USER=\"root\"\nMYSQL_PASS=\"$(createPass)\"\nMYSQL_PORT=\"3306\"\nBACKUPS_DIR=\"/var/backups\"\nWITH_REMOTE_DB=0\nMYSQL_BACKUP_DIR=\"${BACKUPS_DIR}/mysql\"\nGALERA_REPL_PORT=\"4567\"\nGALERA_INCR_PORT=\"4568\"\nGALERA_SNAP_PORT=\"4444\"\nSSH_KEY_FILE=\"\"\n# galera library only available on mariadb ver >= 10.1\n# at the time of writing default repo ver == 5.5\n# they also do have patches for 5.5 and 10.0 if needed\nMYSQL_REQ_VER=\"10.1\"\nDEBUG=0\n\n# global variables used throughout script\nNODE_NAMES=()\nHOST_LIST=()\nCLUSTER_NODE_ADDRS=()\ndeclare -A CLOUD_DICT\nCLOUD_PLATFORM=\"\"\nCLUSTER_RESOURCES=(cluster_vip cluster_srcaddr)\nSSH_CMD_LIST=()\n\nprintUsage() {\n    pprint \"Usage: $0 [-h|--help|-debug|-remotedb] [-i <ssh key file>] <[sshuser1[:sshpass1]@]node1[:sshport1]> <[sshuser2[:sshpass2]@]node2[:sshport2]> ...\"\n}\n\n# loop through args and evaluate any options\nNODES=()\nwhile (( $# > 0 )); do\n    ARG=\"$1\"\n    case $ARG in\n        -h|--help)\n            printUsage\n            exit 0\n            ;;\n        -debug)\n            DEBUG=1\n            shift\n            ;;\n        -remotedb)\n            WITH_REMOTE_DB=1\n            shift\n            ;;\n        -i)\n            shift\n            SSH_KEY_FILE=\"$1\"\n            shift\n            ;;\n        *)  # add to list of args\n            NODES+=( \"$ARG\" )\n            shift\n            ;;\n    esac\ndone\n\nif (( $DEBUG == 1 )); then\n    set -x\nfi\n\nif (( ${#NODES[@]} < 2 )); then\n    printerr \"At least 2 nodes are required to setup replication\"\n    printUsage\n    exit 1\nfi\n\n# install local requirements for script\nif ! cmdExists 'ssh' || ! cmdExists 'sshpass' || ! cmdExists 'nmap' || ! cmdExists 'sed' || ! cmdExists 'awk'; then\n    printdbg 'Installing local requirements for cluster install'\n\n    if cmdExists 'apt-get'; then\n        runas apt-get install -y openssh-client sshpass gawk\n    elif cmdExists 'dnf'; then\n        runas dnf install -y openssh-clients sshpass gawk\n    elif cmdExists 'yum'; then\n        runas yum install --enablerepo=epel -y openssh-clients sshpass gawk\n    else\n        printerr \"Your local OS is not currently not supported\"\n        exit 1\n    fi\nfi\n\n# sanity check\nif (( $? != 0 )); then\n    printerr 'Could not install requirements for cluster install'\n    exit 1\nfi\n\n# prints number of nodes in cluster\ngetClusterSize() {\n    local OPT=\"\"\n    local MYSQL_PARAMS=()\n\n    while (( $# > 0 )); do\n        OPT=\"$1\"\n        case $OPT in\n            --user*)\n                MYSQL_PARAMS+=( --user=$(printf '%s' \"$1\" | cut -d '=' -f 2-) )\n                shift\n                ;;\n            --pass*)\n                MYSQL_PARAMS+=( --password=$(printf '%s' \"$1\" | cut -d '=' -f 2-) )\n                shift\n                ;;\n            --host*)\n                MYSQL_PARAMS+=( --host=$(printf '%s' \"$1\" | cut -d '=' -f 2-) )\n                shift\n                ;;\n            --port*)\n                MYSQL_PARAMS+=( --port=$(printf '%s' \"$1\" | cut -d '=' -f 2-) )\n                shift\n                ;;\n            *)  # no valid args skip\n                shift\n                ;;\n        esac\n    done\n\n    mysql -sN ${MYSQL_PARAMS[@]} \\\n        -e \"select VARIABLE_VALUE from information_schema.GLOBAL_STATUS where VARIABLE_NAME='wsrep_cluster_size'\" \\\n        || echo '0'\n}\n\nsetFirewallRules() {\n    firewall-cmd --zone=public --add-port=${MYSQL_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${GALERA_REPL_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${GALERA_REPL_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${GALERA_INCR_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${GALERA_SNAP_PORT}/tcp --permanent\n\n    firewall-cmd --reload\n}\n\n# find the first private IP address (reverse order) on a physical interface\n# this is typically the secondary interface or secondary IP on the primary interface\n# if no RFC1918 address is found use the IP associated with the default route\n# NOTE: sourced here to allow easier declaration on remote node\nsource <(\n    cat <<EOF\n    getGaleraInternalIP() {\n        $(declare -f getPhysicalIfaces)\n        $(declare -f ipv4TestRFC1918)\n        $(declare -f getInternalIP)\nEOF\n    cat <<'EOF'\n        for IP in $(\n            for IFACE in $(getPhysicalIfaces | sort -r); do\n                ip -4 -o addr show $IFACE | awk '{split($4,a,\"/\"); print a[1];}'\n            done\n        ); do\n            if ipv4TestRFC1918 \"$IP\"; then\n                echo \"$IP\"\n                return 0\n            fi\n        done\n        IP=\"$(getInternalIP)\"\n        [[ -n \"$IP\" ]] && {\n            echo \"$IP\"\n            return 0\n        }\n        return 1\n    }\nEOF\n)\n\n# loop through args and gather variables\ni=0\nfor NODE in ${NODES[@]}; do\n    SSH_OPTS=( ${DEFAULT_SSH_OPTS[@]} )\n    RSYNC_OPTS=()\n\n    NODE_NAME=\"${CLUSTER_NAME}-node$((i+1))\"\n    NODE_NAMES+=( \"$NODE_NAME\" )\n\n    USER=$(printf '%s' \"$NODE\" | cut -s -d '@' -f -1 | cut -d ':' -f -1)\n    PASS=$(printf '%s' \"$NODE\" | cut -s -d '@' -f -1 | cut -s -d ':' -f 2-)\n    HOST=$(printf '%s' \"$NODE\" | cut -d '@' -f 2- | cut -d ':' -f -1)\n    PORT=$(printf '%s' \"$NODE\" | cut -d '@' -f 2- | cut -s -d ':' -f 2-)\n\n    HOST_LIST+=( \"$HOST\" )\n\n    # default user is root for ssh\n    USER=${USER:-root}\n    # default port is 22 for ssh\n    PORT=${PORT:-22}\n\n    # validate host connection\n    if ! checkConn ${HOST} ${PORT}; then\n        printerr \"Could not establish connection to host [${HOST}] on port [${PORT}]\"\n        exit 1\n    fi\n\n    if [[ -z \"$HOST\" ]]; then\n        printerr \"Node [${NODE}] does not contain a host\"\n        printUsage\n        exit 1\n    fi\n    USERHOST_LIST+=( \"${USER}@${HOST}\" )\n\n    if [[ -n \"$PASS\" ]]; then\n        export SSHPASS=\"${PASS}\"\n        SSH_CMD=\"sshpass -e ssh\"\n        RSYNC_CMD=\"sshpass -e rsync\"\n        SSH_OPTS+=(-o PreferredAuthentications=password)\n    else\n        SSH_CMD=\"ssh\"\n        RSYNC_CMD=\"rsync\"\n        if [[ -n \"$SSH_KEY_FILE\" ]]; then\n            SSH_OPTS+=(-o PreferredAuthentications=publickey -i $SSH_KEY_FILE)\n        else\n            SSH_OPTS+=(-o PreferredAuthentications=publickey)\n        fi\n    fi\n\n    RSYNC_OPTS+=(--port=${PORT} -z --exclude=\".*\")\n    SSH_OPTS+=(-p ${PORT})\n\n    printdbg 'validating unattended ssh connection'\n    if ! checkSsh ${SSH_CMD} ${SSH_OPTS[@]} ${USERHOST_LIST[$i]}; then\n        printerr \"Could not establish unattended ssh connection to [${USERHOST_LIST[$i]}] on port [${PORT}]\"\n        exit 1\n    fi\n\n    # wrap up some args / options\n    SSH_CMD_LIST+=( \"${SSH_CMD} ${SSH_OPTS[*]}\" )\n    RSYNC_CMD_LIST+=( \"${RSYNC_CMD} ${RSYNC_OPTS[*]}\" )\n\n    ${SSH_CMD_LIST[$i]} ${USERHOST_LIST[$i]} \"$(typeset -f runas); runas bash\"  <<- EOSSH\n        if (( $DEBUG == 1 )); then\n            set -x\n        fi\n\n        # re-declare functions and vars we pass to remote server\n        # note that variables in function definitions (from calling environement)\n        # lose scope unless local to function, they must be passed to remote\n        ESC_SEQ=\"\\033[\"\n        ANSI_NONE=\"\\${ESC_SEQ}39;49;00m\" # Reset colors\n        ANSI_RED=\"\\${ESC_SEQ}1;31m\"\n        ANSI_GREEN=\"\\${ESC_SEQ}1;32m\"\n        MYSQL_PORT=\"$MYSQL_PORT\"\n        GALERA_REPL_PORT=\"$GALERA_REPL_PORT\"\n        GALERA_INCR_PORT=\"$GALERA_INCR_PORT\"\n        GALERA_SNAP_PORT=\"$GALERA_SNAP_PORT\"\n        $(typeset -f printdbg)\n        $(typeset -f printerr)\n        $(typeset -f cmdExists)\n\n        # awk is required for getInternalIP()\n        printdbg 'installing requirements on remote node ${HOST_LIST[$i]}'\n        if ! cmdExists 'awk'; then\n            if cmdExists 'apt-get'; then\n                export DEBIAN_FRONTEND=noninteractive\n                apt-get install -y gawk\n            elif cmdExists 'dnf'; then\n                dnf install -y gawk\n            elif cmdExists 'yum'; then\n                yum install -y gawk\n            else\n                printerr \"OS on remote node [${HOST_LIST[$i]}] is currently not supported\"\n                exit 1\n            fi\n\n            if (( \\$? != 0 )); then\n                printerr \"Failed to install requirements on remote node ${HOST_LIST[$i]}\"\n                exit 1\n            fi\n        fi\nEOSSH\n\n    printdbg 'checking if node is deployed on a supported cloud platform'\n    CLOUD_PLATFORM=$(${SSH_CMD_LIST[$i]} -q ${USERHOST_LIST[$i]} \"$(typeset -f getCloudPlatform); getCloudPlatform;\")\n    [[ -n \"$CLOUD_PLATFORM\" ]] && CLOUD_DICT[$CLOUD_PLATFORM]=1\n\n    # warn the user if we don't have an integration setup for this provider yet\n    case \"${CLOUD_LIST[$i]}\" in\n        AWS|GCE|AZURE|VULTR|OCE)\n            printwarn 'support for this cloud platform has not been tested'\n            printwarn 'attempting install anyways'\n            ;;\n    esac\n\n    # find the internal IP that the cluster will communicate over\n    # this secondary interface is where the node will attach the floating IP\n    CLUSTER_NODE_ADDRS+=( $(${SSH_CMD_LIST[$i]} -q ${USERHOST_LIST[$i]} \"$(typeset -f getGaleraInternalIP); getGaleraInternalIP;\") )\n\n    i=$((i+1))\ndone\n\n# make sure user does not try to install on 2 different cloud platforms\nif (( ${#CLOUD_DICT[@]} > 1 )); then\n    printerr 'nodes are deployed on different cloud platforms'\n    printerr 'installation on differing cloud platforms is not supported'\n    exit 1\nfi\n\n# loop through args and pre-configure mysql server\ni=0\nwhile (( $i < ${#NODES[@]} )); do\n    # run commands through ssh\n    (${SSH_CMD_LIST[$i]} ${USERHOST_LIST[$i]} \"$(typeset -f runas); runas bash\" <<- EOSSH\n        if (( $DEBUG == 1 )); then\n            set -x\n        fi\n\n        # re-declare functions and vars we pass to remote server\n        # note that variables in function definitions (from calling environement)\n        # lose scope unless local to function, they must be passed to remote\n        ESC_SEQ=\"\\033[\"\n        ANSI_NONE=\"\\${ESC_SEQ}39;49;00m\" # Reset colors\n        ANSI_RED=\"\\${ESC_SEQ}1;31m\"\n        ANSI_GREEN=\"\\${ESC_SEQ}1;32m\"\n        MYSQL_USER=\"$MYSQL_USER\"\n        MYSQL_PASS=\"$MYSQL_PASS\"\n        MYSQL_PORT=\"$MYSQL_PORT\"\n        GALERA_REPL_PORT=\"$GALERA_REPL_PORT\"\n        GALERA_INCR_PORT=\"$GALERA_INCR_PORT\"\n        GALERA_SNAP_PORT=\"$GALERA_SNAP_PORT\"\n        NODE_NAMES=( ${NODE_NAMES[@]} )\n        CLUSTER_NODE_ADDRS=( ${CLUSTER_NODE_ADDRS[@]} )\n        $(typeset -f printdbg)\n        $(typeset -f printwarn)\n        $(typeset -f printerr)\n        $(typeset -f cmdExists)\n        $(typeset -f getPkgVer)\n        $(typeset -f mysqlSecureInstall)\n        $(typeset -f setFirewallRules)\n\n        # state is tracked here, if it exists we have completed this section already\n        STATE_FILE=\"${MYSQL_BACKUP_DIR}/state/${HOST_LIST[$i]}\"\n        if [[ -f \"\\$STATE_FILE\" ]]; then\n            printwarn 'initial configuration already complete, skipping..'\n            exit 0\n        fi\n        # trap exit signals to remove state file in case we exit early\n        cleanupHandler() {\n            rm -f \"\\$STATE_FILE\"\n            trap - EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n        }\n        trap 'cleanupHandler $?' EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n\n        printdbg 'setting up cluster hostname resolution'\n\n        # for each node remove the loopback hostname if present\n        # this will cause issues when adding nodes to the cluster\n        # ref: https://serverfault.com/questions/363095/why-does-my-hostname-appear-with-the-address-127-0-1-1-rather-than-127-0-0-1-in\n        grep -v -E '^127\\.0\\.1\\.1' /etc/hosts >/tmp/hosts &&\n            mv -f /tmp/hosts /etc/hosts\n\n        # add section for the galera hostnames\n        if ! grep -q 'MYSQL_CONFIG_START' /etc/hosts 2>/dev/null; then\n            bash -c \"(\n                echo ''\n                echo '#####MYSQL_CONFIG_START'\n            )>>/etc/hosts\"\n            j=0\n            while (( \\$j < \\${#CLUSTER_NODE_ADDRS[@]} )); do\n                bash -c \"echo '\\${CLUSTER_NODE_ADDRS[\\$j]} \\${NODE_NAMES[\\$j]}' >>/etc/hosts\"\n                j=\\$((j+1))\n            done\n            bash -c \"echo '#####MYSQL_CONFIG_END' >>/etc/hosts\"\n        else\n            j=0; tmp='';\n            while (( \\$j < \\${#CLUSTER_NODE_ADDRS[@]} )); do\n                tmp+=\"\\${CLUSTER_NODE_ADDRS[\\$j]} \\${NODE_NAMES[\\$j]}\\\\n\"\n                j=\\$((j+1))\n            done\n            perl -0777 -i -pe \"s|(#+MYSQL_CONFIG_START).*?(#+MYSQL_CONFIG_END)|\\\\1\\\\n\\${tmp}\\\\2|gms\" /etc/hosts\n        fi\n\n        # backup dirs we will be using\n        mkdir -p ${MYSQL_BACKUP_DIR}/{etc,var/lib,\\${HOME},dumps,state}\n\n        # will determine how we merge databases later on\n        # 0 == no merge (fresh install no changes)\n        # 1 == overwrite (existing databases cloned)\n        # 2 == merge (merge primary databases with existing)\n        echo 'MYSQL_MERGE_ACTION=0' >>\\${STATE_FILE}\n\n        printdbg 'installing requirements on remote node ${HOST_LIST[$i]}'\n        if cmdExists 'apt-get'; then\n            # debian specific settings\n            echo 'MYSQL_SECTION=\"mysqld\"' >>\\${STATE_FILE}\n            echo 'MYSQL_CLUSTER_CONFIG=\"/etc/mysql/mariadb.conf.d/cluster.cnf\"' >>\\${STATE_FILE}\n            export DEBIAN_FRONTEND=noninteractive\n\n            apt-get install -y curl perl sed gawk rsync dirmngr bc expect firewalld\n\n            if (( \\$? != 0 )); then\n                printerr \"failed installing requirements on remote node ${HOST_LIST[$i]}\"\n                exit 1\n            fi\n\n            # install or upgrade mysql if needed\n            # debian has multiple packages for mariadb-server, easier to find version with dpkg\n            MYSQL_VER=\\$(getPkgVer 'mariadb-server(-[0-9.]+)?$')\n            if cmdExists 'mysql'; then\n                # check repo version and update if needed\n                if (( \\$(echo \"\\${MYSQL_VER:-0} < ${MYSQL_REQ_VER}\" | bc -l) )); then\n                    echo 'MYSQL_MERGE_ACTION=1' >>\\${STATE_FILE}\n\n                    # backup data in case upgrade fails\n                    systemctl stop mariadb\n                    mv -f /var/lib/mysql ${MYSQL_BACKUP_DIR}/var/lib/\n                    cp -f /etc/my.cnf* ${MYSQL_BACKUP_DIR}/etc/\n                    cp -rf /etc/my.cnf* ${MYSQL_BACKUP_DIR}/etc/\n                    cp -rf /etc/mysql* ${MYSQL_BACKUP_DIR}/etc/\n                    cp -f \\${HOME}/.my.cnf* ${MYSQL_BACKUP_DIR}/\\${HOME}/\n\n                    curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash\n                    MYSQL_MAJOR_VER=\\$(getPkgVer -l 'mariadb-server' | cut -c -4)\n\n                    debconf-set-selections <<< \"mariadb-server-\\${MYSQL_MAJOR_VER} mysql-server/root_password password temp\"\n                    debconf-set-selections <<< \"mariadb-server-\\${MYSQL_MAJOR_VER} mysql-server/root_password_again password temp\"\n                    apt-get install -y mariadb-client mariadb-server\n\n                    if (( \\$? != 0 )); then\n                        printerr \"failed installing requirements on remote node ${HOST_LIST[$i]}\"\n                        exit 1\n                    fi\n\n                    systemctl enable mariadb\n                    systemctl start mariadb\n\n                    # automate mysql_secure_install cmds\n                    mysqlSecureInstall \"temp\" \"${MYSQL_PASS}\"\n\n                    # allow auto login for root\n                    printf '%s\\n%s\\n%s\\n' '[client]' 'user = root' 'password = ${MYSQL_PASS}' > ~/.my.cnf\n\n                else\n                    echo 'MYSQL_MERGE_ACTION=2' >>\\${STATE_FILE}\n\n                    # make sure root can login remotely\n                    mysql --user='root' --password='${MYSQL_PASS}' mysql \\\\\n                        -e 'CREATE USER IF NOT EXISTS \"root\"@\"%\";' \\\\\n                        -e 'SET PASSWORD FOR \"root\"@\"%\" = PASSWORD(\"${MYSQL_PASS}\");' \\\\\n                        -e 'GRANT ALL PRIVILEGES ON *.* TO \"root\"@\"%\" WITH GRANT OPTION;' \\\\\n                        -e 'FLUSH PRIVILEGES;'\n                fi\n            else\n                # check repo version and update if needed\n                LATEST_VER=\\$(getPkgVer -l 'mariadb-server')\n                if (( \\$(echo \"\\${LATEST_VER:-0} < ${MYSQL_REQ_VER}\" | bc -l) )); then\n                    curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash\n                    MYSQL_MAJOR_VER=\\$(getPkgVer -l 'mariadb-server' | cut -c -4)\n                fi\n\n                debconf-set-selections <<< \"mariadb-server-\\${MYSQL_MAJOR_VER} mysql-server/root_password password temp\"\n                debconf-set-selections <<< \"mariadb-server-\\${MYSQL_MAJOR_VER} mysql-server/root_password_again password temp\"\n                apt-get install -y mariadb-client mariadb-server\n\n                if (( \\$? != 0 )); then\n                    printerr \"failed installing requirements on remote node ${HOST_LIST[$i]}\"\n                    exit 1\n                fi\n\n                systemctl enable mariadb\n                systemctl start mariadb\n\n                # automate mysql_secure_install cmds\n                mysqlSecureInstall \"temp\" \"${MYSQL_PASS}\"\n\n                # allow auto login for root user\n                printf '%s\\n%s\\n%s\\n' '[client]' 'user = root' 'password = ${MYSQL_PASS}' > ~/.my.cnf\n\n            fi\n\n            # make sure mysql is listening for external connections prior to dumping databases\n            if ! grep -qoP '^(?!#)bind-address[ \\t]*=[ \\t]*0\\.0\\.0\\.0' /etc/mysql/mariadb.conf.d/50-server.cnf; then\n                perl -i -pe 's%^(?!#)(bind-address[ \\t]*=[ \\t]*).*\\$%\\${1}0.0.0.0%m' /etc/mysql/mariadb.conf.d/50-server.cnf\n                systemctl restart mariadb\n            fi\n\n        elif cmdExists 'yum' || cmdExists 'dnf'; then\n            # centos specific settings\n            echo 'MYSQL_SECTION=\"galera\"' >>\\${STATE_FILE}\n            echo 'MYSQL_CLUSTER_CONFIG=\"/etc/my.cnf.d/cluster.cnf\"' >>\\${STATE_FILE}\n\n            if cmdExists 'dnf'; then\n                dnf install -y curl perl sed gawk rsync bc expect firewalld\n            else\n                yum install -y curl perl sed gawk rsync bc expect firewalld\n            fi\n\n            if (( \\$? != 0 )); then\n                printerr \"failed installing requirements on remote node ${HOST_LIST[$i]}\"\n                exit 1\n            fi\n\n            # SELINUX integration\n            if sestatus | head -1 | grep -qi 'enabled'; then\n                yum install -y policycoreutils-python setools-console selinux-policy-devel\n\n                if (( \\$? != 0 )); then\n                    printerr \"failed installing requirements on remote node ${HOST_LIST[$i]}\"\n                    exit 1\n                fi\n\n                semanage permissive -a mysqld_t\n\n                semanage port -a -t mysqld_port_t -p tcp 3306\n                semanage port -a -t mysqld_port_t -p tcp 4567\n                semanage port -a -t mysqld_port_t -p tcp 4568\n                semanage port -a -t mysqld_port_t -p tcp 4444\n                semanage port -a -t mysqld_port_t -p udp 4567\n\n                setsebool -P daemons_enable_cluster_mode 1\n\n                mkdir -p /tmp/selinux\n\n                (cat <<'EOF'\nmodule galera 1.0;\n\nrequire {\n    type mysqld_t;\n    type rsync_exec_t;\n    type anon_inodefs_t;\n    type proc_net_t;\n    type kerberos_port_t;\n    class file { read execute execute_no_trans getattr open };\n    class tcp_socket { name_bind name_connect };\n    class process { setpgid siginh rlimitinh noatsecure };\n    type unconfined_t;\n    type initrc_tmp_t;\n    type init_t;\n    class service enable;\n}\n\n#============= mysqld_t ==============\nallow mysqld_t initrc_tmp_t:file open;\nallow mysqld_t self:process setpgid;\nallow mysqld_t rsync_exec_t:file { read execute execute_no_trans getattr open };\nallow mysqld_t anon_inodefs_t:file getattr;\nallow mysqld_t proc_net_t:file { read open };\nallow mysqld_t kerberos_port_t:tcp_socket { name_bind name_connect };\n\n#============= unconfined_t ==============\nallow unconfined_t init_t:service enable;\nEOF\n                ) > /tmp/selinux/galera.te\n\n                checkmodule -M -m /tmp/selinux/galera.te -o /tmp/selinux/galera.mod\n                semodule_package -m /tmp/selinux/galera.mod -o /tmp/selinux/galera.pp\n                semodule -i /tmp/selinux/galera.pp\n            fi\n\n            # install or upgrade mysql if needed\n            MYSQL_VER=\\$(getPkgVer 'mariadb-server')\n            if cmdExists 'mysql'; then\n                # check repo version and update if needed\n                if (( \\$(echo \"\\${MYSQL_VER:-0} < ${MYSQL_REQ_VER}\" | bc -l) )); then\n                    echo 'MYSQL_MERGE_ACTION=1' >>\\${STATE_FILE}\n\n                    # backup data in case upgrade fails\n                    systemctl stop mariadb\n                    mv -f /var/lib/mysql ${MYSQL_BACKUP_DIR}/var/lib/\n                    cp -f /etc/my.cnf* ${MYSQL_BACKUP_DIR}/etc/\n                    cp -rf /etc/my.cnf* ${MYSQL_BACKUP_DIR}/etc/\n                    cp -rf /etc/mysql* ${MYSQL_BACKUP_DIR}/etc/\n                    cp -f \\${HOME}/.my.cnf* ${MYSQL_BACKUP_DIR}/\\${HOME}/\n\n                    curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash\n                    if cmdExists 'dnf'; then\n                        dnf install -y MariaDB-client MariaDB-server\n                    else\n                        yum install -y MariaDB-client MariaDB-server\n                    fi\n\n                    if (( \\$? != 0 )); then\n                        printerr \"failed installing requirements on remote node ${HOST_LIST[$i]}\"\n                        exit 1\n                    fi\n\n                    # on CentOS mariadb fresh install doesn't have socket connection\n                    # we need this to allow unix socket as fallback (parsed last in configs)\n                    if ! grep -q '\\[mysqld\\]' /etc/my.cnf 2>/dev/null; then\n                        (cat <<'EOF'\n[mysqld]\nuser                = mysql\npid-file            = /var/lib/mysql/mysql.pid\nsocket              = /var/lib/mysql/mysql.sock\nport                = 3306\nbasedir             = /usr\ndatadir             = /var/lib/mysql\nbind-address        = 127.0.0.1\ntmpdir              = /tmp\nlog_error           = /var/log/mysql/error.log\nexpire_logs_days    = 10\nmax_binlog_size     = 100M\nplugin-load-add     = auth_socket.so\nskip-external-locking\n\n[mysqld_safe]\nlog-error           = /var/log/mysql/error.log\npid-file            = /var/lib/mysql/mysql.pid\n\nEOF\n                        ) > /etc/my.cnf.d/server.cnf\n\n                        # make other configs lower priority\n                        mv -f /etc/my.cnf.d/server.cnf /etc/my.cnf.d/50-server.cnf\n                        mv -f /etc/my.cnf.d/mysql-clients.cnf /etc/my.cnf.d/50-mysql-clients.cnf\n                    fi\n\n                    systemctl enable mariadb\n                    systemctl start mariadb\n\n                    mysqladmin --user='root' password 'temp'\n\n                    # automate mysql_secure_install cmds\n                    mysqlSecureInstall \"temp\" \"${MYSQL_PASS}\"\n\n                    # allow auto login for root\n                    printf '%s\\n%s\\n%s\\n' '[client]' 'user = root' 'password = ${MYSQL_PASS}' > ~/.my.cnf\n\n                else\n                    echo 'MYSQL_MERGE_ACTION=2' >>\\${STATE_FILE}\n\n                    # make sure root can login remotely\n                    mysql --user='root' --password='${MYSQL_PASS}' mysql \\\\\n                        -e 'CREATE USER IF NOT EXISTS \"root\"@\"%\";' \\\\\n                        -e 'SET PASSWORD FOR \"root\"@\"%\" = PASSWORD(\"${MYSQL_PASS}\");' \\\\\n                        -e 'GRANT ALL PRIVILEGES ON *.* TO \"root\"@\"%\" WITH GRANT OPTION;' \\\\\n                        -e 'FLUSH PRIVILEGES;'\n                fi\n            else\n                # check repo version and update if needed\n                if (( \\$(echo \"\\${MYSQL_VER:-0} < ${MYSQL_REQ_VER}\" | bc -l) )); then\n                    curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash\n                fi\n                if cmdExists 'dnf'; then\n                    dnf install -y MariaDB-client MariaDB-server\n                else\n                    yum install -y MariaDB-client MariaDB-server\n                fi\n\n                if (( \\$? != 0 )); then\n                    printerr \"failed installing requirements on remote node ${HOST_LIST[$i]}\"\n                    exit 1\n                fi\n\n                # on CentOS mariadb fresh install doesn't have socket connection\n                # we need this to allow unix socket as fallback (parsed last in configs)\n                if ! grep -q '\\[mysqld\\]' /etc/my.cnf 2>/dev/null; then\n                    (cat <<'EOF'\n[mysqld]\nuser                = mysql\npid-file            = /var/lib/mysql/mysql.pid\nsocket              = /var/lib/mysql/mysql.sock\nport                = 3306\nbasedir             = /usr\ndatadir             = /var/lib/mysql\nbind-address        = 127.0.0.1\ntmpdir              = /tmp\nlog_error           = /var/log/mysql/error.log\nexpire_logs_days    = 10\nmax_binlog_size     = 100M\nplugin-load-add     = auth_socket.so\nskip-external-locking\n\n[mysqld_safe]\nlog-error           = /var/log/mysql/error.log\npid-file            = /var/lib/mysql/mysql.pid\n\nEOF\n                    ) > /etc/my.cnf.d/server.cnf\n\n                    # make other configs lower priority\n                    mv -f /etc/my.cnf.d/server.cnf /etc/my.cnf.d/50-server.cnf\n                    mv -f /etc/my.cnf.d/mysql-clients.cnf /etc/my.cnf.d/50-mysql-clients.cnf\n                fi\n\n                systemctl enable mariadb\n                systemctl start mariadb\n\n                mysqladmin --user='root' password 'temp'\n\n                # automate mysql_secure_install cmds\n                mysqlSecureInstall \"temp\" \"${MYSQL_PASS}\"\n\n                # allow auto login for root user\n                printf '%s\\n%s\\n%s\\n' '[client]' 'user = root' 'password = ${MYSQL_PASS}' > ~/.my.cnf\n            fi\n\n            # make sure mysql is listening for external connections prior to dumping databases\n            if ! grep -qoP '^(?!#)bind-address[ \\t]*=[ \\t]*0\\.0\\.0\\.0' /etc/my.cnf.d/50-server.cnf; then\n                perl -i -pe 's%^(?!#)(bind-address[ \\t]*=[ \\t]*).*\\$%\\${1}0.0.0.0%m' /etc/my.cnf.d/50-server.cnf\n                systemctl restart mariadb\n            fi\n\n        else\n            printerr \"Your OS Distro is currently not supported\"\n            exit 1\n        fi\n\n        # if mysql is not local to kamailio allow remote auth for kamailio user\n        if (( ${WITH_REMOTE_DB} == 1 )); then\n             mysql -sN -A -e \"SELECT CONCAT('SHOW GRANTS FOR ''',user,'''@''',host,''';') FROM mysql.user WHERE user<>'' AND host='localhost' AND user LIKE 'kamailio%';\" \\\\\n                | mysql -sN -A \\\\\n                | sed 's/$/;/g' \\\\\n                | awk '!x[\\$0]++' \\\\\n                | sed 's/localhost/%/g' \\\\\n                | mysql\n        fi\n\n        printdbg 'updating firewall rules'\n        setFirewallRules\n\n        # store the new root user password for next iteration to use\n        echo \"MYSQL_PASS='\\$MYSQL_PASS'\" >>\\${STATE_FILE}\n\n        # remove signal handler since we were successful\n        trap - EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n\n        exit 0\nEOSSH\n    ) 2>&1\n\n    if (( $? != 0 )); then\n        printerr \"preparing node for cluster install failed on ${HOST_LIST[$i]}\"\n        exit 1\n    fi\n\n    i=$((i+1))\ndone\n\n# loop through args and configure galera cluster\ni=0\nwhile (( $i < ${#NODES[@]} )); do\n    # run commands through ssh\n    (${SSH_CMD_LIST[$i]} ${USERHOST_LIST[$i]} \"$(typeset -f runas); runas bash\"  <<- EOSSH\n        if (( $DEBUG == 1 )); then\n            set -x\n        fi\n\n        # re-declare functions and vars we pass to remote server\n        # note that variables in function definitions (from calling environement)\n        # lose scope unless local to function, they must be passed to remote\n        ESC_SEQ=\"\\033[\"\n        ANSI_NONE=\"\\${ESC_SEQ}39;49;00m\" # Reset colors\n        ANSI_RED=\"\\${ESC_SEQ}1;31m\"\n        ANSI_GREEN=\"\\${ESC_SEQ}1;32m\"\n        MYSQL_USER=\"$MYSQL_USER\"\n        MYSQL_PASS=\"\"\n        MYSQL_PORT=\"$MYSQL_PORT\"\n        GALERA_REPL_PORT=\"$GALERA_REPL_PORT\"\n        GALERA_INCR_PORT=\"$GALERA_INCR_PORT\"\n        GALERA_SNAP_PORT=\"$GALERA_SNAP_PORT\"\n        $(typeset -f printdbg)\n        $(typeset -f printerr)\n        $(typeset -f cmdExists)\n        $(typeset -f getClusterSize)\n        $(typeset -f dumpMysqlDatabases)\n\n        # get the stored state for this node\n        STATE_FILE=\"${MYSQL_BACKUP_DIR}/state/${HOST_LIST[$i]}\"\n        . \\${STATE_FILE}\n\n        # export and merge databases from the primary node\n        SQLCREDS=()\n        case \\$MYSQL_MERGE_ACTION in\n            0)\n                printdbg 'no database export needed on ${HOST_LIST[$i]} using address ${CLUSTER_NODE_ADDRS[$i]}'\n                ;;\n            1)\n                printdbg 'exporting database overwrite on ${HOST_LIST[$i]} using address ${CLUSTER_NODE_ADDRS[$i]}'\n                dumpMysqlDatabases --full ${MYSQL_USER}:${MYSQL_PASS}@localhost:${MYSQL_PORT} >${MYSQL_BACKUP_DIR}/dumps/primary.sql &&\n                dumpMysqlDatabases --grants ${MYSQL_USER}:${MYSQL_PASS}@localhost:${MYSQL_PORT} >>${MYSQL_BACKUP_DIR}/dumps/primary.sql || {\n                    printerr 'failed exporting databases'\n                    exit 1\n                }\n                ;;\n            2)\n                printdbg 'exporting merged databases on ${HOST_LIST[$i]} using address ${CLUSTER_NODE_ADDRS[$i]}'\n\n                for IP in ${CLUSTER_NODE_ADDRS[@]}; do\n                    SQLCREDS+=(${MYSQL_USER}:${MYSQL_PASS}@\\${IP}:${MYSQL_PORT})\n                done\n\n                dumpMysqlDatabases --merge \\${SQLCREDS[@]} >${MYSQL_BACKUP_DIR}/dumps/primary.sql &&\n                dumpMysqlDatabases --grants ${MYSQL_USER}:${MYSQL_PASS}@localhost:${MYSQL_PORT} >>${MYSQL_BACKUP_DIR}/dumps/primary.sql || {\n                    printerr 'failed exporting databases'\n                    exit 1\n                }\n                ;;\n        esac\n\n        # merge databases if needed\n        if (( $i == 0 )) && (( \\$MYSQL_MERGE_ACTION != 0 )); then\n            printdbg 'importing merged databases on primary node'\n\n            mysql < ${MYSQL_BACKUP_DIR}/dumps/primary.sql\n        else\n            printdbg 'importing databases on ${HOST_LIST[$i]}'\n\n            case \\$MYSQL_MERGE_ACTION in\n                0)\n                    dumpMysqlDatabases --grants ${MYSQL_USER}:${MYSQL_PASS}@${CLUSTER_NODE_ADDRS[0]}:${MYSQL_PORT} >${MYSQL_BACKUP_DIR}/dumps/import.sql &&\n                    mysql <${MYSQL_BACKUP_DIR}/dumps/import.sql || {\n                        printerr 'failed importing databases'\n                        exit 1\n                    }\n                    ;;\n                1)\n                    dumpMysqlDatabases --full ${MYSQL_USER}:${MYSQL_PASS}@${CLUSTER_NODE_ADDRS[0]}:${MYSQL_PORT} >${MYSQL_BACKUP_DIR}/dumps/import.sql &&\n                    dumpMysqlDatabases --grants ${MYSQL_USER}:${MYSQL_PASS}@${CLUSTER_NODE_ADDRS[0]}:${MYSQL_PORT} >>${MYSQL_BACKUP_DIR}/dumps/import.sql &&\n                    mysql <${MYSQL_BACKUP_DIR}/dumps/import.sql || {\n                        printerr 'failed importing databases'\n                        exit 1\n                    }\n                    ;;\n                2)\n                    dumpMysqlDatabases --full ${MYSQL_USER}:${MYSQL_PASS}@${CLUSTER_NODE_ADDRS[0]}:${MYSQL_PORT} >${MYSQL_BACKUP_DIR}/dumps/import.sql &&\n                    dumpMysqlDatabases --grants ${MYSQL_USER}:${MYSQL_PASS}@localhost:${MYSQL_PORT} >>${MYSQL_BACKUP_DIR}/dumps/import.sql &&\n                    mysql <${MYSQL_BACKUP_DIR}/dumps/import.sql || {\n                        printerr 'failed importing databases'\n                        exit 1\n                    }\n                    ;;\n            esac\n        fi\n\n        systemctl stop mariadb\n\n        printdbg 'configuring galera cluster settings'\n        ( cat << EOF\n[\\${MYSQL_SECTION}]\nbinlog_format=row\ndefault-storage-engine=innodb\ninnodb_autoinc_lock_mode=2\nbind-address=0.0.0.0\nlog_error=/var/log/mysql/error.log\n\n# Galera Provider Configuration\nwsrep_on=ON\nwsrep_provider=\\$(find /usr/lib{32,64,}{/x86_64*,/i386*,}/galera/ -name 'libgalera_smm*.so' -print -quit 2>/dev/null)\n\n# Galera Cluster Configuration\nwsrep_cluster_name=\"${CLUSTER_NAME}\"\nwsrep_cluster_address=\"gcomm://$(join ',' ${NODE_NAMES[@]})\"\n\n# Galera Synchronization Configuration\nwsrep_sst_method=rsync\n\n# Galera Node Configuration\nwsrep_node_address=\"${NODE_NAMES[$i]}\"\nwsrep_node_name=\"${NODE_NAMES[$i]}\"\nEOF\n        ) > \\${MYSQL_CLUSTER_CONFIG}\n\n        # fix debian.cnf to be the same on all nodes (used by debian-maintenance)\n        if [[ -e \"/etc/mysql/debian.cnf\" ]]; then\n            sed -i -r \\\\\n                -e \"s|(host[\\t ]*\\=[\\t ]*).*|host = localhost|g\" \\\\\n                -e \"s|(user[\\t ]*\\=[\\t ]*).*|user = ${MYSQL_USER}|g\" \\\\\n                -e \"s|(password[\\t ]*\\=[\\t ]*).*|password = ${MYSQL_PASS}|g\" \\\\\n                /etc/mysql/debian.cnf\n        fi\n\n        # startup mysql server\n        if (( $i == 0 )); then\n            # bootstrap first db node\n            perl -i -pe 's%(safe_to_bootstrap:)[ \\t]*[0-9]%\\1 1%' /var/lib/mysql/grastate.dat\n            galera_new_cluster\n            if (( \\$? == 0 )) && (( \\$(getClusterSize) == 1 )); then\n                printdbg \"Bootstrapping cluster success\"\n            else\n                printerr \"Bootstrapping cluster failed [${NODE_NAMES[$i]}]\"\n                exit 1\n            fi\n        else\n            # restart service to connect other db nodes\n            systemctl restart mariadb\n            if (( \\$? == 0 )) && (( \\$(getClusterSize) >= 2 )); then\n                printdbg \"Adding Node to cluster success\"\n            else\n                printerr \"Adding Node to cluster failed [${NODE_NAMES[$i]}]\"\n                exit 1\n            fi\n        fi\n\n        # remove the state file after successful configuration\n        rm -f \\${STATE_FILE}\n\n        exit 0\nEOSSH\n    ) 2>&1\n\n    if (( $? != 0 )); then\n        printerr \"galera configuration failed on ${HOST_LIST[$i]}\"\n        exit 1\n    fi\n\n    i=$((i+1))\ndone\n\nexit 0"
  },
  {
    "path": "HA/mysql/installAAGroupReplication.sh",
    "content": "#!/usr/bin/env bash\n#\n# Summary:      mysql active active group replication\n# Supported OS: debian\n# Author:       DevOpSec <tmoore@goflyball.com>\n# Date:         Feb-2019\n# Notes:        uses mysql community db\n#\n\n# TODO: configure from remote server using ssh keys\n# TODO: add support for rhel-based distros\n\n######################### warn user and exit #########################\n{\n    echo \"\"\n    printerr \"Mysql Group Replication install is under construction\"\n    echo \"\"\n    printwarn \"Use this script AT YOUR OWN RISK\"\n    echo \"\"\n    echo \"We suggest you use Mysql Galera Replication (install script included in repo) at this time\"\n    echo \"If you must have Mysql Group Replication, READ THE ENTIRE script and FOLLOW THE COMMENTS\"\n    echo \"\"\n    exit 1\n}\n######################################################################\n\n# set project root, if in a git repo resolve top level dir\nPROJECT_ROOT=${PROJECT_ROOT:-$(dirname $(dirname $(dirname $(readlink -f \"$0\"))))}\n# import shared library functions\n. ${PROJECT_ROOT}/HA/shared_lib.sh\n\n\n# node configuration settings\nMYSQL_PORT=\"3306\"\nREPL_PORT=\"6606\"\n\n\nprintUsage() {\n    pprint \"Usage: $0 [-remotedb|-h|--help] <[user1[:pass1]@]node1[:port1]> <[user2[:pass2]@]node2[:port2]> ...\"\n}\n\nif ! isRoot; then\n    printerr \"Must be run with root privileges\" && exit 1\nfi\n\nif (( $# < 2 )) || [[ \"$1\" == \"-h\" ]] || [[ \"$1\" == \"--help\" ]]; then\n    printerr \"At least 2 nodes are required to setup replication\" && printUsage && exit 1\nfi\n\nsetOSInfo\n# install local requirements for script\ncase \"$DISTRO\" in\n    debian|ubuntu|linuxmint)\n        apt-get install -y sshpass gawk\n        ;;\n    centos|redhat|amazon)\n        yum install -y epel-release\n        yum install -y sshpass gawk\n        ;;\n    *)\n        printerr \"Your OS Distro is currently not supported\"\n        exit 1\n        ;;\nesac\n\n\n##### shared between ALL NODES (set per your own configs)\n# get using: uuidgen\nGROUP_ID=\"2ff15901-0fda-4ea8-9391-51fbb53ae28d\"\nIP_LIST=\"10.10.2.40,10.10.2.41\"\nSEED_LIST=\"10.10.2.40:${REPL_PORT},10.10.2.41:${REPL_PORT}\"\n########################################\n\n##### install requirements FOR EACH NODE\nMYSQL_APT_CONFIG_VER=\"0.8.10-1\"\nMYSQL_APT_CONFIG_NAME=\"mysql-apt-config_${MYSQL_APT_CONFIG_VER}_all.deb\"\nMYSQL_APT_CONFIG_URL=\"https://dev.mysql.com/get/${MYSQL_APT_CONFIG_NAME}\"\nIP4RESTORE_FILE=\"/etc/iptables/rules.v4\"\nIP6RESTORE_FILE=\"/etc/iptables/rules.v6\"\nwget ${MYSQL_APT_CONFIG_URL}\ndpkg --force-depends -i ${MYSQL_APT_CONFIG_NAME}\napt-get update -y\napt-get install -y libaio1\napt-get install -y libmecab2\napt-get install -y mysql-common mysql-client mysql-server\napt-get install -y uuid-runtime\napt-get install -y iptables-persistent netfilter-persistent\napt-get -f install -y\n########################################\n\n##### set firewall rules FOR EACH NODE\n# use firewalld if installed\nif cmdExists \"firewall-cmd\"; then\n    firewall-cmd --zone=public --add-port=${MYSQL_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${REPL_PORT}/tcp --permanent\n    firewall-cmd --reload\nelse\n    # set ipv4 firewall rules for each node\n    iptables -I INPUT 1 -p tcp --dport ${MYSQL_PORT} -j ACCEPT\n    iptables -I INPUT 1 -p tcp --dport ${REPL_PORT} -j ACCEPT\n    # set ipv6 firewall rules for each node\n    ip6tables -I INPUT 1 -p tcp --dport ${MYSQL_PORT} -j ACCEPT\n    ip6tables -I INPUT 1 -p tcp --dport ${REPL_PORT} -j ACCEPT\nfi\n\n# Remove duplicates and save\nmkdir -p $(dirname ${IP4RESTORE_FILE})\niptables-save | awk '!x[$0]++' > ${IP4RESTORE_FILE}\nmkdir -p $(dirname ${IP6RESTORE_FILE})\nip6tables-save | awk '!x[$0]++' > ${IP6RESTORE_FILE}\n########################################\n\n##### set dynamic variables FOR EACH NODE (id increments per node)\nID=1\nINTERNAL_IP=$(hostname -I | awk '{print $1}')\n########################################\n\n##### add cluster config FOR EACH NODE\ncat << EOF > /etc/mysql/conf.d/cluster.cnf\n[mysqld]\nbind-address=0.0.0.0\n\n# General replication settings\nplugin-load = group_replication.so\ngtid_mode = ON\nenforce_gtid_consistency = ON\nmaster_info_repository = TABLE\nrelay_log_info_repository = TABLE\nbinlog_checksum = NONE\nlog_slave_updates = ON\nlog_bin = binlog\nbinlog_format = ROW\n# prevent use of non-transactional storage engines\ndisabled_storage_engines=\"MyISAM,BLACKHOLE,FEDERATED,ARCHIVE\"\ntransaction_write_set_extraction = XXHASH64\n# InnoDB gap locks are problematic for multi-primary conflict detection,\n# but none are used with READ-COMMITTED so isolate those\ntransaction-isolation = 'READ-COMMITTED'\ngroup_replication = FORCE_PLUS_PERMANENT\ngroup_replication_bootstrap_group = OFF\ngroup_replication_start_on_boot = ON\n\n# Shared replication group configuration\ngroup_replication_group_name = \"${GROUP_ID}\"\ngroup_replication_ip_whitelist = \"${IP_LIST}\"\ngroup_replication_group_seeds = \"${SEED_LIST}\"\n\n# Multi-primary mode, where any host can accept writes\ngroup_replication_single_primary_mode = OFF\ngroup_replication_enforce_update_everywhere_checks = ON\n\n# Host specific replication configuration\nserver_id = ${ID}\ngroup_replication_local_address = \"${INTERNAL_IP}:${REPL_PORT}\"\nEOF\n\n# restart mysql with new config loaded\nMYSQL_SERVICES=$(systemctl list-unit-files | grep 'mysql' | awk '{print $1}')\nfor SERVICE in $MYSQL_SERVICES; do systemctl restart ${SERVICE}; done\n########################################\n\n##### set logging and privileges ON PRIMARY NODE\nmysql <<- 'EOF'\nSET SQL_LOG_BIN=0;\nALTER USER IF EXISTS kamailio@'%' IDENTIFIED WITH 'mysql_native_password' BY 'kamailiorw';\nFLUSH PRIVILEGES;\nCHANGE MASTER TO MASTER_USER='kamailio', MASTER_PASSWORD='kamailiorw' FOR CHANNEL 'group_replication_recovery';\n# Parallel applier support -- Speedup distributed recovery time??\n#SET @@GLOBAL.binlog_transaction_dependency_tracking=WRITESET;\n#SET @@GLOBAL.binlog_transaction_dependency_tracking=WRITESET_SESSION;\nSET @@GLOBAL.binlog_transaction_dependency_tracking=COMMIT_ORDER; #default\nSET SQL_LOG_BIN=1;\nEOF\n########################################\n\n##### dump db ON PRIMARY NODE\n# TODO: change this to use functions from shared_lib\nmysqldump --single-transaction --opt --events --routines --triggers --all-databases --add-drop-database --flush-privileges > repl_dump.sql\nmysql -sN -A -e \"SELECT CONCAT('SHOW GRANTS FOR ''',user,'''@''',host,''';') FROM mysql.user WHERE user<>''\" \\\n    | mysql -sN -A \\\n    | sed 's/$/;/g' \\\n    | awk '!x[$0]++' >> repl_dump.sql\n########################################\n\n\n##### import db dump to ALL NON-PRIMARY NODES\nmysql --init-command=\"RESET MASTER\" < repl_dump.sql\n########################################\n\n##### bootstrap the PRIMARY NODE\nmysql <<- 'EOF'\nSET GLOBAL group_replication_bootstrap_group=ON;\nSTART GROUP_REPLICATION;\nSET GLOBAL group_replication_bootstrap_group=OFF;\nEOF\n########################################\n\n### run replication on ALL NON-PRIMARY NODES\nmysql -e 'START GROUP_REPLICATION'\n####################################\n\n# NOTE: if a node becomes out of sync and can not rejoin (irrecoverable GTID's out of sync)\n#       here is the process to resync an out of sync server:\n# 1. dump one of the good databases\n# 2. import to down server (shown below)\n# 3. resync with the group (shown below)\n#\n#mysql --init-command=\"RESET MASTER\" < repl_dump.sql\n#mysql -e 'START GROUP_REPLICATION'\n\n# check the status of group\nmysql -e 'SELECT * FROM performance_schema.replication_group_members'\n\nexit 0"
  },
  {
    "path": "HA/pacemaker/AWS/ocf-floatip",
    "content": "#!/bin/sh\n#\n#\n# Manage Elastic IP with Pacemaker\n#\n#\n# Copyright 2016-2018 guessi <guessi@gmail.com>\n# Copyright 2024 Tyler Moore <tmoore@dopensource.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n#\n#\n# Prerequisites:\n#   - preconfigured AWS CLI running environment (AccessKey, SecretAccessKey, etc.) or\n#     (AWSRole) Setup up relevant AWS Policies to allow agent related functions to be executed.\n#   - a reserved secondary private IP address for EC2 instances high availability\n#     this secondary IP should be assigned to the primary NIC\n#   - IAM user role with the following permissions:\n#     * DescribeInstances\n#     * AssociateAddress\n#     * DescribeAddresses\n#     * DisassociateAddress\n#   - jq is required for parsing json\n#\n\n#######################################################################\n# Initialization:\n\n: ${OCF_ROOT=${OCF_ROOT:-\"/usr/lib/ocf\"}}\n: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/resource.d/heartbeat}\n. ${OCF_FUNCTIONS_DIR}/.ocf-shellfuncs\n\n#######################################################################\n\n#\n# Defaults\n#\nOCF_RESKEY_awscli_default=\"/usr/bin/aws\"\nOCF_RESKEY_auth_type_default=\"key\"\nOCF_RESKEY_profile_default=\"default\"\nOCF_RESKEY_region_default=\"\"\nOCF_RESKEY_api_delay_default=\"3\"\n\n: ${OCF_RESKEY_awscli=${OCF_RESKEY_awscli_default}}\n: ${OCF_RESKEY_auth_type=${OCF_RESKEY_auth_type_default}}\n: ${OCF_RESKEY_profile=${OCF_RESKEY_profile_default}}\n: ${OCF_RESKEY_region=${OCF_RESKEY_region_default}}\n: ${OCF_RESKEY_api_delay=${OCF_RESKEY_api_delay_default}}\n\nmeta_data() {\n    cat <<XML\n<?xml version=\"1.0\"?>\n<!DOCTYPE resource-agent SYSTEM \"ra-api-1.dtd\">\n<resource-agent name=\"aws_floatip\" version=\"0.1\">\n  <version>1.0</version>\n  <longdesc lang=\"en\">\n  Resource Agent for Amazon AWS Elastic IP Addresses.\n\n  It manages AWS Elastic IP Addresses with awscli.\n\n  Credentials needs to be setup by running \"aws configure\", or by using AWS Policies.\n\n  See https://aws.amazon.com/cli/ for more information about awscli.\n  </longdesc>\n  <shortdesc lang=\"en\">Amazon AWS Elastic IP Address Resource Agent</shortdesc>\n  <parameters>\n    <parameter name=\"awscli\" unique=\"0\">\n      <longdesc lang=\"en\">\n      Path to the \"aws\" executable.\n      </longdesc>\n      <shortdesc lang=\"en\">aws cli</shortdesc>\n      <content type=\"string\" default=\"${OCF_RESKEY_awscli_default}\"/>\n    </parameter>\n    <parameter name=\"auth_type\">\n      <longdesc lang=\"en\">\n      Authentication type \"key\" for AccessKey and SecretAccessKey set via \"aws configure\",\n      or \"role\" to use AWS Policies.\n      </longdesc>\n      <shortdesc lang=\"en\">Authentication type</shortdesc>\n      <content type=\"string\" default=\"${OCF_RESKEY_auth_type_default}\"/>\n    </parameter>\n    <parameter name=\"profile\">\n      <longdesc lang=\"en\">\n      Valid AWS CLI profile name (see ~/.aws/config and 'aws configure')\n      </longdesc>\n      <shortdesc lang=\"en\">profile name</shortdesc>\n      <content type=\"string\" default=\"${OCF_RESKEY_profile_default}\"/>\n    </parameter>\n    <parameter name=\"elastic_ip\" unique=\"1\" required=\"1\">\n      <longdesc lang=\"en\">\n      Reserved elastic IP for ec2 instance.\n      </longdesc>\n      <shortdesc lang=\"en\">reserved elastic ip address</shortdesc>\n      <content type=\"string\" default=\"\" />\n    </parameter>\n    <parameter name=\"private_ip\" unique=\"1\" required=\"0\">\n      <longdesc lang=\"en\">\n      Secondary private ip address for ec2 instance to bind elastic IP to.\n      If not set, the resource will attempt to resolve it from the pacemaker configs.\n      </longdesc>\n      <shortdesc lang=\"en\">secondary private ip address</shortdesc>\n      <content type=\"string\" default=\"\"/>\n    </parameter>\n    <parameter name=\"region\" required=\"0\">\n      <longdesc lang=\"en\">\n      Region for AWS resource (required for role-based authentication).\n      </longdesc>\n      <shortdesc lang=\"en\">Region</shortdesc>\n      <content type=\"string\" default=\"${OCF_RESKEY_region_default}\"/>\n    </parameter>\n    <parameter name=\"api_delay\" unique=\"0\">\n      <longdesc lang=\"en\">\n      A short delay between API calls, to avoid sending API too quick.\n      </longdesc>\n      <shortdesc lang=\"en\">a short delay between API calls</shortdesc>\n      <content type=\"integer\" default=\"${OCF_RESKEY_api_delay_default}\"/>\n    </parameter>\n  </parameters>\n  <actions>\n    <action name=\"start\"        timeout=\"30s\"/>\n    <action name=\"stop\"         timeout=\"30s\"/>\n    <action name=\"monitor\"      timeout=\"30s\" interval=\"20s\" depth=\"0\"/>\n    <action name=\"migrate_to\"   timeout=\"30s\"/>\n    <action name=\"migrate_from\" timeout=\"30s\"/>\n    <action name=\"meta-data\"    timeout=\"5s\"/>\n    <action name=\"validate\"     timeout=\"10s\"/>\n    <action name=\"validate-all\" timeout=\"10s\"/>\n  </actions>\n</resource-agent>\nXML\n}\n\n#######################################################################\n\n\n\nawseip_usage() {\n    echo \"usage: $0 {start|stop|monitor|migrate_to|migrate_from|validate|validate-all|meta-data}\"\n}\n\nawseip_start() {\n    awseip_monitor && return $OCF_SUCCESS\n\n    if [ -z \"${PRIVATE_IP}\" ]; then\n        # TODO: make this the secondary / float ip, probably set in /etc/hosts\n        PRIVATE_IP=$(getent hosts \"$(crm_node -n)\" 2>/dev/null | awk '{print $1}' | head -1)\n    fi\n\n    local ENI_MAC=$(\n        ip -j address show |\n        jq -re --arg ip $PRIVATE_IP '.[] | select(.addr_info[].local==$ip).address'\n    )\n    if [ -z \"$ENI_MAC\" ]; then\n        ocf_log info \"NIC for private IP ($PRIVATE_IP) not found while starting aws_floatip\"\n        return $OCF_NOT_RUNNING\n    fi\n\n    local ENI_ID=$(\n        curl -s -H \"X-aws-ec2-metadata-token: $TOKEN\" \\\n            \"http://169.254.169.254/latest/meta-data/network/interfaces/macs/${ENI_MAC}/interface-id\"\n    )\n\n    $AWSCLI_CMD ec2 associate-address  \\\n        --allocation-id ${ALLOCATION_ID} \\\n        --network-interface-id ${ENI_ID} \\\n        --private-ip-address ${PRIVATE_IP}\n    RET=$?\n\n    # delay to avoid sending request too fast\n    sleep ${OCF_RESKEY_api_delay}\n\n    if [ $RET -ne 0 ]; then\n        return $OCF_NOT_RUNNING\n    fi\n\n    ocf_log info \"aws_floatip has been successfully brought up (${ELASTIC_IP})\"\n    return $OCF_SUCCESS\n}\n\nawseip_stop() {\n    awseip_monitor || return $OCF_SUCCESS\n\n    local ASSOCIATION_ID=$(\n        $AWSCLI_CMD ec2 describe-addresses --allocation-id \"$ALLOCATION_ID\" --output json |\n         jq -re '.Addresses[0].AssociationId'\n    )\n\n    if [ -z \"$ASSOCIATION_ID\" ]; then\n        ocf_log info \"ASSOCIATION_ID not found while stopping aws_floatip\"\n        return $OCF_NOT_RUNNING\n    fi\n\n    $AWSCLI_CMD ec2 disassociate-address --association-id \"$ASSOCIATION_ID\"\n    RET=$?\n\n    # delay to avoid sending request too fast\n    sleep ${OCF_RESKEY_api_delay}\n\n    if [ $RET -ne 0 ]; then\n        return $OCF_NOT_RUNNING\n    fi\n\n    ocf_log info \"elastic_ip has been successfully brought down (${ELASTIC_IP})\"\n    return $OCF_SUCCESS\n}\n\nawseip_monitor() {\n    $AWSCLI_CMD ec2 describe-instances --instance-id \"$INSTANCE_ID\" --output json |\n    jq -re '.Reservations[].Instances[0].NetworkInterfaces[].PrivateIpAddresses[].Association.PublicIp | select(. == \"3.19.8.113\")' >/dev/null\n\n    if [ $? -eq 0 ]; then\n        return $OCF_SUCCESS\n    fi\n\n    return $OCF_NOT_RUNNING\n}\n\nawseip_validate() {\n    check_binary \"${OCF_RESKEY_awscli}\"\n\n    if [ \"x${OCF_RESKEY_auth_type}\" = \"xkey\" ] && [ -z \"$OCF_RESKEY_profile\" ]; then\n        ocf_exit_reason \"profile parameter not set\"\n        return $OCF_ERR_CONFIGURED\n    fi\n\n    if [ -z \"${INSTANCE_ID}\" ]; then\n        ocf_exit_reason \"instance_id not found. Is this a EC2 instance?\"\n        return $OCF_ERR_GENERIC\n    fi\n\n    return $OCF_SUCCESS\n}\n\ncase $__OCF_ACTION in\n    meta-data)\n        meta_data\n        exit $OCF_SUCCESS\n        ;;\n    usage|help)\n        awseip_usage\n        exit $OCF_SUCCESS\n        ;;\nesac\n\nAWSCLI_CMD=\"${OCF_RESKEY_awscli}\"\nif [ \"x${OCF_RESKEY_auth_type}\" = \"xkey\" ]; then\n\tAWSCLI_CMD=\"$AWSCLI_CMD --profile ${OCF_RESKEY_profile}\"\nelif [ \"x${OCF_RESKEY_auth_type}\" = \"xrole\" ]; then\n\tif [ -z \"${OCF_RESKEY_region}\" ]; then\n\t\tocf_exit_reason \"region needs to be set when using role-based authentication\"\n\t\texit $OCF_ERR_CONFIGURED\n\tfi\nelse\n\tocf_exit_reason \"Incorrect auth_type: ${OCF_RESKEY_auth_type}\"\n\texit $OCF_ERR_CONFIGURED\nfi\nif [ -n \"${OCF_RESKEY_region}\" ]; then\n\tAWSCLI_CMD=\"$AWSCLI_CMD --region ${OCF_RESKEY_region}\"\nfi\nELASTIC_IP=\"${OCF_RESKEY_elastic_ip}\"\nPRIVATE_IP=\"${OCF_RESKEY_private_ip}\"\nTOKEN=$(curl -sX PUT \"http://169.254.169.254/latest/api/token\" -H \"X-aws-ec2-metadata-token-ttl-seconds: 21600\")\nINSTANCE_ID=$(curl -s \"http://169.254.169.254/latest/meta-data/instance-id\" -H \"X-aws-ec2-metadata-token: $TOKEN\")\nALLOCATION_ID=$($AWSCLI_CMD ec2 describe-addresses --public-ips \"$ELASTIC_IP\" --output json | jq -re '.Addresses[0].AllocationId')\n\ncase $__OCF_ACTION in\n    start)\n        awseip_validate\n        awseip_start\n        ;;\n    stop)\n        awseip_stop\n        ;;\n    monitor)\n        awseip_monitor\n        ;;\n    migrate_to)\n        ocf_log info \"Migrating ${OCF_RESOURCE_INSTANCE} to ${OCF_RESKEY_CRM_meta_migrate_target}.\"\n        awseip_stop\n        ;;\n    migrate_from)\n        ocf_log info \"Migrating ${OCF_RESOURCE_INSTANCE} from ${OCF_RESKEY_CRM_meta_migrate_source}.\"\n        awseip_start\n        ;;\n    reload)\n        ocf_log info \"Reloading ${OCF_RESOURCE_INSTANCE} ...\"\n        ;;\n    validate|validate-all)\n        awseip_validate\n        ;;\n    *)\n        awseip_usage\n        exit $OCF_ERR_UNIMPLEMENTED\n        ;;\nesac\n\nrc=$?\nocf_log debug \"${OCF_RESOURCE_INSTANCE} $__OCF_ACTION : $rc\"\nexit $rc\n"
  },
  {
    "path": "HA/pacemaker/DO/assign-ip",
    "content": "#!/usr/bin/env bash\n\n# derived from http://do.co/ocf-floatip\n\napi_base='https://api.digitalocean.com/v2'\n\n\nusage() {\n    echo \"$0 <Floating IP>\"\n    echo ''\n    echo 'Your DigitialOcean API token must be in the \"DO_TOKEN\" environmental variable.'\n}\n\nmain() {\n    local floating_ip=\"$1\"\n\n    droplet_id=$(curl -s http://169.254.169.254/metadata/v1/id)\n\n    resp=$(\n        curl -s \\\n            -H \"Authorization: Bearer $DO_TOKEN\" \\\n            -H 'Content-type: application/json' \\\n            -d '{\"type\": \"assign\", \"droplet_id\": '$droplet_id'}' \\\n           \"${api_base}/floating_ips/${floating_ip}/actions\"\n    )\n    msg=$(jq -e -r '.message' <<<\"$resp\") && {\n        echo \"$(jq -r '.id' <<<\"$resp\"): $msg\"\n        return 1\n    }\n    echo \"Moving IP address: $(jq -r '.action.status' <<<\"$resp\")\"\n    return 0\n}\n\nif [[ -z \"$DO_TOKEN\" ]] || (( $# != 1 )); then\n    usage\n    exit 1\nfi\n\nmain \"$@\"\nexit $?\n\n"
  },
  {
    "path": "HA/pacemaker/DO/ocf-floatip",
    "content": "#!/bin/bash\n\n# derived from http://do.co/ocf-floatip\n\nparam=$1\n\nexport DO_TOKEN=$OCF_RESKEY_do_token\nIP=$OCF_RESKEY_floating_ip\n\nhas_floating_ip() {\n    [ \"$(curl -s http://169.254.169.254/metadata/v1/floating_ip/ipv4/active)\" == \"true\" ] &&\n    return 0 ||\n    return 1\n}\n\nmeta_data() {\n  cat <<END\n<?xml version=\"1.0\"?>\n<!DOCTYPE resource-agent SYSTEM \"ra-api-1.dtd\">\n<resource-agent name=\"floatip\" version=\"1.0\">\n  <version>0.1</version>\n  <longdesc lang=\"en\">\nfloatip ocf resource agent for claiming a specified Floating IP via the DigitalOcean API</longdesc>\n  <shortdesc lang=\"en\">Assign Floating IP via DigitalOcean API</shortdesc>\n<parameters>\n<parameter name=\"do_token\" unique=\"0\" required=\"1\">\n<longdesc lang=\"en\">\nDigitalOcean API Token with Read/Write Permissions\n</longdesc>\n<shortdesc lang=\"en\">DigitalOcean API Token</shortdesc>\n</parameter>\n<parameter name=\"floating_ip\" unique=\"0\" required=\"1\">\n<longdesc lang=\"en\">\nFloating IP to reassign\n</longdesc>\n<shortdesc lang=\"en\">Floating IP</shortdesc>\n</parameter>\n</parameters>\n  <actions>\n    <action name=\"start\"        timeout=\"20\" />\n    <action name=\"stop\"         timeout=\"20\" />\n    <action name=\"monitor\"      timeout=\"20\"\n                                interval=\"10\" depth=\"0\" />\n    <action name=\"meta-data\"    timeout=\"5\" />\n  </actions>\n</resource-agent>\nEND\n}\n\nif [ \"start\" == \"$param\" ] ; then\n    /usr/local/bin/assign-ip $IP\n    exit 0\nelif [ \"stop\" == \"$param\" ] ; then\n    exit 0\nelif [ \"status\" == \"$param\" ] ; then\n    if has_floating_ip ; then\n        echo \"Has Floating IP\"\n        exit 0\n    else\n        echo \"Does Not Have Floating IP\"\n        exit 1\n    fi\nelif [ \"monitor\" == \"$param\" ] ; then\n    if has_floating_ip ; then\n        exit 0\n    else\n        exit 7\n    fi\nelif [ \"meta-data\" == \"$param\" ] ; then\n    meta_data\n    exit 0\nelse\n    echo \"no such command $param\"\n    exit 1;\nfi\n"
  },
  {
    "path": "HA/pacemaker/installKamCluster.sh",
    "content": "#!/usr/bin/env bash\n#\n# Summary:  corosync / pacemaker kamailio cluster config\n#\n# Notes:    more than 2 nodes may require fencing settings\n#           you must be able to ssh to every node in the cluster from where script is run\n#           supported ssh authentication methods: password, pubkey\n#           supported DB configurations: central, active/active\n#           a secondary private IP address on a dedicated subnet is the preferred method\n#           for communication between the pacemaker nodes\n##\n# TODO:     support active/passive galera replication\n#           https://mariadb.com/kb/en/changing-a-replica-to-become-the-primary/\n#           better output to user when not in debug mode\n#\n### log file locations:\n# /var/log/pcsd/pcsd.log\n# /var/log/cluster/corosync.log\n# /var/log/pcsd/pacemaker.log\n# /var/log/nodeutil.log\n\n#======================================================================================\n# process and validate the CLI args\n#======================================================================================\n\n# process args before doing anything else\nprintUsage() {\n    echo \"Usage: $0 [OPTIONAL OPTIONS] <REQUIRED OPTIONS> <REQUIRED ARGUMENTS>\"\n    echo \"OPTIONAL OPTIONS:\"\n    echo \"    -h|--help\"\n    echo \"    -debug\"\n    echo \"    -i <ssh key file>\"\n    echo \"    --do-token=<your token>\"\n    echo \"    --aws-access-key=<your key>\"\n    echo \"    --aws-secret-key=<your key>\"\n    echo \"REQUIRED OPTIONS (one of):\"\n    echo \"    -vip <virtual ip>\"\n    echo \"    -net <subnet cidr>\"\n    echo \"REQUIRED ARGUMENTS:\"\n    echo \" <[sshuser1[:sshpass1]@]node1[:sshport1]> <[sshuser2[:sshpass2]@]node2[:sshport2]> ...\"\n}\n\n# loop through args and evaluate any options\nIN_NODES=()\nwhile (( $# > 0 )); do\n    ARG=\"$1\"\n    case $ARG in\n        -h|--help)\n            printUsage\n            exit 0\n            ;;\n        -debug)\n            IN_DEBUG=1\n            shift\n            ;;\n        -vip)\n            shift\n            IN_KAM_VIP=\"$1\"\n            shift\n            ;;\n        -net)\n            shift\n            IN_CIDR_FULL=\"$1\"\n            IN_CIDR_NETMASK=$(cut -s -d '/' -f 2 <<<\"$1\")\n            shift\n            ;;\n        -i)\n            shift\n            IN_SSH_KEY_FILE=\"$1\"\n            shift\n            ;;\n        --do-token=*)\n            IN_DO_TOKEN=$(cut -s -d '=' -f 2 <<<\"$1\")\n            shift\n            ;;\n        --aws-access-key=*)\n            IN_AWS_ACCESS_KEY=$(cut -s -d '=' -f 2 <<<\"$1\")\n            shift\n            ;;\n        --aws-secret-key=*)\n            IN_AWS_SECRET_TOKEN=$(cut -s -d '=' -f 2 <<<\"$1\")\n            shift\n            ;;\n        *)  # add to list of args\n            IN_NODES+=( \"$ARG\" )\n            shift\n            ;;\n    esac\ndone\n\n# make sure required args are fulfilled\nif [[ -z \"$IN_KAM_VIP\" ]] && [[ -z \"$IN_CIDR_NETMASK\" ]]; then\n    echo 'Kamailio virtual IP or CIDR network is required'\n    echo \"For usage information run: $0 --help\"\n    exit 1\nfi\n\nif (( ${#IN_NODES[@]} < 2 )); then\n    echo \"At least 2 nodes are required to setup kam cluster\"\n    echo \"For usage information run: $0 --help\"\n    exit 1\nfi\n\n# start debugging if requested\nif (( ${IN_DEBUG:-0} == 1 )); then\n    set -x\nfi\n\n#======================================================================================\n# setup the environment we will use on remote nodes\n#======================================================================================\n\n# export current environment (will be used later)\nINITIAL_ENV_FILE=$(mktemp)\nDIRTY_ENV_FILE=$(mktemp)\nCLEAN_ENV_FILE=$(mktemp)\ndeclare -p >\"$INITIAL_ENV_FILE\"\n\n# trap exit signals to remove environment files no matter how the script exits\ncleanupHandler() {\n    printdbg 'cleaning up temp files onn all hosts prior to exit'\n    # local files\n    rm -f \"$INITIAL_ENV_FILE\" \"$DIRTY_ENV_FILE\" \"$CLEAN_ENV_FILE\" 2>/dev/null\n    # remote files\n    if (( ${#SSH_CMD_LIST[@]} > 0 )); then\n        for i in $(seq 0 $((${#NODES[@]}-1))); do\n            ${SSH_CMD_LIST[$i]} ${USERHOST_LIST[$i]} \"rm -f $CLEAN_ENV_FILE\"\n        done\n    fi\n    # reset signals\n    trap - EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n}\ntrap 'cleanupHandler $?' EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n\n# set project root, if in a git repo resolve top level dir\nPROJECT_ROOT=${PROJECT_ROOT:-$(dirname $(dirname $(dirname $(readlink -f \"$0\"))))}\n# import shared library functions\nsource ${PROJECT_ROOT}/HA/shared_lib.sh\n\n# node configuration settings\nPACEMAKER_TCP_PORTS=(2224 3121 5403 21064)\nPACEMAKER_UDP_PORTS=(5404 5405)\nCLUSTER_NAME=\"kamcluster\"\nCLUSTER_PASS=\"$(createPass)\"\nRESOURCE_STARTUP_TIMEOUT=30\nCLUSTER_OPTIONS=(transport udpu link bindnetaddr=0.0.0.0 broadcast=1)\nKAM_VIP=\"$IN_KAM_VIP\"\nCIDR_NETMASK=${IN_CIDR_NETMASK:-32}\nDSIP_SYSTEM_CONFIG_DIR=\"/etc/dsiprouter\"\nSTATIC_NETWORKING_MODE=1\nRETRY_CLUSTER_START=3\nRETRY_SSH_CONNECT=3 # TODO: implement this\nPKG_MGR_TIMEOUT=300\n\n# global variables used throughout script\nNODES=( ${IN_NODES[@]} )\nDEBUG=${IN_DEBUG:-0}\nSSH_KEY_FILE=\"$IN_SSH_KEY_FILE\"\nDO_TOKEN=\"$IN_DO_TOKEN\"\nAWS_ACCESS_KEY=\"$IN_AWS_ACCESS_KEY\"\nAWS_SECRET_TOKEN=\"$IN_AWS_SECRET_TOKEN\"\nNODE_NAMES=()\nHOST_LIST=()\nUSERHOST_LIST=()\nCLUSTER_NODE_ADDRS=()\ndeclare -A CLOUD_DICT\nCLOUD_PLATFORM=\"\"\nCLUSTER_RESOURCES=()\nSSH_CMD_LIST=()\nRSYNC_CMD_LIST=()\n\n# install local requirements for script\nif ! cmdExists 'ssh' || ! cmdExists 'sshpass' || ! cmdExists 'nmap' || ! cmdExists 'sed' || ! cmdExists 'awk' || ! cmdExists 'comm'; then\n    printdbg 'Installing local requirements for cluster install'\n\n    if cmdExists 'apt-get'; then\n        runas apt-get install -y openssh-client sshpass nmap sed gawk rsync coreutils\n    elif cmdExists 'dnf'; then\n        runas dnf install -y openssh-clients sshpass nmap sed gawk rsync coreutils\n    elif cmdExists 'yum'; then\n        runas yum install --enablerepo=epel -y openssh-clients sshpass nmap sed gawk rsync coreutils\n    else\n        printerr \"Your local OS is not currently not supported\"\n        exit 1\n    fi\nfi\n\n# sanity check\nif (( $? != 0 )); then\n    printerr 'Could not install requirements for cluster install'\n    exit 1\nfi\n\n# go back and find VIP if subnet given (nmap required)\nif [[ -z \"$KAM_VIP\" ]] && [[ -n \"$IN_CIDR_FULL\" ]]; then\n    KAM_VIP=$(findAvailableIP \"$IN_CIDR_FULL\")\n    if [[ -z \"$KAM_VIP\" ]]; then\n        printerr \"Could not available IP to use as the VIP in subnet '$CIDR_NETMASK'\"\n        exit 1\n    fi\nfi\n\n# find the first private IP address (reverse order) on a physical interface\n# this is typically the secondary interface or secondary IP on the primary interface\n# if no RFC1918 address is found use the IP associated with the default route\n# NOTE: sourced here to allow easier declaration on remote node\nsource <(\n    cat <<EOF\n    getPacemakerInternalIP() {\n        $(declare -f getPhysicalIfaces)\n        $(declare -f ipv4TestRFC1918)\n        $(declare -f getInternalIP)\nEOF\n    cat <<'EOF'\n        for IP in $(\n            for IFACE in $(getPhysicalIfaces | sort -r); do\n                ip -4 -o addr show $IFACE | awk '{split($4,a,\"/\"); print a[1];}'\n            done\n        ); do\n            if ipv4TestRFC1918 \"$IP\"; then\n                echo -n \"$IP\"\n                return 0\n            fi\n        done\n        IP=\"$(getInternalIP)\"\n        [[ -n \"$IP\" ]] && {\n            echo -n \"$IP\"\n            return 0\n        }\n        return 1\n    }\nEOF\n)\n\n#======================================================================================\n# validate connections / prepare commands for main logic\n#======================================================================================\n\ni=0\nfor NODE in ${NODES[@]}; do\n    SSH_OPTS=( ${DEFAULT_SSH_OPTS[@]} )\n    RSYNC_OPTS=()\n\n    NODE_NAME=\"${CLUSTER_NAME}-node$((i+1))\"\n    NODE_NAMES+=( \"$NODE_NAME\" )\n\n    USER=$(printf '%s' \"$NODE\" | cut -s -d '@' -f -1 | cut -d ':' -f -1)\n    PASS=$(printf '%s' \"$NODE\" | cut -s -d '@' -f -1 | cut -s -d ':' -f 2-)\n    HOST=$(printf '%s' \"$NODE\" | cut -d '@' -f 2- | cut -d ':' -f -1)\n    PORT=$(printf '%s' \"$NODE\" | cut -d '@' -f 2- | cut -s -d ':' -f 2-)\n\n    HOST_LIST+=( \"$HOST\" )\n\n    # default user is root for ssh\n    USER=${USER:-root}\n    # default port is 22 for ssh\n    PORT=${PORT:-22}\n\n    # validate host connection\n    if ! checkConn ${HOST} ${PORT}; then\n        printerr \"Could not establish connection to host [${HOST}] on port [${PORT}]\"\n        exit 1\n    fi\n\n    if [[ -z \"$HOST\" ]]; then\n        printerr \"Node [${NODE}] does not contain a host\"\n        printUsage\n        exit 1\n    fi\n    USERHOST_LIST+=( \"${USER}@${HOST}\" )\n\n    if [[ -n \"$PASS\" ]]; then\n        export SSHPASS=\"${PASS}\"\n        SSH_CMD=\"sshpass -e ssh\"\n        RSYNC_CMD=\"sshpass -e rsync\"\n        SSH_OPTS+=(-o PreferredAuthentications=password)\n    else\n        SSH_CMD=\"ssh\"\n        RSYNC_CMD=\"rsync\"\n        if [[ -n \"$SSH_KEY_FILE\" ]]; then\n            SSH_OPTS+=(-o PreferredAuthentications=publickey -i $SSH_KEY_FILE)\n        else\n            SSH_OPTS+=(-o PreferredAuthentications=publickey)\n        fi\n    fi\n\n    RSYNC_OPTS+=(--port=${PORT} -z --exclude=\".*\")\n    SSH_OPTS+=(-p ${PORT})\n\n    printdbg 'validating unattended ssh connection'\n    if ! checkSsh ${SSH_CMD} ${SSH_OPTS[@]} ${USERHOST_LIST[$i]}; then\n        printerr \"Could not establish unattended ssh connection to [${USERHOST_LIST[$i]}] on port [${PORT}]\"\n        exit 1\n    fi\n    if ! checkSshRunas ${SSH_CMD} ${SSH_OPTS[@]} ${USERHOST_LIST[$i]}; then\n        printerr \"User [${USERHOST_LIST[$i]}] does not have sufficient privileges\"\n        printwarn 'Ensure the user can escalate to root via sudo or su'\n        exit 1\n    fi\n\n    # wrap up some args / options\n    SSH_CMD_LIST+=( \"${SSH_CMD} ${SSH_OPTS[*]}\" )\n    RSYNC_CMD_LIST+=( \"${RSYNC_CMD} ${RSYNC_OPTS[*]}\" )\n\n    # install requirements for the following commands (can not be copied as a script to remote node)\n    ${SSH_CMD_LIST[$i]} ${USERHOST_LIST[$i]} bash <<- EOSSH\n        if (( $DEBUG == 1 )); then\n            set -x\n        fi\n\n        # re-declare functions and vars we pass to remote server\n        # note that variables in function definitions (from calling environement)\n        # lose scope unless local to function, they must be passed to remote\n        ESC_SEQ=\"\\033[\"\n        ANSI_NONE=\"\\${ESC_SEQ}39;49;00m\" # Reset colors\n        ANSI_RED=\"\\${ESC_SEQ}1;31m\"\n        ANSI_GREEN=\"\\${ESC_SEQ}1;32m\"\n        $(typeset -f printdbg)\n        $(typeset -f printerr)\n        $(typeset -f cmdExists)\n        $(typeset -f runas)\n\n        printdbg 'installing requirements'\n        # awk is required for getInternalIP()\n        # curl is required for getCloudPlatform()\n        # rsync is required to for the main script\n        if cmdExists 'apt-get'; then\n            runas apt-get -o DPkg::Lock::Timeout=$PKG_MGR_TIMEOUT install -y gawk curl rsync\n        elif cmdExists 'dnf'; then\n            runas dnf install -y gawk curl rsync\n        elif cmdExists 'yum'; then\n            runas yum install -y gawk curl rsync\n        else\n            printerr \"OS on remote node [${HOST_LIST[$i]}] is currently not supported\"\n            exit 1\n        fi\n\n        if (( \\$? != 0 )); then\n            printerr \"Failed to install requirements on remote node ${HOST_LIST[$i]}\"\n            exit 1\n        fi\nEOSSH\n\n    # find the cloud platform the node is deployed on\n    CLOUD_PLATFORM=$(${SSH_CMD_LIST[$i]} -q ${USERHOST_LIST[$i]} \"$(typeset -f getCloudPlatform); getCloudPlatform;\")\n    [[ -n \"$CLOUD_PLATFORM\" ]] && CLOUD_DICT[$CLOUD_PLATFORM]=1\n\n    # warn the user if we don't have an integration setup for this provider yet\n    case \"${CLOUD_LIST[$i]}\" in\n        GCE|AZURE|VULTR|OCE)\n            printwarn 'support for virtual IP assignment on this cloud platform has not been tested'\n            printwarn 'attempting install anyways'\n            ;;\n    esac\n\n    # find the internal IP that the cluster will communicate over\n    # this secondary interface is where the node will attach the floating IP\n    CLUSTER_NODE_ADDRS+=( $(${SSH_CMD_LIST[$i]} -q ${USERHOST_LIST[$i]} \"$(typeset -f getPacemakerInternalIP); getPacemakerInternalIP;\") )\n\n    # find which resources we will be configuring\n    if (( $i == 0 )); then\n        case \"$CLOUD_PLATFORM\" in\n            DO|AWS)\n                CLUSTER_RESOURCES+=(cluster_vip)\n                ;;\n            *)\n                CLUSTER_RESOURCES+=(cluster_vip cluster_srcaddr)\n                ;;\n        esac\n\n        CLUSTER_RESOURCES+=($(${SSH_CMD_LIST[$i]} ${USERHOST_LIST[$i]} bash <<- EOSSH\n            if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\" ]]; then\n                echo 'rtpengine_service'\n            fi\n            if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]]; then\n                echo 'kamailio_service'\n            fi\n            if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]]; then\n                echo 'dsiprouter_service'\n            fi\nEOSSH\n        ))\n    fi\n\n    i=$((i+1))\ndone\n\n# make sure user does not try to install on 2 different cloud platforms\nif (( ${#CLOUD_DICT[@]} > 1 )); then\n    printerr 'nodes are deployed on different cloud platforms'\n    printerr 'installation on differing cloud platforms is not supported'\n    exit 1\nfi\n\n# make sure the credentials for the cloud provider was provided by the user\nif [[ \"$CLOUD_PLATFORM\" == \"DO\" ]] && [[ -z \"$DO_TOKEN\" ]]; then\n    printerr '--do-token is required when deploying on digital ocean'\n    exit 1\nelif [[ \"$CLOUD_PLATFORM\" == \"AWS\" ]] && [[ -z \"$AWS_ACCESS_KEY\" || -z \"$AWS_SECRET_TOKEN\" ]]; then\n    printerr '--aws-access-key and --aws-secret-key are required when deploying on amazon web services'\n    exit 1\nfi\n\n#======================================================================================\n# stage1: install requirements and start configuring individual nodes\n#======================================================================================\n\nprintdbg 'configuring servers for cluster deployment'\ni=0\nwhile (( $i < ${#NODES[@]} )); do\n    # copy over any files needed for the install\n    printdbg \"Copying configuration files to ${HOST_LIST[$i]}\"\n    if [[ -n \"$CLOUD_PLATFORM\" ]]; then\n        ${RSYNC_CMD_LIST[$i]} --rsh=\"ssh ${SSH_OPTS[*]} -o IPQoS=throughput\" -a ${PROJECT_ROOT}/HA/pacemaker/${CLOUD_PLATFORM}/ ${USERHOST_LIST[$i]}:/tmp/cloud/ 2>&1 &&\n        ${RSYNC_CMD_LIST[$i]} --rsh=\"ssh ${SSH_OPTS[*]} -o IPQoS=throughput\" -a ${PROJECT_ROOT}/HA/pacemaker/scripts/ ${USERHOST_LIST[$i]}:/tmp/scripts/ 2>&1\n        if (( $? != 0 )); then\n            printerr \"Copying configuration files to ${HOST_LIST[$i]} failed\"\n            exit 1\n        fi\n    fi\n    # the runtime environment we will use on the remote node needs cleaned and copied over\n    declare -p >\"$DIRTY_ENV_FILE\"\n    (\n        comm -3 <(sort \"$INITIAL_ENV_FILE\") <(sort \"$DIRTY_ENV_FILE\")\n        declare -f\n    ) >\"$CLEAN_ENV_FILE\"\n    ${RSYNC_CMD_LIST[$i]} --rsh=\"ssh ${SSH_OPTS[*]} -o IPQoS=throughput\" \"$CLEAN_ENV_FILE\" \"${USERHOST_LIST[$i]}:${CLEAN_ENV_FILE}\" 2>&1\n    if (( $? != 0 )); then\n        printerr \"Copying runtime environment file to ${HOST_LIST[$i]} failed\"\n        exit 1\n    fi\n\n    # run commands through ssh\n    ${SSH_CMD_LIST[$i]} ${USERHOST_LIST[$i]} \"bash /tmp/scripts/stage1.sh ${CLEAN_ENV_FILE}\" 2>&1\n\n    if (( $? != 0 )); then\n        printerr \"server configuration failed on ${HOST_LIST[$i]}\"\n        exit 1\n    fi\n\n    i=$((i+1))\ndone\n\n#======================================================================================\n# stage2: configure pacemaker settings on each node\n#======================================================================================\n\nprintdbg 'initializing pacemaker cluster'\ni=0\nwhile (( $i < ${#NODES[@]} )); do\n    # run commands through ssh\n    ${SSH_CMD_LIST[$i]} ${USERHOST_LIST[$i]} \"bash /tmp/scripts/stage2.sh ${CLEAN_ENV_FILE}\" 2>&1\n\n    if (( $? != 0 )); then\n        printerr \"initializing pacemaker cluster failed on ${HOST_LIST[$i]}\"\n        exit 1\n    fi\n\n    i=$((i+1))\ndone\n\n#======================================================================================\n# stage3: configure the pacemaker cluster and resources\n#======================================================================================\n\n# creates and configures the cluster\n# TODO: add support for updating virtual IP from various cloud providers\nprintdbg 'configuring pacemaker cluster'\ni=0\nwhile (( $i < ${#NODES[@]} )); do\n    # run commands through ssh\n    ${SSH_CMD_LIST[$i]} ${USERHOST_LIST[$i]} \"bash /tmp/scripts/stage3.sh ${CLEAN_ENV_FILE}\" 2>&1\n\n    if (( $? != 0 )); then\n        printerr \"pacemaker cluster configuration failed on ${HOST_LIST[$i]}\"\n        exit 1\n    fi\n\n    i=$((i+1))\ndone\n\nprintdbg 'Successfully configured pacemaker cluster'\nexit 0\n"
  },
  {
    "path": "HA/pacemaker/nodeutil.sh",
    "content": "#!/usr/bin/env bash\n#\n# Summary: Utility functions for managing cluster nodes\n# Summary: Program should be run locally on desired node\n# TODO:    panic() and kill() and startup() need ssh cmds w/ key setup\n#          to execute on remote node, only work locally now\n\nLOG_FILE=\"/var/log/nodeutil.log\"\n\n# defaults to current node\nNODE_NAME=$(sudo -u hacluster crm_node -n)\n\nstandby() {\n    sudo -u hacluster pcs cluster standby ${1:-$NODE_NAME}\n    log_status \"standby()\"\n}\n\nunstandby() {\n    sudo -u hacluster pcs cluster unstandby ${1:-$NODE_NAME}\n    log_status \"unstandby()\"\n}\n\nstop() {\n    sudo -u hacluster pcs cluster stop ${1:-$NODE_NAME}\n    log_status \"stop()\"\n}\n\nstart() {\n    sudo -u hacluster pcs cluster start ${1:-$NODE_NAME}\n    log_status \"start()\"\n}\n\nrestart() {\n    sudo -u hacluster pcs cluster stop ${1:-$NODE_NAME}\n    sleep 2\n    sudo -u hacluster pcs cluster start ${1:-$NODE_NAME}\n    log_status \"restart()\"\n}\n\nstartup() {\n    sudo systemctl start pacemaker\n    sudo systemctl start corosync\n    log_status \"startup()\"\n}\n\npanic() {\n    echo c | sudo tee /proc/sysrq-trigger\n    sudo systemctl stop pacemaker\n    sudo systemctl stop corosync\n    log_status \"panic()\"\n}\n\nkill() {\n    sudo pkill -9 pacemaker\n    sudo pkill -9 corosync\n    sudo shutdown -h now\n    log_status \"kill()\"\n}\n\nstart_cluster() {\n    sudo -u hacluster pcs cluster start --all\n    log_status \"start_cluster()\"\n}\n\nrestart_cluster() {\n    sudo -u hacluster pcs cluster stop --all\n    sleep 5\n    sudo -u hacluster pcs cluster start --all\n    sleep 2\n    log_status \"restart_cluster()\"\n}\n\nstop_cluster() {\n    sudo -u hacluster pcs cluster stop --all\n    log_status \"stop_cluster()\"\n}\n\n# $1 == resource name\n# returns: 0 == success, else == failure\nfind_resource() {\n    (\n        sudo -u hacluster pcs status resources | grep \"$1\" | awk '{print $NF}'\n        exit ${PIPESTATUS[0]}\n    ) 2>/dev/null\n    return $?\n}\n\n# $1 == name of command to log\nlog_status() {\n    printf '#===== log_status entry: %s =====#\\n' \"$(date)\" >> ${LOG_FILE}\n    printf 'current executing function: %s\\n' \"$1\" >> ${LOG_FILE}\n    {\n        sudo -u hacluster pcs status\n        printf '--------------------\\n'\n        sudo -u hacluster corosync-cfgtool -s\n    } 2>&1 >> ${LOG_FILE}\n    printf '#===== end of log entry =====#\\n\\n' >> ${LOG_FILE}\n}\n\nshow_status() {\n    sudo -u hacluster corosync-cfgtool -s\n    sudo -u hacluster pcs status --full\n}\n\n### arg parsing ###\ncmd=\"\"  # Default none\n\nHELP() {\n    echo \"Usage: \"\n    echo \"nodeutil -h                    Display this help message.\"\n    echo \"nodeutil -t|--target <target>  Specify a specific node as target.\"\n    echo \"nodeutil -c|--cmd <cmd>        Specify <cmd> from list of cmds to run.\"\n    echo \"Available <cmd> options:\"\n    echo \"standby, unstandby, stop, start, panic, start_cluster, restart_cluster, stop_cluster\"\n}\n\n#Check the number of arguments. If none are passed, print help and exit\noptions=$(getopt -o hc:t: -l cmd:,target: -n nodeutil -- \"$@\")\nif [ $? -ne 0 ]; then\n\tHELP\n\texit 1\nfi\n\n### parse options ###\neval set -- \"$options\"\nwhile true; do\n    OPT=\"$1\"\n    case \"$OPT\" in\n        -h)                 # Show help option menu\n            shift\n            HELP\n            exit 1\n            ;;\n        -t|--target)        # Grab the target node name\n            shift\n            NODE_NAME=\"$1\"\n            shift\n            ;;\n        -c|--cmd)           # Grab the cmd to execute\n            shift\n            cmd=\"$1\"\n            shift\n\n            case $cmd in\n                standby|unstandby|stop|start|restart|startup|panic|start_cluster|restart_cluster|stop_cluster|find_resource|show_status)\n                    $cmd \"$@\"\n                    ;;\n                *)\n                    HELP && exit 1\n                    ;;\n            esac\n            ;;\n        --)\n            shift\n            break\n            ;;\n\t  esac\ndone\n\nexit 0\n"
  },
  {
    "path": "HA/pacemaker/scripts/stage1.sh",
    "content": "#!/bin/bash\n\n# import runtime environment\nif ! [[ -f \"$1\" ]] || ! source \"$1\"; then\n   echo \"Could not import runtime environment\"\n   exit 1\nfi\n\nif (( ${DEBUG:-0} == 1 )); then\n    set -x\nfi\n\naddDefRoute() {\n    local IP=\"$1\"\n\n    local VIP_CIDR=\"${IP}/${CIDR_NETMASK:-32}\"\n    local ROUTE_INFO=$(ip route get 8.8.8.8 | head -1)\n    local DEF_IF=$(printf '%s' \"${ROUTE_INFO}\" | grep -oP 'dev \\K\\w+')\n    local VIP_ROUTE_INFO=$(printf '%s' \"${ROUTE_INFO}\" | sed -r \"s|8.8.8.8|0.0.0.0/1|; s|dev [\\w\\d]+|dev ${DEF_IF}|; s|src [\\w\\d]+|src ${IP}|\")\n\n    runas ip address add $VIP_CIDR dev $DEF_IF\n    runas ip route add $VIP_ROUTE_INFO\n}\n\nremoveDefRoute() {\n    local IP=\"$1\"\n\n    local ROUTE_INFO=$(ip route get 8.8.8.8 | head -1)\n    local DEF_IF=$(printf '%s' \"${ROUTE_INFO}\" | grep -oP 'dev \\K\\w+')\n\n    runas ip address del $VIP_CIDR dev $DEF_IF\n    runas ip route del 0.0.0.0/1\n}\n\nprintdbg 'installing requirements'\nif cmdExists 'apt-get'; then\n    runas apt-get -o DPkg::Lock::Timeout=$PKG_MGR_TIMEOUT install -y corosync pacemaker pcs firewalld jq perl dnsutils sed unzip\nelif cmdExists 'dnf'; then\n    runas dnf install -y corosync pacemaker pcs firewalld jq perl bind-utils sed unzip\nelif cmdExists 'yum'; then\n    runas yum install -y corosync pacemaker pcs firewalld jq perl bind-utils sed unzip\nelse\n    printerr \"OS on remote node [${HOST_LIST[$i]}] is currently not supported\"\n    exit 1\nfi\n\nif (( $? != 0 )); then\n    printerr \"Failed to install requirements on remote node ${HOST_LIST[$i]}\"\n    exit 1\nfi\n\nprintdbg 'configuring server for cluster deployment'\n\nprintdbg 'updating firewall rules'\nfor PORT in ${PACEMAKER_TCP_PORTS[@]}; do\n    runas firewall-cmd --zone=public --add-port=${PORT}/tcp --permanent\ndone\nfor PORT in ${PACEMAKER_UDP_PORTS[@]}; do\n    runas firewall-cmd --zone=public --add-port=${PORT}/udp --permanent\ndone\nrunas firewall-cmd --reload\n\nprintdbg 'setting up cluster password'\necho \"${CLUSTER_PASS}\" | runas passwd -q --stdin hacluster 2>/dev/null ||\n    echo \"hacluster:${CLUSTER_PASS}\" | runas chpasswd 2>/dev/null ||\n    { printerr \"could not change hacluster user password\"; exit 1; }\n\nprintdbg 'setting up cluster hostname resolution'\n\n# for each node remove the loopback hostname if present\n# this will cause issues when adding nodes to the cluster\n# ref: https://serverfault.com/questions/363095/why-does-my-hostname-appear-with-the-address-127-0-1-1-rather-than-127-0-0-1-in\ngrep -v -E '^127\\.0\\.1\\.1' /etc/hosts >/tmp/hosts &&\n    runas mv -f /tmp/hosts /etc/hosts\n\n# add section for the pacemaker hostnames\nif ! grep -q 'PACEMAKER_CONFIG_START' /etc/hosts 2>/dev/null; then\n    runas bash -c \"(\n        echo ''\n        echo '#####PACEMAKER_CONFIG_START'\n    )>>/etc/hosts\"\n    j=0\n    while (( $j < ${#CLUSTER_NODE_ADDRS[@]} )); do\n        runas bash -c \"echo '${CLUSTER_NODE_ADDRS[$j]} ${NODE_NAMES[$j]}' >>/etc/hosts\"\n        j=$((j+1))\n    done\n    runas bash -c \"echo '#####PACEMAKER_CONFIG_END' >>/etc/hosts\"\nelse\n    j=0; tmp='';\n    while (( $j < ${#CLUSTER_NODE_ADDRS[@]} )); do\n        tmp+=\"${CLUSTER_NODE_ADDRS[$j]} ${NODE_NAMES[$j]}\\n\"\n        j=$((j+1))\n    done\n    runas perl -0777 -i -pe \"s|(#+PACEMAKER_CONFIG_START).*?(#+PACEMAKER_CONFIG_END)|\\1\\n${tmp}\\2|gms\" /etc/hosts\nfi\n\nprintdbg 'configuring floating IP support on server'\n\n# enable binding to floating ip (on each node)\nrunas bash -c \"echo '1' >/proc/sys/net/ipv4/ip_nonlocal_bind\"\nrunas bash -c \"echo 'net.ipv4.ip_nonlocal_bind = 1' >/etc/sysctl.d/99-non-local-bind.conf\"\n\n# change kamcfg and rtpenginecfg to use floating ip (on each node)\nif [[ -e \"${DSIP_SYSTEM_CONFIG_DIR}\" ]]; then\n    printdbg 'updating dsiprouter services'\n\n    DSIP_INIT_PATH=$(systemctl show -P FragmentPath dsip-init)\n\n    if [[ -n \"$CLOUD_PLATFORM\" ]]; then\n        DSIP_VERSION=$(getConfigAttrib 'VERSION' ${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py)\n        DSIP_MAJ_VER=$(perl -pe 's%([0-9]+)\\..*%\\1%' <<<\"$DSIP_VERSION\")\n        DSIP_MIN_VER=$(perl -pe 's%[0-9]+\\.([0-9]).*%\\1%' <<<\"$DSIP_VERSION\")\n        DSIP_PATCH_VER=$(perl -pe 's%[0-9]+\\.[0-9]([0-9]).*%\\1%' <<<\"$DSIP_VERSION\")\n\n        # v0.72 and above have static networking supported\n        if (( $DSIP_MAJ_VER > 0 )) || (( $DSIP_MAJ_VER == 0 && $DSIP_MIN_VER >= 7 )); then\n            setConfigAttrib 'NETWORK_MODE' \"$STATIC_NETWORKING_MODE\" ${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py\n        else\n            removeExecStartCmd 'dsiprouter updatertpconfig' ${DSIP_INIT_PATH}\n            removeExecStartCmd 'dsiprouter updatekamconfig' ${DSIP_INIT_PATH}\n            removeExecStartCmd 'dsiprouter updatedsipconfig' ${DSIP_INIT_PATH}\n        fi\n\n        setConfigAttrib 'INTERNAL_IP_ADDR' '${CLUSTER_NODE_ADDRS[$i]}' ${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py\n        setConfigAttrib 'EXTERNAL_IP_ADDR' '$KAM_VIP' ${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py\n        NEW_EXT_FQDN=$(dig +short -x $KAM_VIP)\n        if [[ -n \"$NEW_EXT_FQDN\" ]]; then\n            setConfigAttrib 'EXTERNAL_FQDN' '$NEW_EXT_FQDN' ${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py\n        fi\n        setConfigAttrib 'UAC_REG_ADDR' '$KAM_VIP' ${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py\n\n        # update the settings in the various services\n        if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]]; then\n            runas dsiprouter updatedsipconfig\n        fi\n        if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]]; then\n            runas dsiprouter updatekamconfig\n        fi\n        if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\" ]]; then\n            runas dsiprouter updatertpconfig\n        fi\n    else\n        # manually add default route to vip before updating settings\n        addDefRoute \"${KAM_VIP}\"\n\n        # TODO: enable kamailio to listen to both ip's (maybe??)\n        if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]]; then\n            runas dsiprouter updatedsipconfig\n        fi\n        if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]]; then\n            runas dsiprouter updatekamconfig\n        fi\n        if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\" ]]; then\n            runas dsiprouter updatertpconfig\n        fi\n\n        removeDefRoute \"${KAM_VIP}\"\n    fi\n\n    # systemd services will be managed by corosync/pacemaker instead of dsip-init\n    if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]]; then\n        removeDependsOnService \"dsiprouter.service\" ${DSIP_INIT_PATH}\n        runas systemctl stop dsiprouter\n        runas systemctl disable dsiprouter\n    fi\n    if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]]; then\n        removeDependsOnService \"kamailio.service\" ${DSIP_INIT_PATH}\n        runas systemctl stop kamailio\n        runas systemctl disable kamailio\n    fi\n    if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\" ]]; then\n        removeDependsOnService \"rtpengine.service\" ${DSIP_INIT_PATH}\n        runas systemctl stop rtpengine\n        runas systemctl disable rtpengine\n    fi\n\n    addDependsOnService \"corosync.service\" ${DSIP_INIT_PATH}\n    addDependsOnService \"pacemaker.service\" ${DSIP_INIT_PATH}\nfi\n\nprintdbg 'configuring systemd services for pacemaker cluster'\nrunas systemctl enable pcsd\nrunas systemctl enable corosync\nrunas systemctl enable pacemaker\nrunas systemctl start pcsd\n\nprintdbg 'removing any previous corosync configurations'\nPCS_MAJMIN_VER=$(pcs --version | cut -d '.' -f -2 | tr -d '.')\n\nif (( $((10#$PCS_MAJMIN_VER)) >= 10 )); then\n    runas pcs host deauth 2>/dev/null\n    runas pcs cluster destroy --force 2>/dev/null\nelse\n    runas pcs pcsd clear-auth 2>/dev/null\n    runas pcs cluster destroy --force 2>/dev/null\nfi\n\nexit 0"
  },
  {
    "path": "HA/pacemaker/scripts/stage2.sh",
    "content": "#!/bin/bash\n\n# import runtime environment\nif ! [[ -f \"$1\" ]] || ! source \"$1\"; then\n   echo \"Could not import runtime environment\"\n   exit 1\nfi\n\nif (( ${DEBUG:-0} == 1 )); then\n    set -x\nfi\n\n# get the current region via the metadata api\nawsGetCurrentRegion() {\n    RET=$(curl -s -o /dev/null -w '%{http_code}' http://169.254.169.254/latest/meta-data/ami-id 2>/dev/null)\n    if (( $RET == 200 )); then\n        curl -s -f http://169.254.169.254/latest/meta-data/placement/region 2>/dev/null\n    elif (( $RET == 401 )); then\n        TOKEN=$(curl -s -X PUT --connect-timeout 2 -H 'X-aws-ec2-metadata-token-ttl-seconds: 60' http://169.254.169.254/latest/api/token 2>/dev/null)\n        curl -s -f --connect-timeout 2 -H \"X-aws-ec2-metadata-token: $TOKEN\" http://169.254.169.254/latest/meta-data/placement/region 2>/dev/null\n    else\n        return 1\n    fi\n    return 0\n}\n\nPCS_MAJMIN_VER=$(pcs --version | cut -d '.' -f -2 | tr -d '.')\n\nprintdbg 'authenticating hacluster user to pcsd'\nrunas -u hacluster pcs client local-auth -u hacluster -p ${CLUSTER_PASS}\n\nif (( $((10#$PCS_MAJMIN_VER)) >= 10 )); then\n    printdbg 'authenticating nodes to pcsd'\n    runas pcs host auth -u hacluster -p ${CLUSTER_PASS} ${NODE_NAMES[@]} || {\n        printerr \"Cluster auth failed\"\n        exit 1\n    }\n\n    if (( $i == ${#NODES[@]} - 1 )); then\n        printdbg 'creating the cluster'\n        runas pcs cluster setup --force --enable ${CLUSTER_NAME} ${NODE_NAMES[@]} ${CLUSTER_OPTIONS[@]} || {\n            printerr \"Cluster creation failed\"\n            exit 1\n        }\n    fi\nelse\n    printdbg 'authenticating nodes to pcsd'\n    runas pcs cluster auth --force -u hacluster -p ${CLUSTER_PASS} ${NODE_NAMES[@]} || {\n        printerr \"Cluster auth failed\"\n        exit 1\n    }\n\n    if (( $i == ${#NODES[@]} - 1 )); then\n        printdbg 'creating the cluster'\n        runas pcs cluster setup --force --enable --name ${CLUSTER_NAME} ${NODE_NAMES[@]} ${CLUSTER_OPTIONS[@]} || {\n            printerr \"Cluster creation failed\"\n            exit 1\n        }\n    fi\nfi\n\n# start cluster on the last node after all auth is completed\nif (( $i == ${#NODES[@]} - 1 )); then\n    j=0\n    while (( $j < $RETRY_CLUSTER_START )); do\n        runas pcs cluster start --all --request-timeout=15 --wait=15 &&\n            break\n        j=$((j+1))\n    done\n    # if we attempted all retries and finished the above loop we failed\n    if (( $j == $RETRY_CLUSTER_START )); then\n        printerr \"Starting cluster failed\"\n        exit 1\n    fi\nfi\n\n# setup any cloud provider specific configurations\ncase \"$CLOUD_PLATFORM\" in\n    DO)\n        runas cp -f /tmp/cloud/assign-ip /usr/local/bin/assign-ip\n        runas chmod +x /usr/local/bin/assign-ip\n        runas mkdir -p /usr/lib/ocf/resource.d/digitalocean\n        runas cp -f /tmp/cloud/ocf-floatip /usr/lib/ocf/resource.d/digitalocean/floatip\n        runas chmod +x /usr/lib/ocf/resource.d/digitalocean/floatip\n        ;;\n    AWS)\n        if ! cmdExists 'aws'; then\n            cd /tmp &&\n            curl 'https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip' -o awscli.zip &&\n            unzip -qo awscli.zip &&\n            rm -f awscli.zip &&\n            runas ./aws/install -b /usr/bin\n        fi\n\n        AWS_REGION=$(awsGetCurrentRegion) || {\n            printerr \"Could not determine current AWS region\"\n            exit 1\n        }\n        runas aws configure set aws_access_key_id $AWS_ACCESS_KEY\n        runas aws configure set aws_secret_access_key $AWS_SECRET_TOKEN\n        runas aws configure set region $AWS_REGION\n\n        runas mkdir -p /usr/lib/ocf/resource.d/aws\n        runas cp -f /tmp/cloud/ocf-floatip /usr/lib/ocf/resource.d/aws/floatip\n        runas chmod +x /usr/lib/ocf/resource.d/aws/floatip\n        ;;\nesac\n\nexit 0"
  },
  {
    "path": "HA/pacemaker/scripts/stage3.sh",
    "content": "#!/bin/bash\n\n# import runtime environment\nif ! [[ -f \"$1\" ]] || ! source \"$1\"; then\n   echo \"Could not import runtime environment\"\n   exit 1\nfi\n\nif (( ${DEBUG:-0} == 1 )); then\n    set -x\nfi\n\n# $1 == resource name\n# returns: 0 == success, else == failure\n# notes: prints node name where resource is found\nfindResource() {\n    local RESOURCE_FIND_TIMEOUT=5\n    local NODE\n\n    timeout \"$RESOURCE_FIND_TIMEOUT\" bash <<EOF 2>/dev/null\nwhile true; do\n    NODE=\\$(\n        runas -u hacluster pcs status resources |\n        awk '\\$2==\"'$1'\" && \\$4==\"Started\" {print \\$5}'\n    )\n    if [[ -n \"\\$NODE\" ]]; then\n        echo \"\\$NODE\"\n        break\n    fi\n    sleep 1\ndone\nEOF\n    return $?\n}\n\n# output: prints numbers of pacemaker resources that are down\ngetNumResourcesDown() {\n    runas -u hacluster pcs status resources |\n    grep -v -F 'Resource Group' |\n    awk '$4!=\"Started\" {print $2}' |\n    wc -l\n}\n\n# $1 == timeout\n# returns: 0 == resources up within timeout, else == resources not up within timeout\n# notes: block while waiting for resources to come online until timeout\nwaitResources() {\n    timeout \"$1\" bash <<'EOF' 2>/dev/null\nRESOURCES_DOWN=$(getNumResourcesDown)\nwhile (( $RESOURCES_DOWN > 0 )); do\n    sleep 1\n    RESOURCES_DOWN=$(getNumResourcesDown)\ndone\nEOF\n    return $?\n}\n\n# notes: prints out detailed info about cluster\nshowClusterStatus() {\n    runas -u hacluster corosync-cfgtool -s\n    runas -u hacluster pcs status --full\n}\n\n## Pacemaker / Corosync cluster\nif (( $i == 0 )); then\n    printdbg 'configuring pacemaker cluster'\n\n    # disabling stonith/quorum for now because it has caused issues in the past (on first node)\n    runas pcs property set stonith-enabled=false\n    runas pcs property set no-quorum-policy=ignore\n\n    printdbg 'Setting up the virtual ip address resource'\n    # create resource for services and virtual ip and default route (on first node)\n    case \"$CLOUD_PLATFORM\" in\n        DO)\n            runas pcs resource create cluster_vip ocf:digitalocean:floatip \\\n                do_token=${DO_TOKEN} floating_ip=${KAM_VIP} \\\n                op monitor interval=15s timeout=15s \\\n                op start interval=0 timeout=30s \\\n                meta resource-stickiness=100 \\\n                --group vip_group\n            ;;\n        AWS)\n            runas pcs resource create cluster_vip ocf:aws:floatip \\\n                elastic_ip=${KAM_VIP} \\\n                op monitor interval=15s timeout=15s \\\n                op start interval=0 timeout=30s \\\n                meta resource-stickiness=100 \\\n                --group vip_group\n            ;;\n        *)\n            runas pcs resource create cluster_vip ocf:heartbeat:IPaddr2 \\\n                ip=${KAM_VIP} cidr_netmask=${CIDR_NETMASK} \\\n                op monitor interval=15s timeout=15s \\\n                op start interval=0 timeout=30s \\\n                meta resource-stickiness=100 \\\n                --group vip_group\n            runas pcs resource create cluster_srcaddr ocf:heartbeat:IPsrcaddr \\\n                ipaddress=${KAM_VIP} cidr_netmask=${CIDR_NETMASK} \\\n                op monitor interval=15s timeout=15s \\\n                op start interval=0 timeout=30s \\\n                meta resource-stickiness=100 \\\n                --group vip_group\n            ;;\n    esac\n\n    printdbg 'Setting up resources for dsiprouter services'\n    if grep -q 'rtpengine_service' 2>/dev/null <<<\"${CLUSTER_RESOURCES[@]}\"; then\n        runas pcs resource create rtpengine_service systemd:rtpengine \\\n            op monitor interval=30s timeout=15s \\\n            op start interval=0 timeout=30s on-fail=restart \\\n            op stop interval=0 timeout=30s \\\n            meta resource-stickiness=100 \\\n            --group dsip_group\n    fi\n    if grep -q 'kamailio_service' 2>/dev/null <<<\"${CLUSTER_RESOURCES[@]}\"; then\n        runas pcs resource create kamailio_service systemd:kamailio \\\n            op monitor interval=30s timeout=15s \\\n            op start interval=0 timeout=30s on-fail=restart \\\n            op stop interval=0 timeout=30s \\\n            meta resource-stickiness=100 \\\n            --group dsip_group\n    fi\n    if grep -q 'dsiprouter_service' 2>/dev/null <<<\"${CLUSTER_RESOURCES[@]}\"; then\n        runas pcs resource create dsiprouter_service systemd:dsiprouter \\\n            op monitor interval=60s timeout=15s \\\n            op start interval=0 timeout=30s on-fail=restart \\\n            op stop interval=0 timeout=30s \\\n            meta resource-stickiness=100 \\\n            --group dsip_group\n    fi\n\n    printdbg 'colocating resources on the same node'\n    runas pcs constraint colocation set vip_group dsip_group \\\n        sequential=true \\\n        setoptions score=INFINITY\n    runas pcs constraint order set vip_group dsip_group \\\n        action=start sequential=true require-all=true \\\n        setoptions symmetrical=false kind=Mandatory\nfi\n\nif (( $i == ${#NODES[@]} - 1 )); then\n    printdbg 'testing cluster'\n\n    PCS_MAJMIN_VER=$(pcs --version | cut -d '.' -f -2 | tr -d '.')\n\n    RESOURCES=(${CLUSTER_RESOURCES[@]})\n    CURRENT_RESOURCE_LOCATIONS=()\n    PREVIOUS_RESOURCE_LOCATIONS=()\n\n    # wait on resources to come online\n    waitResources ${RESOURCE_STARTUP_TIMEOUT} || {\n        printerr \"Cluster resources failed to start within ${RESOURCE_STARTUP_TIMEOUT} seconds\"\n        exit 1\n    }\n\n    # grab operation data for tests\n    # TODO: error checking for resources not yet started\n    for RESOURCE in ${RESOURCES[@]}; do\n        PREVIOUS_RESOURCE_LOCATIONS+=( $(findResource ${RESOURCE}) )\n    done\n\n    printdbg \"current resource locations: ${PREVIOUS_RESOURCE_LOCATIONS[@]}\"\n    printdbg \"setting ${PREVIOUS_RESOURCE_LOCATIONS[0]} to standby\"\n\n    if (( $((10#$PCS_MAJMIN_VER)) >= 10 )); then\n        runas -u hacluster pcs node standby ${PREVIOUS_RESOURCE_LOCATIONS[0]}\n    else\n        runas -u hacluster pcs cluster standby ${PREVIOUS_RESOURCE_LOCATIONS[0]}\n    fi\n\n    # wait for transfer to finish (to another node)\n    waitResources ${RESOURCE_STARTUP_TIMEOUT} || {\n        printerr \"Cluster resources failed to start within ${RESOURCE_STARTUP_TIMEOUT} seconds\"\n        exit 1\n    }\n\n    for RESOURCE in ${RESOURCES[@]}; do\n        CURRENT_RESOURCE_LOCATIONS+=( $(findResource ${RESOURCE}) )\n    done\n\n    printdbg \"current resource locations: ${CURRENT_RESOURCE_LOCATIONS[@]}\"\n    printdbg \"resetting ${PREVIOUS_RESOURCE_LOCATIONS[0]}\"\n\n    if (( $(pcs --version | cut -d '.' -f 2- | tr -d '.') >= 100 )); then\n        runas -u hacluster pcs node unstandby ${PREVIOUS_RESOURCE_LOCATIONS[0]}\n    else\n        runas -u hacluster pcs cluster unstandby ${PREVIOUS_RESOURCE_LOCATIONS[0]}\n    fi\n\n    # wait for transfer to finish (to another node, could be original)\n    waitResources ${RESOURCE_STARTUP_TIMEOUT} || {\n        printerr \"Cluster resources failed to start within ${RESOURCE_STARTUP_TIMEOUT} seconds\"\n        exit 1\n    }\n\n    # run tests to make sure operations worked\n    i=0\n    while (( $i < ${#RESOURCES[@]} )); do\n        if [[ ${PREVIOUS_RESOURCE_LOCATIONS[$i]} != ${PREVIOUS_RESOURCE_LOCATIONS[$((i+1))]:-${PREVIOUS_RESOURCE_LOCATIONS[0]}} ]]; then\n            printerr \"Cluster resource colocation tests failed (before migration)\"\n            exit 1\n        fi\n\n        if [[ ${CURRENT_RESOURCE_LOCATIONS[$i]} != ${CURRENT_RESOURCE_LOCATIONS[$((i+1))]:-${CURRENT_RESOURCE_LOCATIONS[0]}} ]]; then\n            printerr \"Cluster resource colocation tests failed (after migration)\"\n            exit 1\n        fi\n\n        if [[ ${PREVIOUS_RESOURCE_LOCATIONS[$i]} == ${CURRENT_RESOURCE_LOCATIONS[$i]} ]]; then\n            printerr \"Cluster resource ${RESOURCE} failover tests failed\"\n            exit 1\n        fi\n\n        i=$((i+1))\n    done\n\n    printdbg 'Any non-critical resource errors are shown below:'\n    runas -u hacluster pcs resource failcount show\n    printdbg 'Clearing any non-critical resource errors'\n    runas -u hacluster -n pcs resource cleanup\n\n    # show status to user\n    printdbg 'cluster info:'\n    showClusterStatus\nfi\n\nexit 0"
  },
  {
    "path": "HA/shared_lib.sh",
    "content": "#!/usr/bin/env bash\n#\n# NOTES:\n# contains utility functions and shared variables\n# should be sourced by an external script\n#\n# TODO: section library scripts into more manageable files (grouping related funcs)\n# we should also put them in a central location such as: <project dir>/bashlibs\n#\n\n#############\n# Constants #\n#############\n\n# Ansi Colors\nESC_SEQ=\"\\033[\"\nANSI_NONE=\"${ESC_SEQ}39;49;00m\" # Reset colors\nANSI_RED=\"${ESC_SEQ}1;31m\"\nANSI_GREEN=\"${ESC_SEQ}1;32m\"\nANSI_YELLOW=\"${ESC_SEQ}1;33m\"\nANSI_CYAN=\"${ESC_SEQ}1;36m\"\n\n# Shared Script Defaults\nDEFAULT_SSH_OPTS=(-o StrictHostKeyChecking=no -o CheckHostIp=no -o UserKnownHostsFile=/dev/null -o ServerAliveInterval=5 -o ServerAliveCountMax=2 -x -t)\n\n##############################################\n# Printing functions and String Manipulation #\n##############################################\n\nprinterr() {\n    printf \"%b%s%b\\n\" \"${ANSI_RED}\" \"$*\" \"${ANSI_NONE}\"\n}\n\nprintwarn() {\n    printf \"%b%s%b\\n\" \"${ANSI_YELLOW}\" \"$*\" \"${ANSI_NONE}\"\n}\n\nprintdbg() {\n    printf \"%b%s%b\\n\" \"${ANSI_GREEN}\" \"$*\" \"${ANSI_NONE}\"\n}\n\npprint() {\n    printf \"%b%s%b\\n\" \"${ANSI_CYAN}\" \"$*\" \"${ANSI_NONE}\"\n}\n\n######################################\n# Traceback / Debug helper functions #\n######################################\n\nbacktrace() {\n    local DEPTN=${#FUNCNAME[@]}\n\n    for ((i=1; i < ${DEPTN}; i++)); do\n        local FUNC=\"${FUNCNAME[$i]}\"\n        local LINE=\"${BASH_LINENO[$((i-1))]}\"\n        local SRC=\"${BASH_SOURCE[$((i-1))]}\"\n        printf '%*s' $i '' # indent\n        printerr \"[ERROR]: ${FUNC}(), ${SRC}, line: ${LINE}\"\n    done\n}\n\nsetErrorTracing() {\n    set -o errtrace\n    trap 'backtrace' ERR\n}\n\n# $1 == command to test\n# returns: 0 == true, 1 == false\ncmdExists() {\n    if command -v \"$1\" > /dev/null 2>&1; then\n        return 0\n    else\n        return 1\n    fi\n}\n\n# $1 == directory to check for in PATH\n# returns: 0 == found, 1 == not found\npathCheck() {\n    case \":${PATH-}:\" in\n        *:\"$1\":*)\n            return 0\n            ;;\n        *)\n            return 1\n            ;;\n    esac\n}\n\n# $1 == delimeter to join args with\n# $* == strings to join\n# usage: STR=$(join ',' ${ARR[@]})\njoin() {\n    local IFS=\"$1\"; shift; echo \"$*\"\n}\n\n# $1 == delimeter to join args with\n# $@ == string(s) to split\n# usage: ARR=( $(split ',' '1,2,3,4') )\nsplit() {\n    local IFS=\"$1\"; shift; read -r -a ARR <<< \"$@\"; echo \"${ARR[@]}\"; unset ARR;\n}\n\n# returns: 0 == root user, else not root user\nisRoot() {\n    return $(id -u 2>/dev/null)\n}\n\n# sets DISTRO and DISTRO_VER variables in current shell\nsetOSInfo() {\n    DISTRO=$(cat /etc/os-release 2>/dev/null | grep '^ID=' | cut -d '=' -f 2 | cut -d '\"' -f 2)\n\n    if [[ \"$DISTRO\" == \"linuxmint\" ]]; then\n        export DISTRO=\"linuxmint\"\n        export DISTRO_VER=$(grep -w \"VERSION_ID\" /etc/os-release | cut -d '\"' -f 2)\n    elif [[ \"$DISTRO\" == \"ubuntu\" ]]; then\n        export DISTRO=\"ubuntu\"\n        export DISTRO_VER=$(grep -w \"VERSION_ID\" /etc/os-release | cut -d '\"' -f 2)\n    elif [[ \"$DISTRO\" == \"debian\" ]]; then\n        export DISTRO=\"debian\"\n        export DISTRO_VER=$(grep -w \"VERSION_ID\" /etc/os-release | cut -d '\"' -f 2)\n    elif [[ \"$DISTRO\" == \"amzn\" ]]; then\n        export DISTRO=\"amazon\"\n        export DISTRO_VER=$(grep -w \"VERSION_ID\" /etc/os-release | cut -d '\"' -f 2)\n    elif [[ \"$DISTRO\" == \"centos\" ]]; then\n        export DISTRO=\"centos\"\n        export DISTRO_VER=$(grep -w \"VERSION_ID\" /etc/os-release | cut -d '\"' -f 2)\n    elif [[ -f /etc/redhat-release ]] && [[ \"$(cat /etc/redhat-release | awk '{ print tolower($1) }')\" == \"red\" ]]; then\n        export DISTRO=\"redhat\"\n        export DISTRO_VER=$(cat /etc/redhat-release | sed 's/.* \\(7\\).[0-9] .*/\\1/')\n    else\n        export DISTRO=\"\"\n        export DISTRO_VER=\"\"\n    fi\n}\n\n# usage:    runas [options] <command>\n# options:  -u <run user>\n# notes:    default runas user is root\n# returns:  255 == failed before running command, else == exit code of command\nrunas() {\n    local RUN_USER=\"root\"\n    if [[ \"$1\" == \"-u\" ]]; then\n        shift\n        RUN_USER=\"$1\"\n        shift\n    fi\n\n    if [[ $(id -u \"$RUN_USER\" 2>/dev/null) == $(id -u \"$RUN_USER\" 2>/dev/null) ]]; then\n        \"$@\"\n    elif command -v 'sudo' &>/dev/null; then\n        sudo -u \"$RUN_USER\" \"$@\"\n    elif command -v 'su' &>/dev/null; then\n        su \"$RUN_USER\" -s /bin/bash -c \"$*\"\n    else\n        echo \"$0: could not run command as user '$RUN_USER'\"\n        return 255\n    fi\n}\n\n# $1 == ip or hostname\n# $2 == port\n# returns: 0 == connection good, 1 == connection bad\n# note: timeout is set to 3 sec\ncheckConn() {\n    if cmdExists 'nc'; then\n        nc -w 3 \"$1\" \"$2\" < /dev/null 2>&1 > /dev/null; return $?\n    else\n        timeout 3 bash -c \"< /dev/tcp/$1/$2\" 2> /dev/null; return $?\n    fi\n}\n\n# $@ == ssh command to test\n# returns: 0 == ssh connected, 1 == ssh could not connect\ncheckSsh() {\n    local SSH_CMD=\"$@ -o ConnectTimeout=5 -q 'exit 0'\"\n    bash -c \"${SSH_CMD}\" 2>&1 > /dev/null; return $?\n}\n\n# $@ == ssh command to test\n# returns: 0 == user can escalate privileges, 1 == user could not escalate privileges\ncheckSshRunas() {\n    local SSH_CMD=\"$@ -o ConnectTimeout=5 -q '$(typeset -f runas); runas echo -n'\"\n    bash -c \"trap 'exit 1' SIGINT; ${SSH_CMD}\"; return $?\n}\n\n# Notes: prints generated password\ncreatePass() {\n    tr -dc 'a-zA-Z0-9' </dev/urandom | dd if=/dev/stdin of=/dev/stdout bs=1 count=32 2>/dev/null\n}\n\n# usage: getPkgVer [--opt] <arg>\n# opt == what version(s) to show (default --installed)\n#       -i|--installed:    get installed package version\n#       -l|--latest:       get latest version available\n#       -a|--all:          get all available versions\n# arg == the package to search for in repos\n# returns: 0 == success, else == failure\n# note: does not handle same versions from different repos\n# bug: if search is on a glob expr xargs will error out silently\ngetPkgVer() {\n    local OPT=\"\"\n    local ARG=\"\"\n    local KEY=\"\"\n\n    while (( $# > 0 )); do\n        OPT=\"$1\"\n        case $OPT in\n            -i|--installed)\n                KEY=\"installed\"\n                shift\n                ;;\n            -l|--latest)\n                KEY=\"latest\"\n                shift\n                ;;\n            -a|--all)\n                KEY=\"all\"\n                shift\n                ;;\n            *)  # only one arg is valid\n                ARG=\"$OPT\"\n                shift\n                ;;\n        esac\n    done\n\n    case ${KEY:-installed} in\n            installed)\n                ( apt-cache policy \"$ARG\" \\\n                    | grep -oP 'Installed:\\h*([0-9]+:)?\\K([0-9]+\\.)([0-9\\.]+)' \\\n                    | sed 's/\\./%/; s/\\.//g; s/%/\\./';\n                    exit ${PIPESTATUS[0]}; ) 2>/dev/null && return $? ||\n                ( yum --cacheonly list installed \"$ARG\" 2>/dev/null \\\n                    | xargs -d '\\r\\n' -n 3 \\\n                    | grep -oP '\\h*([0-9]+:)?\\K([0-9]+\\.)([0-9\\.]+)' \\\n                    | sed 's/\\./%/; s/\\.//g; s/%/\\./';\n                    exit ${PIPESTATUS[0]}; ) 2>/dev/null && return $? ||\n                    return 1\n                ;;\n            latest)\n                ( apt-cache policy \"$ARG\" \\\n                    | grep -oP 'Candidate:\\h*([0-9]+:)?\\K([0-9]+\\.)([0-9\\.]+)' \\\n                    | sed 's/\\./%/; s/\\.//g; s/%/\\./';\n                    exit ${PIPESTATUS[0]}; ) 2>/dev/null ||\n                ( yum --cacheonly --showduplicates list available \"$ARG\" 2>/dev/null \\\n                    | xargs -d '\\r\\n' -n 3 \\\n                    | grep -oP '\\h*([0-9]+:)?\\K([0-9]+\\.)([0-9\\.]+)' \\\n                    | sed 's/\\./%/; s/\\.//g; s/%/\\./' \\\n                    | head -1;\n                    exit ${PIPESTATUS[0]}; ) 2>/dev/null && return $? ||\n                    return 1\n                ;;\n            all)\n                ( apt-cache policy \"$ARG\" \\\n                    | grep -oP '((?<![a-zA-Z:])[\\*\\h])+([0-9]+:)?\\K([0-9]+\\.)([0-9\\.]+)' \\\n                    | sed 's/\\./%/; s/\\.//g; s/%/\\./' \\\n                    | uniq;\n                    exit ${PIPESTATUS[0]}; ) 2>/dev/null ||\n                ( yum --cacheonly --showduplicates list \"$ARG\" 2>/dev/null \\\n                    | xargs -d '\\r\\n' -n 3 \\\n                    | grep -oP '\\h*([0-9]+:)?\\K([0-9]+\\.)([0-9\\.]+)' \\\n                    | sed 's/\\./%/; s/\\.//g; s/%/\\./' \\\n                    | uniq;\n                    exit ${PIPESTATUS[0]}; ) 2>/dev/null && return $? ||\n                    return 1\n                ;;\n    esac\n}\n\n# usage: dumpMysqlDatabases [options] [ <[user1[:pass]@]host1[:port]> <[user1[:pass]@]host2[:port]> ... ]\n# options:  -a|--all\n#           -f|--full\n#           -m|--merge\n#           -g|--grants\n# notes: redirect output sql as needed (in shell)\ndumpMysqlDatabases() {\n    local KEY=\"full\" #default\n    local OPT NODE USER PASS HOST PORT NON_SYSTEM_DB\n    local IDX=0 IDX_MAX=0 IDX_LAST=0\n    local USERS=() PASSES=() HOSTS=() PORTS=()\n\n\n    while (( $# > 0 )); do\n        OPT=\"$1\"\n        case $OPT in\n            -a|--all)\n                KEY=\"all\"\n                shift\n                ;;\n            -f|--full)\n                KEY=\"full\"\n                shift\n                ;;\n            -m|--merge)\n                KEY=\"merge\"\n                shift\n                ;;\n            -g|--grants)\n                KEY=\"grants\"\n                shift\n                ;;\n            *)\n                NODE=\"$1\"\n                USER=$(printf '%s' \"$NODE\" | cut -s -d '@' -f -1 | cut -d ':' -f -1)\n                PASS=$(printf '%s' \"$NODE\" | cut -s -d '@' -f -1 | cut -s -d ':' -f 2-)\n                HOST=$(printf '%s' \"$NODE\" | cut -d '@' -f 2- | cut -d ':' -f -1)\n                PORT=$(printf '%s' \"$NODE\" | cut -d '@' -f 2- | cut -s -d ':' -f 2-)\n                USERS+=(\"$USER\")\n                PASSES+=(\"$PASS\")\n                HOSTS+=(\"$HOST\")\n                PORTS+=(\"$PORT\")\n                IDX_MAX=$(( IDX_MAX + 1 ))\n                shift\n                ;;\n        esac\n    done\n\n    IDX_LAST=$(( IDX_MAX - 1 ))\n\n    # key is not handled\n    case \"$KEY\" in\n        all|full|merge|grants) ;;\n        *) return 1;;\n    esac\n\n    # for all databases\n    if [[ \"$KEY\" == \"all\" ]] || [[ \"$KEY\" == \"full\" ]]; then\n        IDX=0\n        while (( $IDX < $IDX_MAX )); do\n            mysqldump --single-transaction --opt --events --routines --triggers --all-databases --add-drop-database --flush-privileges --hex-blob \\\n                --user=\"${USERS[$IDX]}\" --password=\"${PASSES[$IDX]}\" --port=\"${PORTS[$IDX]}\" --host=\"${HOSTS[$IDX]}\" 2>/dev/null \\\n                | sed -r -e 's|DEFINER=[`\"'\"'\"'][a-zA-Z0-9_%]*[`\"'\"'\"']@[`\"'\"'\"'][a-zA-Z0-9_%]*[`\"'\"'\"']||g'\n            IDX=$(( IDX + 1 ))\n        done\n    fi\n    # for merging non system databases\n    if [[ \"$KEY\" == \"merge\" ]]; then\n        # TODO: handle nodes other than 1st have other non-system DBs\n        NON_SYSTEM_DB=$(mysql -sN --user=\"${USERS[0]}\" --password=\"${PASSES[0]}\" --port=\"${PORTS[0]}\" --host=\"${HOSTS[0]}\" \\\n            -e \"SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('mysql','information_schema','performance_schema')\" 2>/dev/null)\n\n        while (( $IDX < $IDX_MAX )); do\n            if (( $IDX == 0 )); then\n                # recreate DB schema without triggers\n                mysqldump --single-transaction --skip-opt --quick --skip-triggers --routines --create-options --disable-keys --set-charset --add-drop-database --no-data --skip-comments \\\n                     --user=\"${USERS[$IDX]}\" --password=\"${PASSES[$IDX]}\" --port=\"${PORTS[$IDX]}\" --host=\"${HOSTS[$IDX]}\" --databases ${NON_SYSTEM_DB} 2>/dev/null \\\n                     | sed -r -e 's|DEFINER=[`\"'\"'\"'][a-zA-Z0-9_%]*[`\"'\"'\"']@[`\"'\"'\"'][a-zA-Z0-9_%]*[`\"'\"'\"']||g'\n            fi\n\n            # fill data from each node\n            mysqldump --single-transaction --skip-opt --skip-triggers --no-create-db --no-create-info --insert-ignore --hex-blob --skip-comments \\\n                --user=\"${USERS[$IDX]}\" --password=\"${PASSES[$IDX]}\" --port=\"${PORTS[$IDX]}\" --host=\"${HOSTS[$IDX]}\" --databases ${NON_SYSTEM_DB} 2>/dev/null\n\n            if (( $IDX == $IDX_LAST )); then\n                # recreate DB triggers now that data is processed\n                mysqldump --single-transaction --skip-opt --quick --triggers --no-create-db --no-create-info --no-data --skip-comments \\\n                     --user=\"${USERS[$IDX]}\" --password=\"${PASSES[$IDX]}\" --port=\"${PORTS[$IDX]}\" --host=\"${HOSTS[$IDX]}\" --databases ${NON_SYSTEM_DB} 2>/dev/null \\\n                     | sed -r -e 's|DEFINER=[`\"'\"'\"'][a-zA-Z0-9_%]*[`\"'\"'\"']@[`\"'\"'\"'][a-zA-Z0-9_%]*[`\"'\"'\"']||g'\n            fi\n\n            IDX=$(( IDX + 1 ))\n        done\n    fi\n    # for copying privileges\n    if [[ \"$KEY\" == \"all\" ]] || [[ \"$KEY\" == \"grants\" ]]; then\n        (\n            IDX=0\n            while (( $IDX < $IDX_MAX )); do\n                mysql -sN -A --user=\"${USERS[$IDX]}\" --password=\"${PASSES[$IDX]}\" --port=\"${PORTS[$IDX]}\" --host=\"${HOSTS[$IDX]}\" \\\n                    -e \"SELECT DISTINCT CONCAT('SHOW GRANTS FOR ''',user,'''@''',host,''';') FROM mysql.user WHERE user<>''\" 2>/dev/null \\\n                    | mysql -sN -A --user=\"${USERS[$IDX]}\" --password=\"${PASSES[$IDX]}\" --port=\"${PORTS[$IDX]}\" --host=\"${HOSTS[$IDX]}\" 2>/dev/null \\\n                    | sed 's/$/;/g' \\\n                    | awk '!x[$0]++'\n            IDX=$(( IDX + 1 ))\n            done\n        ) | sort -u\n        echo 'FLUSH PRIVILEGES;'\n    fi\n\n    return 0\n}\n\ndetectServiceMan() {\n    INIT_PROC=$(runas readlink -f $(runas readlink -f /proc/1/exe))\n\n    case \"$INIT_PROC\" in\n        *systemd)\n            SERVICE_MANAGER=\"systemd\"\n            ;;\n        *upstart)\n            SERVICE_MANAGER=\"upstart\"\n            ;;\n        *runit-init)\n            SERVICE_MANAGER=\"runit\"\n            ;;\n        *openrc-init)\n            SERVICE_MANAGER=\"openrc\"\n            ;;\n        /sbin/init)\n            INIT_PROC_INFO=$(/sbin/init --version 2>/dev/null | head -1)\n            case \"$INIT_PROC_INFO\" in\n                *systemd*)\n                    SERVICE_MANAGER=\"systemd\"\n                    ;;\n                *upstart*)\n                    SERVICE_MANAGER=\"upstart\"\n                    ;;\n                *runit-init*)\n                    SERVICE_MANAGER=\"runit\"\n                    ;;\n                *openrc-init*)\n                    SERVICE_MANAGER=\"openrc\"\n                    ;;\n            esac\n            ;;\n        *)\n            SERVICE_MANAGER=\"sysv\"\n            ;;\n    esac\n\n    export SERVICE_MANAGER\n}\n\n# $1 == ip to test\n# returns: 0 == success, 1 == failure\n# notes: regex credit to <https://helloacm.com>\nipv4Test() {\n    local IP=\"$1\"\n\n    if [[ $IP =~ ^([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$ ]]; then\n        return 0\n    fi\n    return 1\n}\n\n# $1 == ip to test\n# returns: 0 == success, 1 == failure\n# notes: regex credit to <https://helloacm.com>\nipv6Test() {\n    local IP=\"$1\"\n\n    if [[ $IP =~ ^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$ ]]; then\n        return 0\n    fi\n    return 1\n}\n\n# $1 == ip to test\n# returns: 0 == success, 1 == failure\nipv4TestRFC1918() {\n    local IP=\"$1\"\n    if [[ $IP =~ ^(10\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|172\\.(1[6-9]|2[0-9]|3[01])\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|192\\.168\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5]))$ ]]; then\n        return 0\n    fi\n    return 1\n}\n\n# notes: prints external ip, or empty string if not available\n# notes: below we have measurements for average time of each service\n#        over 10 non-cached requests, in seconds, round trip\n#\n# |          External Service         | Mean RTT | IP Protocol |\n# |:---------------------------------:|:--------:|:-----------:|\n# | https://icanhazip.com             | 0.38080  | IPV4        |\n# | https://ipecho.net/plain          | 0.39810  | IPV4        |\n# | https://myexternalip.com/raw      | 0.51850  | IPV4        |\n# | https://api.ipify.org             | 0.64860  | IPV4        |\n# | https://bot.whatismyipaddress.com | 0.69640  | IPV4        |\n# | https://icanhazip.com             | 0.40190  | IPV6        |\n# | https://bot.whatismyipaddress.com | 0.72490  | IPV6        |\n# | https://ifconfig.co               | 0.80290  | IPV6        |\n# | https://ident.me                  | 0.97620  | IPV6        |\n# | https://api6.ipify.org            | 1.08510  | IPV6        |\n#\ngetExternalIP() {\n    local IPV6_ENABLED=${IPV6_ENABLED:-0}\n    local EXTERNAL_IP=\"\"\n    local URLS=() CURL_CMD=\"curl\"\n\n    if (( ${IPV6_ENABLED} == 1 )); then\n        URLS=(\n            \"https://icanhazip.com\"\n            \"https://bot.whatismyipaddress.com\"\n            \"https://ifconfig.co\"\n            \"https://ident.me\"\n            \"https://api6.ipify.org\"\n        )\n        CURL_CMD=\"curl -6\"\n        IP_TEST=\"ipv6Test\"\n    else\n        URLS=(\n            \"https://icanhazip.com\"\n            \"https://ipecho.net/plain\"\n            \"https://myexternalip.com/raw\"\n            \"https://api.ipify.org\"\n            \"https://bot.whatismyipaddress.com\"\n        )\n        CURL_CMD=\"curl -4\"\n        IP_TEST=\"ipv4Test\"\n    fi\n\n    for URL in ${URLS[@]}; do\n        EXTERNAL_IP=$(${CURL_CMD} -s --connect-timeout 2 $URL 2>/dev/null)\n        ${IP_TEST} \"$EXTERNAL_IP\" && break\n    done\n\n    printf '%s' \"$EXTERNAL_IP\"\n}\n\n# prints internal ip address for the default route\ngetInternalIP() {\n    local IFACE=$(ip -4 route show default | awk '{print $5}' | head -1)\n    ip -4 -o addr show $IFACE | awk '{split($4,a,\"/\"); print a[1];}' | head -1\n}\n\n# $1 == cidr subnet\n# returns: 0 == success, 1 == failure\n# notes: prints first available ip in subnet\n# notes: assumes .1 is used as default gw in net\nfindAvailableIP() {\n    local NET_TAKEN_LIST=$(runas nmap -n -sP -T 5 \"$1\" -oG - | awk '/Up$/{print $2}')\n    local NET_ADDR_LIST=$(runas nmap -n -sL \"$1\" | grep \"Nmap scan report\" | awk '{print $NF}' | tail -n +3 | sed '$ d')\n    for IP in ${NET_ADDR_LIST[@]}; do\n        for ip in ${NET_TAKEN_LIST[@]}; do\n            if [[ \"$IP\" != \"$ip\" ]]; then\n                printf '%s' \"$IP\"\n                return 0\n            fi\n        done\n    done\n    return 1\n}\n\n# automate mysql_secure_installation\n# original: https://gist.github.com/kahidna/512b0d507ac90d1cbbf6b0230d38a502\n# $1 == new root password\n# $2 == old root password\nmysqlSecureInstall() {\n    local NEW_MYSQL_PASSWORD=\"$1\"\n    local CURRENT_MYSQL_PASSWORD=\"$2\"\n\nexpect <<EOF\nset timeout 3\nspawn mysql_secure_installation\nexpect \"Enter current password for root (enter for none):\"\nsend \"$CURRENT_MYSQL_PASSWORD\\r\"\nexpect \"root password?\"\nsend \"y\\r\"\nexpect \"New password:\"\nsend \"$NEW_MYSQL_PASSWORD\\r\"\nexpect \"Re-enter new password:\"\nsend \"$NEW_MYSQL_PASSWORD\\r\"\nexpect \"Remove anonymous users?\"\nsend \"y\\r\"\nexpect \"Disallow root login remotely?\"\nsend \"n\\r\"\nexpect \"Remove test database and access to it?\"\nsend \"y\\r\"\nexpect \"Reload privilege tables now?\"\nsend \"y\\r\"\nexpect eof\nEOF\n}\n\ngetCloudPlatform() {\n    # -- amazon web service check --\n    if curl -s -f --connect-timeout 2 http://169.254.169.254/latest/dynamic/instance-identity/ &>/dev/null; then\n        echo -n 'AWS'\n    # -- digital ocean check --\n    elif curl -s -f --connect-timeout 2 http://169.254.169.254/metadata/v1/id &>/dev/null; then\n        echo -n 'DO'\n    # -- google compute engine check --\n    elif curl -s -f --connect-timeout 2 -H \"Metadata-Flavor: Google\" http://metadata.google.internal/computeMetadata/v1/id &>/dev/null; then\n        echo -n 'GCE'\n    # -- microsoft azure check --\n    elif curl -s -f --connect-timeout 2 -H \"Metadata: true\" \"http://169.254.169.254/metadata/instance?api-version=2018-10-01\" &>/dev/null; then\n        echo -n 'AZURE'\n    # -- vultr cloud check --\n    elif curl -s -f --connect-timeout 2 http://169.254.169.254/v1/instanceid &>/dev/null; then\n        echo -n 'VULTR'\n    # -- oracle cloud environment check --\n    elif curl -s -f --connect-timeout 2 -H 'Authorization: Bearer Oracle' http://169.254.169.254/opc/v2/instance; then\n        echo -n 'OCE'\n    fi\n    # -- bare metal or unsupported cloud platform --\n}\n\n# $1 == attribute name\n# $2 == python config file\n# output: attribute value\ngetConfigAttrib() {\n    local NAME=\"$1\"\n    local CONFIG_FILE=\"$2\"\n\n    local VALUE=$(runas grep -oP '^(?!#)(?:'${NAME}')[ \\t]*=[ \\t]*\\K(?:\\w+\\(.*\\)[ \\t\\v]*$|[\\w\\d\\.]+[ \\t]*$|\\{.*\\}|\\[.*\\][ \\t]*$|\\(.*\\)[ \\t]*$|b?\"\"\".*\"\"\"[ \\t]*$|'\"b?'''.*'''\"'[ \\v]*$|b?\".*\"[ \\t]*$|'\"b?'.*'\"')' ${CONFIG_FILE})\n    printf '%s' \"${VALUE}\" | perl -0777 -pe 's~^b?[\"'\"'\"']+(.*?)[\"'\"'\"']+$|(.*)~\\1\\2~g'\n}\n\n# $1 == attribute name\n# $2 == attribute value\n# $3 == python config file\n# $4 == -q (quote string) | -qb (quote byte string)\nsetConfigAttrib() {\n    local NAME=\"$1\"\n    local VALUE=\"$2\"\n    local CONFIG_FILE=\"$3\"\n\n    if (( $# >= 4 )); then\n        if [[ \"$4\" == \"-q\" ]]; then\n            VALUE=\"'${VALUE}'\"\n        elif [[ \"$4\" == \"-qb\" ]]; then\n            VALUE=\"b'${VALUE}'\"\n        fi\n    fi\n    runas sed -i -r -e \"s|$NAME[ \\t]*=[ \\t]*.*|$NAME = $VALUE|g\" ${CONFIG_FILE}\n}\n\n# $1 == cmd as executed in systemd (by ExecStart=)\n# $2 == service file to add command to\n# notes: take precaution when adding long running functions as they will block startup in boot order\n# notes: adding init commands on an AMI instance must not be long running processes, otherwise they will fail\naddExcStartCmd() {\n    local CMD=$(printf '%s' \"$1\" | sed -e 's|[\\/&]|\\\\&|g') # escape string\n    local SVC_FILE=\"$2\"\n    local TMP_FILE=\"/tmp/${SVC_FILE}\"\n\n    # sanity check, does the entry already exist?\n    grep -q -oP \"^ExecStart\\=.*${CMD}.*\" 2>/dev/null ${SVC_FILE} && return 0\n\n    tac ${SVC_FILE} | sed -r \"0,\\|^ExecStart\\=.*|{s|^ExecStart\\=.*|ExecStart=${CMD}\\n&|}\" | tac > ${TMP_FILE}\n    runas mv -f ${TMP_FILE} ${SVC_FILE}\n\n    runas systemctl daemon-reload\n}\n\n# $1 == string to match for removal (after ExecStart=)\n# $2 == service file to remove command from\nremoveExecStartCmd() {\n    local STR=$(printf '%s' \"$1\" | sed -e 's|[\\/&]|\\\\&|g') # escape string\n    local SVC_FILE=\"$2\"\n\n    runas sed -i -r \"\\|^ExecStart\\=.*${STR}.*|d\" ${SVC_FILE}\n    runas systemctl daemon-reload\n}\n\n# $1 == service name (full name with target) to be dependent\n# $2 == service file to add dependency to\n# notes: only adds startup ordering dependency (service continues if dependency fails)\n# notes: the Before= section of init will link to an After= dependency on daemon-reload\naddDependsOnService() {\n    local SERVICE=\"$1\"\n    local SVC_FILE=\"$2\"\n\n    # sanity check, does the entry already exist?\n    grep -q -oP \"^(Before\\=|Wants\\=).*${SERVICE}.*\" 2>/dev/null ${SVC_FILE} && return 0\n\n    runas perl -i -e \"\\$service='$SERVICE';\" -pe 's%^(Before\\=|Wants\\=)(.*)%length($2)==0 ? \"${1}${service}\" : \"${1}${2} ${service}\"%ge;' ${SVC_FILE}\n    runas systemctl daemon-reload\n}\n\n# $1 == service name (full name with target) to remove dependency on\n# $2 == service file to remove dependency from\nremoveDependsOnService() {\n    local SERVICE=\"$1\"\n    local SVC_FILE=\"$2\"\n\n    runas perl -i -e \"\\$service='$SERVICE';\" -pe 's%^((?:Before\\=|Wants\\=).*?)( ${service}|${service} |${service})(.*)%\\1\\3%g;' ${SVC_FILE}\n    runas systemctl daemon-reload\n}\n\n# output: all physical network interfaces on this machine\ngetPhysicalIfaces() {\n    ( cd /sys/class/net && dirname */device; )\n}\n"
  },
  {
    "path": "Jenkinsfile",
    "content": "pipeline {\n  parameters {\n    password (name: 'DIGITALOCEAN_TOKEN')\n  }\n  environment {\n    TF_WORKSPACE = 'dev' //Sets the Terraform Workspace\n    TF_IN_AUTOMATION = 'true'\n    DIGITALOCEAN_TOKEN = \"${params.DIGITALOCEAN_TOKEND}\"\n  }\n  stages {\n    stage('Terraform Init') {\n      steps {\n        sh \"${env.TERRAFORM_HOME}/terraform init -input=false\"\n      }\n    }\n    stage('Terraform Plan') {\n      steps {\n        sh \"${env.TERRAFORM_HOME}/terraform plan -out=tfplan -input=false -var-file='dev.tfvars'\"\n      }\n    }\n    stage('Terraform Apply') {\n      steps {\n        input 'Apply Plan'\n        sh \"${env.TERRAFORM_HOME}/terraform apply -input=false tfplan\"\n      }\n    }\n   }\n}\n"
  },
  {
    "path": "Jenkinsfile.common",
    "content": "gitlabBuilds(builds: ['build', 'test', 'artifacts']) {\n\n  stage('build') { gitlabCommitStatus(name: 'build') {\n    sh \"./gradlew --no-daemon clean build\"\n  }}\n\n  stage('test') { gitlabCommitStatus(name: 'test') {\n    sh \"./gradlew --no-daemon check\"\n  }}\n\n  stage('artifacts') { gitlabCommitStatus(name: 'artifacts') {\n    archiveArtifacts artifacts: '**/build/libs/*.jar', fingerprint: true, onlyIfSuccessful: true\n    step([$class: 'JavadocArchiver', javadocDir: 'thrifty-compiler/build/docs/javadoc/', keepAll: false])\n    step([$class: 'JavadocArchiver', javadocDir: 'thrifty-schema/build/docs/javadoc/', keepAll: false])\n    step([$class: 'JavadocArchiver', javadocDir: 'thrifty-java-codegen/build/docs/javadoc/', keepAll: false])\n    step([$class: 'JavadocArchiver', javadocDir: 'thrifty-runtime/build/docs/javadoc/', keepAll: false])\n  }}\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright {yyyy} {name of copyright owner}\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# dSIPRouter Platform\n\n\n[<p align=\"center\"><img src=\"docs/dsiprouter_300px.png\" alt=\"dSIPRouter Logo\" width=\"300\"/></p>](https://dsiprouter.org)\n\n\n## What is dSIPRouter?\n\ndSIPRouter allows you to quickly turn [Kamailio](https://www.kamailio.org/) into an easy to use SIP Service Provider platform, which enables three basic use cases:\n\n- **SIP Trunking services:**\nProvide services to customers that have an on-premise PBX such as FreePBX, FusionPBX, Avaya, etc.\nWe have support for IP and credential based authentication.\n\n- **Hosted PBX services:**\nProxy SIP Endpoint requests to a multi-tenant PBX such as FusionPBX or single-tenant such as FreePBX.\nWe have an integration with FusionPBX that make this really easy and scalable!\n\n- **Microsoft Teams Direct Routing (Core Subscription Required):**\nWe can provide SBC functionality that allows dSIPRouter to interconnect your existing voice infrastructure or VoIP carrier to your Microsoft Teams environment.\n\n- **WebRTC Proxy (Core Subscription Required):**\nWe can provide functionality that allows dSIPRouter to register WebRTC clients to PBX's that has extensions being exposed as just UDP and TCP.  Hence, becoming a WebRTC Proxy.\n\nThe dSIPRouter UI allows you to manage the platform.  We also make it easy to intergrate dSIPRouter into your existing workflow by using our [API](https://www.postman.com/dopensource/workspace/dsiprouter/overview)\n\n**Follow us at [#dsiprouter](https://twitter.com/dsiprouter) on Twitter to get the latest updates on dSIPRouter**\n\n### Project Web Site\n\nCheck out our official website [dsiprouter.org](http://dsiprouter.org)\n\n### Demo System\n\nTry out our demo system [demo.dsiprouter.net](https://demo.dsiprouter.net:5000/)\n\nDemo system GUI Credentials:\n- username: `admin`\n- password: `ZmIwMTdmY2I5NjE4`\n\nDemo system API Credentials:\n\nYou can test out the API using the demo system.  We have defined a [Postman](https://www.postman.com/dopensource/workspace/dsiprouter/overview) collection that will make the process easier.  The API token is below:\n\n- bearer token: `9lyrny3HOtwgjR6JIMwRaMej9LijIS835zhVbD8ywHDzXT07Xm6vem1sgfvWkFz3`\n\n### Documentation\n\nYou can find our documentation online: [dSIPRouter Documentation](https://dsiprouter.readthedocs.io/en/latest)\nFor a list of updates and changes refer to our [Changelog](CHANGELOG.md)\n\n### Contributing\n\nSee the [Contributing Guidelines](CONTRIBUTING.md) for more details\nA current list of contributors can be found [here](CONTRIBUTORS.md)\n\n### Getting Started\n\nYou can find the steps to install of support operating systems:\n\n- [Debian Based Systems](https://dsiprouter.readthedocs.io/en/latest/debian_install.html#debian-install)\n- [Redhat Based Systems](https://dsiprouter.readthedocs.io/en/latest/rhel_install.html#rhel-install)\n\n### Support\n\n- Free Support: [dSIPRouter Question & Answer Forum](https://groups.google.com/forum/#!forum/dsiprouter)\n- Paid Support: [dSIPRouter Support](https://dsiprouter.org/#fh5co-support-section)\n\n### Training\n\nDetails on training can be found [here](https://dopensource.com/product/dsiprouter-admin-course/)\n\n### License\n\n- Apache License 2.0, [read more here](LICENSE)\n\n### Supported Features\n\n- Carrier Management\n  - Manage carriers as a group\n- Endpoint Management\n  - Manage endpoints as a group\n  - Call Limiting per Endpoint Group\n  - Call Detail Records generation per Endpoint Group\n- Notification System\n  - Over Call Limit Notifications\n  - Endpoint Failure Notifications\n  - Call Detail Record Notifications\n- Enhanced DID Management\n  - DID Failover to a Carrier/Endpoint Group or DID\n  - DID Hard Forwarding to a Carrier/Endpoint Group or DID\n  - Flowroute DID synchronization\n- Enhanced Route Management\n  - FusionPBX Domain Routing Enhancements\n  - Outbound / Inbound DID prefix routing\n  - Least Cost Locality Outbound routing\n  - Load balancing / sequential routing via groups\n  - Integration with your own custom Kamailio routes\n  - E911 Priority routing\n  - Local Extension routing\n  - Voicemail routing\n- Security\n  - TLS Enabled by Default\n  - Rate-limiting / DOS protection\n  - Teleblock blacklist support\n- High Availablity (Subscription Required)\n  - Mysql Active-Active replication\n  - Pacemaker / Corosync Active-Passive floating IP\n  - Consul DNS Load-balancing and DNS Failover\n  - dSIPRouter cluster synchronization\n  - Kamailio DMQ replication\n- Microsoft Teams Support (Subscription Required)\n- WebSockets Enabled by Default\n"
  },
  {
    "path": "cloud/build_image.sh",
    "content": "#!/usr/bin/env bash\n#\n# Summary:  build dsiprouter as an VM/VPS Deployable Image\n# Usage:    ./build_image.sh [--ver='<dsiprouter version>'] [--dir='<dsiprouter project directory>'] [--repo='<dsiprouter repo url>'] [--opts='<dsiprouter build options>']\n#\n\n# parse args if given\nwhile (( $# > 0 )); do\n    ARG=\"$1\"\n    case \"$ARG\" in\n        --ver=*)\n            DSIP_VERSION=$(echo \"$1\" | cut -d '=' -f 2)\n            shift\n            ;;\n        --dir=*)\n            DSIP_DIR=$(echo \"$1\" | cut -d '=' -f 2)\n            shift\n            ;;\n        --repo=*)\n            DSIP_REPO=$(echo \"$1\" | cut -d '=' -f 2)\n            shift\n            ;;\n        --opts=*)\n            BUILD_OPTIONS=$(echo \"$1\" | cut -d '=' -f 2)\n            shift\n            ;;\n        *)\n            echo \"[ERROR] argument $ARG is not valid\"\n            exit 1\n            ;;\n    esac\ndone\n\n# set defaults if needed, exports will be passed to dsiprouter.sh\nexport DSIP_VERSION=${DSIP_VERSION:-\"master\"}\nDSIP_DIR=${DSIP_DIR:-\"/opt/dsiprouter\"}\nDSIP_REPO=${DSIP_REPO:-\"https://github.com/dOpensource/dsiprouter.git\"}\nBUILD_OPTIONS=${BUILD_OPTIONS:-\"install -all\"}\nexport IMAGE_BUILD=1\n\nfunction cmdExists() {\n    if command -v \"$1\" > /dev/null 2>&1; then\n        return 0\n    else\n        return 1\n    fi\n}\n\n# wait for any other programs using package manager to complete\nif cmdExists \"apt-get\"; then\n    while fuser /var/lib/apt/lists/lock >/dev/null 2>&1; do\n        sleep 1\n    done\n\n    # make package manager quieter\n    export DEBIAN_FRONTEND=\"noninteractive\"\n    export DEBIAN_PRIORITY=\"critical\"\n\n    apt-get update -qq -y >/dev/null\n    apt-get install -qq -y git perl >/dev/null\n\n    # TODO: move to installScriptRequirements()\n    # make sure english UTF-8 locale is installed\n    if ! locale -a 2>/dev/null | grep -q 'en_US.UTF-8'; then\n          perl -i -pe 's%# (en_US\\.UTF-8 UTF-8)%\\1%' /etc/locale.gen\n          locale-gen\n    fi\nelif cmdExists 'dnf'; then\n    while [[ -f /var/run/dnf.pid ]]; do\n        sleep 1\n    done\n\n    dnf makecache -y --quiet --errorlevel=0 >/dev/null\n    dnf install -y --quiet --errorlevel=0 git >/dev/null\nelif cmdExists \"yum\"; then\n    while [ -f /var/run/yum.pid ]; do\n        sleep 1\n    done\n\n    yum makecache -y -q -e 0 >/dev/null\n    yum install -y -q -e 0 git >/dev/null\nfi\n\n# clone and install\ngit clone --depth 1 -c advice.detachedHead=false ${DSIP_REPO} -b ${DSIP_VERSION} ${DSIP_DIR} || exit 1\n${DSIP_DIR}/dsiprouter.sh ${BUILD_OPTIONS} || exit 1\n# cleanup environment for image\n${DSIP_DIR}/cloud/pre-snapshot.sh || exit 1\n\nexit 0\n"
  },
  {
    "path": "cloud/build_instance.sh",
    "content": "#!/usr/bin/env bash\n#\n# Summary:  build dsiprouter as an VM/VPS Instance\n# Usage:    ./build_instance.sh [--ver='<dsiprouter version>'] [--dir='<dsiprouter project directory>'] [--repo='<dsiprouter repo url>'] [--opts='<dsiprouter build options>']\n#\n\n# parse args if given\nwhile (( $# > 0 )); do\n    ARG=\"$1\"\n    case \"$ARG\" in\n        --ver=*)\n            DSIP_VERSION=$(echo \"$1\" | cut -d '=' -f 2)\n            shift\n            ;;\n        --dir=*)\n            DSIP_DIR=$(echo \"$1\" | cut -d '=' -f 2)\n            shift\n            ;;\n        --repo=*)\n            DSIP_REPO=$(echo \"$1\" | cut -d '=' -f 2)\n            shift\n            ;;\n        --opts=*)\n            BUILD_OPTIONS=$(echo \"$1\" | cut -d '=' -f 2)\n            shift\n            ;;\n        *)\n            echo \"[ERROR] argument $ARG is not valid\"\n            exit 1\n            ;;\n    esac\ndone\n\n# set defaults if needed, exports will be passed to dsiprouter.sh\nexport DSIP_VERSION=${DSIP_VERSION:-\"master\"}\nDSIP_DIR=${DSIP_DIR:-\"/opt/dsiprouter\"}\nDSIP_REPO=${DSIP_REPO:-\"https://github.com/dOpensource/dsiprouter.git\"}\nBUILD_OPTIONS=${BUILD_OPTIONS:-\"install -all\"}\n\nfunction cmdExists() {\n    if command -v \"$1\" > /dev/null 2>&1; then\n        return 0\n    else\n        return 1\n    fi\n}\n\n# wait for any other programs using package manager to complete\nif cmdExists \"apt-get\"; then\n    while fuser /var/lib/apt/lists/lock >/dev/null 2>&1; do\n        sleep 1\n    done\n\n    # make package manager quieter\n    export DEBIAN_FRONTEND=\"noninteractive\"\n    export DEBIAN_PRIORITY=\"critical\"\n\n    apt-get update -qq -y >/dev/null\n    apt-get install -qq -y git perl >/dev/null\n\n    # TODO: move to installScriptRequirements()\n    # make sure english UTF-8 locale is installed\n    if ! locale -a 2>/dev/null | grep -q 'en_US.UTF-8'; then\n          perl -i -pe 's%# (en_US\\.UTF-8 UTF-8)%\\1%' /etc/locale.gen\n          locale-gen\n    fi\nelif cmdExists 'dnf'; then\n    while [[ -f /var/run/dnf.pid ]]; do\n        sleep 1\n    done\n\n    dnf makecache -y --quiet --errorlevel=0 >/dev/null\n    dnf install -y --quiet --errorlevel=0 git >/dev/null\nelif cmdExists \"yum\"; then\n    while [[ -f /var/run/yum.pid ]]; do\n        sleep 1\n    done\n\n    yum makecache -y -q -e 0 >/dev/null\n    yum install -y -q -e 0 git >/dev/null\nfi\n\n# clone and install\ngit clone --depth 1 -c advice.detachedHead=false ${DSIP_REPO} -b ${DSIP_VERSION} ${DSIP_DIR} || exit 1\n${DSIP_DIR}/dsiprouter.sh ${BUILD_OPTIONS} || exit 1\n\nexit 0\n"
  },
  {
    "path": "cloud/cloud-init/configs/AWS.cfg",
    "content": "datasource_list: [ Ec2, NoCloud, None ]\ndisable_root: true\nssh_pwauth: false\nssh_deletekeys: true\nssh_genkeytypes: [rsa, dsa, ecdsa, ed25519]\nallow_public_ssh_keys: true\nssh_quiet_keygen: true\nmanage_etc_hosts: true\nmanage_resolv_conf: false\npreserve_hostname: false\napt_preserve_sources_list: true\n"
  },
  {
    "path": "cloud/cloud-init/configs/AZURE.cfg",
    "content": "datasource_list: [ Azure, NoCloud, None ]\ndisable_root: true\nssh_pwauth: false\nssh_deletekeys: true\nssh_genkeytypes: [rsa, dsa, ecdsa, ed25519]\nallow_public_ssh_keys: true\nssh_quiet_keygen: true\nmanage_etc_hosts: true\nmanage_resolv_conf: false\npreserve_hostname: false\napt_preserve_sources_list: true\n"
  },
  {
    "path": "cloud/cloud-init/configs/DO.cfg",
    "content": "datasource_list: [ ConfigDrive, DigitalOcean, NoCloud, None ]\ndisable_root: false\nssh_pwauth: false\nssh_deletekeys: true\nssh_genkeytypes: [rsa, dsa, ecdsa, ed25519]\nallow_public_ssh_keys: true\nssh_quiet_keygen: true\nmanage_etc_hosts: true\nmanage_resolv_conf: false\npreserve_hostname: false\napt_preserve_sources_list: true\n"
  },
  {
    "path": "cloud/cloud-init/configs/GCE.cfg",
    "content": "datasource_list: datasource_list: [ GCE, NoCloud, None ]\ndisable_root: true\nssh_pwauth: false\nssh_deletekeys: true\nssh_genkeytypes: [rsa, dsa, ecdsa, ed25519]\nallow_public_ssh_keys: true\nssh_quiet_keygen: true\nmanage_etc_hosts: true\nmanage_resolv_conf: false\npreserve_hostname: false\napt_preserve_sources_list: true\n"
  },
  {
    "path": "cloud/cloud-init/configs/VULTR.cfg",
    "content": "datasource_list: [ Vultr, NoCloud, None ]\ndisable_root: true\nssh_pwauth: false\nssh_deletekeys: true\nssh_genkeytypes: [rsa, dsa, ecdsa, ed25519]\nallow_public_ssh_keys: true\nssh_quiet_keygen: true\nmanage_etc_hosts: true\nmanage_resolv_conf: false\npreserve_hostname: false\napt_preserve_sources_list: true\n"
  },
  {
    "path": "cloud/cloud-init/templates/hosts.almalinux.tmpl",
    "content": "## template:jinja\n{#\nThis file /etc/cloud/templates/hosts.redhat.tmpl is only utilized\nif enabled in cloud-config.  Specifically, in order to enable it\nyou need to add the following to config:\n  manage_etc_hosts: True\n-#}\n# Your system has configured 'manage_etc_hosts' as True.\n# As a result, if you wish for changes to this file to persist\n# then you will need to either\n# a.) make changes to the master file in /etc/cloud/templates/hosts.redhat.tmpl\n# b.) change or remove the value of 'manage_etc_hosts' in\n#     /etc/cloud/cloud.cfg or cloud-config from user-data\n#\n# The following lines are desirable for IPv4 capable hosts\n127.0.0.1 {{fqdn}} {{hostname}}\n127.0.0.1 localhost.localdomain localhost\n127.0.0.1 localhost4.localdomain4 localhost4\n\n# The following lines are desirable for IPv6 capable hosts\n::1 {{fqdn}} {{hostname}}\n::1 localhost.localdomain localhost\n::1 localhost6.localdomain6 localhost6\n\n#####DSIP_CONFIG_START\n#####DSIP_CONFIG_END\n\n#####PACEMAKER_CONFIG_START\n#####PACEMAKER_CONFIG_END\n"
  },
  {
    "path": "cloud/cloud-init/templates/hosts.amzn.tmpl",
    "content": "## template:jinja\n{#\nThis file /etc/cloud/templates/hosts.redhat.tmpl is only utilized\nif enabled in cloud-config.  Specifically, in order to enable it\nyou need to add the following to config:\n  manage_etc_hosts: True\n-#}\n# Your system has configured 'manage_etc_hosts' as True.\n# As a result, if you wish for changes to this file to persist\n# then you will need to either\n# a.) make changes to the master file in /etc/cloud/templates/hosts.redhat.tmpl\n# b.) change or remove the value of 'manage_etc_hosts' in\n#     /etc/cloud/cloud.cfg or cloud-config from user-data\n#\n# The following lines are desirable for IPv4 capable hosts\n127.0.0.1 {{fqdn}} {{hostname}}\n127.0.0.1 localhost.localdomain localhost\n127.0.0.1 localhost4.localdomain4 localhost4\n\n# The following lines are desirable for IPv6 capable hosts\n::1 {{fqdn}} {{hostname}}\n::1 localhost.localdomain localhost\n::1 localhost6.localdomain6 localhost6\n\n#####DSIP_CONFIG_START\n#####DSIP_CONFIG_END\n\n#####PACEMAKER_CONFIG_START\n#####PACEMAKER_CONFIG_END\n"
  },
  {
    "path": "cloud/cloud-init/templates/hosts.centos.tmpl",
    "content": "## template:jinja\n{#\nThis file /etc/cloud/templates/hosts.redhat.tmpl is only utilized\nif enabled in cloud-config.  Specifically, in order to enable it\nyou need to add the following to config:\n  manage_etc_hosts: True\n-#}\n# Your system has configured 'manage_etc_hosts' as True.\n# As a result, if you wish for changes to this file to persist\n# then you will need to either\n# a.) make changes to the master file in /etc/cloud/templates/hosts.redhat.tmpl\n# b.) change or remove the value of 'manage_etc_hosts' in\n#     /etc/cloud/cloud.cfg or cloud-config from user-data\n#\n# The following lines are desirable for IPv4 capable hosts\n127.0.0.1 {{fqdn}} {{hostname}}\n127.0.0.1 localhost.localdomain localhost\n127.0.0.1 localhost4.localdomain4 localhost4\n\n# The following lines are desirable for IPv6 capable hosts\n::1 {{fqdn}} {{hostname}}\n::1 localhost.localdomain localhost\n::1 localhost6.localdomain6 localhost6\n\n#####DSIP_CONFIG_START\n#####DSIP_CONFIG_END\n\n#####PACEMAKER_CONFIG_START\n#####PACEMAKER_CONFIG_END\n"
  },
  {
    "path": "cloud/cloud-init/templates/hosts.debian.tmpl",
    "content": "## template:jinja\n{#\nThis file (/etc/cloud/templates/hosts.debian.tmpl) is only utilized\nif enabled in cloud-config.  Specifically, in order to enable it\nyou need to add the following to config:\n   manage_etc_hosts: True\n-#}\n# Your system has configured 'manage_etc_hosts' as True.\n# As a result, if you wish for changes to this file to persist\n# then you will need to either\n# a.) make changes to the master file in /etc/cloud/templates/hosts.debian.tmpl\n# b.) change or remove the value of 'manage_etc_hosts' in\n#     /etc/cloud/cloud.cfg or cloud-config from user-data\n#\n{# The value '{{hostname}}' will be replaced with the local-hostname -#}\n127.0.1.1 {{fqdn}} {{hostname}}\n127.0.0.1 localhost\n\n# The following lines are desirable for IPv6 capable hosts\n::1 ip6-localhost ip6-loopback\nfe00::0 ip6-localnet\nff00::0 ip6-mcastprefix\nff02::1 ip6-allnodes\nff02::2 ip6-allrouters\nff02::3 ip6-allhosts\n\n#####DSIP_CONFIG_START\n#####DSIP_CONFIG_END\n\n#####PACEMAKER_CONFIG_START\n#####PACEMAKER_CONFIG_END\n"
  },
  {
    "path": "cloud/cloud-init/templates/hosts.rhel.tmpl",
    "content": "## template:jinja\n{#\nThis file /etc/cloud/templates/hosts.redhat.tmpl is only utilized\nif enabled in cloud-config.  Specifically, in order to enable it\nyou need to add the following to config:\n  manage_etc_hosts: True\n-#}\n# Your system has configured 'manage_etc_hosts' as True.\n# As a result, if you wish for changes to this file to persist\n# then you will need to either\n# a.) make changes to the master file in /etc/cloud/templates/hosts.redhat.tmpl\n# b.) change or remove the value of 'manage_etc_hosts' in\n#     /etc/cloud/cloud.cfg or cloud-config from user-data\n#\n# The following lines are desirable for IPv4 capable hosts\n127.0.0.1 {{fqdn}} {{hostname}}\n127.0.0.1 localhost.localdomain localhost\n127.0.0.1 localhost4.localdomain4 localhost4\n\n# The following lines are desirable for IPv6 capable hosts\n::1 {{fqdn}} {{hostname}}\n::1 localhost.localdomain localhost\n::1 localhost6.localdomain6 localhost6\n\n#####DSIP_CONFIG_START\n#####DSIP_CONFIG_END\n\n#####PACEMAKER_CONFIG_START\n#####PACEMAKER_CONFIG_END\n"
  },
  {
    "path": "cloud/cloud-init/templates/hosts.rocky.tmpl",
    "content": "## template:jinja\n{#\nThis file /etc/cloud/templates/hosts.redhat.tmpl is only utilized\nif enabled in cloud-config.  Specifically, in order to enable it\nyou need to add the following to config:\n  manage_etc_hosts: True\n-#}\n# Your system has configured 'manage_etc_hosts' as True.\n# As a result, if you wish for changes to this file to persist\n# then you will need to either\n# a.) make changes to the master file in /etc/cloud/templates/hosts.redhat.tmpl\n# b.) change or remove the value of 'manage_etc_hosts' in\n#     /etc/cloud/cloud.cfg or cloud-config from user-data\n#\n# The following lines are desirable for IPv4 capable hosts\n127.0.0.1 {{fqdn}} {{hostname}}\n127.0.0.1 localhost.localdomain localhost\n127.0.0.1 localhost4.localdomain4 localhost4\n\n# The following lines are desirable for IPv6 capable hosts\n::1 {{fqdn}} {{hostname}}\n::1 localhost.localdomain localhost\n::1 localhost6.localdomain6 localhost6\n\n#####DSIP_CONFIG_START\n#####DSIP_CONFIG_END\n\n#####PACEMAKER_CONFIG_START\n#####PACEMAKER_CONFIG_END\n"
  },
  {
    "path": "cloud/cloud-init/templates/hosts.ubuntu.tmpl",
    "content": "## template:jinja\n{#\nThis file (/etc/cloud/templates/hosts.debian.tmpl) is only utilized\nif enabled in cloud-config.  Specifically, in order to enable it\nyou need to add the following to config:\n   manage_etc_hosts: True\n-#}\n# Your system has configured 'manage_etc_hosts' as True.\n# As a result, if you wish for changes to this file to persist\n# then you will need to either\n# a.) make changes to the master file in /etc/cloud/templates/hosts.debian.tmpl\n# b.) change or remove the value of 'manage_etc_hosts' in\n#     /etc/cloud/cloud.cfg or cloud-config from user-data\n#\n{# The value '{{hostname}}' will be replaced with the local-hostname -#}\n127.0.1.1 {{fqdn}} {{hostname}}\n127.0.0.1 localhost\n\n# The following lines are desirable for IPv6 capable hosts\n::1 ip6-localhost ip6-loopback\nfe00::0 ip6-localnet\nff00::0 ip6-mcastprefix\nff02::1 ip6-allnodes\nff02::2 ip6-allrouters\nff02::3 ip6-allhosts\n\n#####DSIP_CONFIG_START\n#####DSIP_CONFIG_END\n\n#####PACEMAKER_CONFIG_START\n#####PACEMAKER_CONFIG_END\n"
  },
  {
    "path": "cloud/find_hosts_tmpl.sh",
    "content": "#!/usr/bin/env bash\n#\n# Summary: find the correct hosts template for the current environment\n#\n\ncase \"$(cloud-init query distro)\" in\nalpine)\n    echo '/etc/cloud/templates/hosts.alpine.tmpl'\n    ;;\narch)\n    echo '/etc/cloud/templates/hosts.arch.tmpl'\n    ;;\ndebian|ubuntu)\n    echo '/etc/cloud/templates/hosts.debian.tmpl'\n    ;;\nfreebsd|dragonfly)\n    echo '/etc/cloud/templates/hosts.freebsd.tmpl'\n    ;;\ngentoo|cos)\n    echo '/etc/cloud/templates/hosts.gentoo.tmpl'\n    ;;\nnetbsd)\n    echo '/etc/cloud/templates/hosts.netbsd.tmpl'\n    ;;\nopenbsd)\n    echo '/etc/cloud/templates/hosts.openbsd.tmpl'\n    ;;\nalmalinux|amazon|centos|cloudlinux|eurolinux|fedora|mariner|miraclelinux|openmandriva|photon|rhel|rocky|virtuozzo)\n    echo '/etc/cloud/templates/hosts.redhat.tmpl'\n    ;;\nopensuse|opensuse-leap|opensuse-microos|opensuse-tumbleweed|sle_hpc|sle-micro|sles|suse)\n    echo '/etc/cloud/templates/hosts.suse.tmpl'\n    ;;\nopeneuler)\n    echo '/etc/cloud/templates/hosts.openeuler.tmpl'\n    ;;\nOpenCloudOS|TencentOS)\n    echo '/etc/cloud/templates/hosts.OpenCloudOS.tmpl'\n    ;;\n*)\n    echo '/etc/cloud/templates/hosts.tmpl'\n    ;;\nesac\n"
  },
  {
    "path": "cloud/pre-snapshot.sh",
    "content": "#!/usr/bin/env bash\n#\n# Summary: clean up / harden system before creating an image\n#\n\nfunction cmdExists() {\n    if command -v \"$1\" >/dev/null 2>&1; then\n        return 0\n    else\n        return 1\n    fi\n}\n\nfunction getDistroName() {\n    grep '^ID=' /etc/os-release 2>/dev/null | cut -d '=' -f 2 | cut -d '\"' -f 2\n}\n\nfunction joinwith() {\n    local START=\"$1\" IFS=\"$2\" END=\"$3\" ARR=()\n    shift;shift;shift\n\n    for VAR in \"$@\"; do\n        ARR+=(\"${START}${VAR}${END}\")\n    done\n\n    echo \"${ARR[*]}\"\n}\n\n# removed from cleanup logic as this is run on virtual hardware\n# we shouldn't need to flush the disks, this saves us time\nfunction clearDiskCache() {\n    dd if=/dev/zero of=/zerofile 2>/dev/null\n    rm -f /zerofile\n    sync\n}\n\n# make sure all security updates are installed\n# remove insecure services (FTP, Telnet, Rlogin/Rsh)\n# TEMP: remove known bad packages\n# TODO: in the future this will instead be handled by using pre-packaged binaries\nfunction runSecurityUpdates() {\n    if cmdExists 'apt-get'; then\n        # grub updates adhere to ucf not debconf\n        # make sure ucf defaults to unattended upgrade\n        unset UCF_FORCE_CONFFOLD\n        export UCF_FORCE_CONFFNEW=YES\n        ucf --purge /boot/grub/menu.lst\n\n        apt-mark hold linux-image-* linux-headers-*\n        apt-get update -y\n        apt-get upgrade -y\n        apt-mark unhold linux-image-* linux-headers-*\n\n        apt-get remove -y --purge xinetd nis yp-tools tftpd atftpd tftpd-hpa telnetd rsh-server rsh-redone-server\n        apt-get remove -y --purge libcap-dev\n\n        apt-get autoremove -y --purge\n        apt-get clean -y\n    elif cmdExists 'dnf'; then\n        dnf upgrade -y --exclude='kernel*' --exclude='linux-headers-*'\n\n        dnf remove -y xinetd ypserv tftp-server telnet-server rsh-server\n        dnf remove -y libcap-devel\n\n        dnf autoremove -y\n        dnf clean all\n    elif cmdExists 'yum'; then\n        yum upgrade -y --exclude='linux-image-*' --exclude='linux-headers-*'\n\n        yum remove -y xinetd ypserv tftp-server telnet-server rsh-server\n        yum remove -y libcap-devel\n\n        yum autoremove -y\n        yum clean all -y\n    fi\n}\n\nfunction hardenSshdConfigs() {\n    (\n        cat <<'EOF'\n# |== SSHD Server Settings ==|\nPort 22\nProtocol 2\n\n# |== Log Settings ==|\nSyslogFacility AUTH\nLogLevel INFO\n\n# |== Authentication Settings ==|\n# we only allow pubkey auth using ssh protocol v2\nPermitRootLogin no\nStrictModes yes\n# enable protocol v2 auth\nPubkeyAuthentication yes\n# disable protocol v1 auth\nRSAAuthentication no\nChallengeResponseAuthentication no\nPasswordAuthentication no\nKerberosAuthentication no\nGSSAPIAuthentication no\nPermitEmptyPasswords no\n# HostKeys for protocol v2\n# see: man sshd_config for more details\nHostKey /etc/ssh/ssh_host_rsa_key\nHostKey /etc/ssh/ssh_host_dsa_key\nHostKey /etc/ssh/ssh_host_ecdsa_key\nHostKey /etc/ssh/ssh_host_ed25519_key\n# where to check for authorized keys\nAuthorizedKeysFile .ssh/authorized_keys\n\n# |== Security Settings ==|\n# Process is unprivileged until auth is complete\nUsePrivilegeSeparation yes\n# Make brute force attempts much harder\n# NOTE: if you have many identity keys (>5) each one causes an auth attempt and this may cause auth failure\n# clients with this issue need to specify the key explicitly for that host (on cmdline or in ~/.ssh/ssh_config)\n# ex) ssh -o IdentitiesOnly=yes -i ~/.ssh/<your key>.pem <user>@<host>\nMaxAuthTries 5\nLoginGraceTime 60\n# Don't read the user's ~/.rhosts and ~/.shosts files\nIgnoreRhosts yes\n# Don't allow remote host auth protocol v1\nRhostsRSAAuthentication no\n# Don't allow remote host auth protocol v2\nHostbasedAuthentication no\n# PAM is needed for some 2-factor auth solutions\nUsePAM yes\n# Some exploits have been published using X11 offsets\n# so we disable it just in case\nX11Forwarding no\n\n# |== General sSettings ==|\nPrintMotd yes\nTCPKeepAlive yes\nClientAliveInterval 240\n# Allow client to pass locale environment variable\nAcceptEnv LANG LANGUAGE LC_*\n# Allow sftp over ssh\nSubsystem sftp /usr/lib/openssh/sftp-server\nEOF\n    ) >/etc/ssh/sshd_config\n}\n\nfunction hardenKernelConfigs() {\n    # source: https://www.cyberciti.biz/tips/linux-security.html\n    (\n        cat <<'EOF'\n######################################################################\n# /etc/sysctl.conf - Configuration file for setting system variables\n# See /etc/sysctl.d/ for additional system variables.\n# See sysctl.conf (5) for information.\n######################################################################\n\n# Turn on execshield\nkernel.exec-shield=1\n# ASLR enabled on boot\nkernel.randomize_va_space=1\n# Enable IP spoofing protection\nnet.ipv4.conf.all.rp_filter=1\n# Disable IP source routing\nnet.ipv4.conf.all.accept_source_route=0\n# Ignoring broadcasts request\nnet.ipv4.icmp_echo_ignore_broadcasts=1\nnet.ipv4.icmp_ignore_bogus_error_messages=1\n# Make sure spoofed packets get logged\nnet.ipv4.conf.all.log_martians = 1\nEOF\n    ) >/etc/sysctl.conf\n\n    # ensure address space layout randomization (ASLR) is enabled\n    echo '2' >/proc/sys/kernel/randomize_va_space\n}\n\n# sets up the filesystem as a golden image\n# we are running a portoin of the \"cloud-init clean\" logic to ensure we keep dsiprouter specific scripts\n# cloud-init versions < 23.1 remove machine-id instead of zeroing it out\n# see discussion here: https://bugs.launchpad.net/ubuntu/+source/cloud-init/+bug/1563951\n# TODO: revisit this in the future, there is a bit more logic they do we might want to incorporate:\n# ref: https://github.com/canonical/cloud-init/blob/main/cloudinit/cmd/clean.py#L165\nfunction cleanCloudInit() {\n    find /var/lib/cloud -mindepth 1 -maxdepth 1 -type d ! -name 'scripts' -exec rm -rf {} +\n    find /var/lib/cloud -mindepth 1 -maxdepth 1 ! -type d -exec rm -f {} +\n    truncate -s 0 /etc/machine-id\n}\n\nfunction cleanUserAccounts() {\n    # delete any accounts attempting to be root\n    BAD_USERS=$(joinwith '' ';' 'd' $(awk -F ':' '($3 == \"0\") && !/root/ {print FNR}' /etc/passwd))\n    [[ ! -z \"${BAD_USERS}\" ]] && sed -i \"/${BAD_USERS}/d\" /etc/passwd\n    # remove and lock the root user's password\n    passwd -d root\n    passwd -l root\n}\n\n# remove ssh keys, remove known hosts files\nfunction cleanKeys() {\n    rm -f /etc/ssh/*key* /root/.ssh/{authorized_keys,known_hosts} /home/*/.ssh/{authorized_keys,known_hosts} 2>/dev/null\n    touch /etc/ssh/revoked_keys\n    chmod 600 /etc/ssh/revoked_keys\n}\n\n# let cloud-init write these on boot\nfunction cleanNetworkConfigs() {\n    rm -f /etc/systemd/network/*\n    rm -f /etc/netplan/*\n}\n\nfunction cleanSourceFiles() {\n    find /usr/local/src -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +\n    find /usr/local/src -mindepth 1 -maxdepth 1 ! -type d -exec rm -f {} +\n}\n\nfunction cleanRuntimeFiles() {\n    (\n        shopt -s globstar\n        rm -rf /opt/dsiprouter/**/__pycache__/\n    )\n    rm -rf /run/*\n    rm -rf /tmp/* /var/tmp/*\n}\n\n# remove logs and any information from build process\nfunction cleanLogs() {\n    history -c\n    truncate -s 0 /root/.*history /home/*/.*history 2>/dev/null\n    unset HISTFILE\n    find /var/log -mtime -1 -type f -exec truncate -s 0 {} +\n    rm -rf /var/log/*.gz /var/log/*.[0-9] /var/log/*-????????\n    truncate -s 0 /var/log/lastlog /var/log/wtmp\n}\n\n# main logic\nrunSecurityUpdates\nhardenSshdConfigs\nhardenKernelConfigs\ncleanUserAccounts\ncleanKeys\ncleanNetworkConfigs\ncleanSourceFiles\ncleanLogs\ncleanCloudInit\ncleanRuntimeFiles\nexit 0\n"
  },
  {
    "path": "dnsmasq/almalinux/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # mask the service before running package manager to avoid faulty startup errors\n    systemctl mask dnsmasq.service\n\n    yum install -y dnsmasq\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # make sure we unmask before configuring the service ourselves\n    systemctl unmask dnsmasq.service\n\n    # Configure dnsmasq systemd service\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/systemd/dnsmasq-v2.service /lib/systemd/system/dnsmasq.service\n    chmod 644 /lib/systemd/system/dnsmasq.service\n    systemctl daemon-reload\n    systemctl enable dnsmasq\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop and disable services\n    systemctl disable dnsmasq\n    systemctl stop dnsmasq\n\n    # Uninstall packages\n    yum remove -y dnsmasq\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dnsmasq/amzn/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # mask the service before running package manager to avoid faulty startup errors\n    systemctl mask dnsmasq.service\n\n    yum install -y dnsmasq\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # make sure we unmask before configuring the service ourselves\n    systemctl unmask dnsmasq.service\n\n    # configure dnsmasq systemd service\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/systemd/dnsmasq-v3.service /lib/systemd/system/dnsmasq.service\n    chmod 644 /lib/systemd/system/dnsmasq.service\n    systemctl daemon-reload\n    systemctl enable dnsmasq\n\n    # backup the original resolv.conf and dhclient scripts\n    [[ ! -e \"${BACKUPS_DIR}/etc/resolv.conf\" ]] && {\n        mkdir -p ${BACKUPS_DIR}/{etc/sysconfig/network-scripts/,usr/sbin/}\n        cp -df /etc/resolv.conf ${BACKUPS_DIR}/etc/resolv.conf\n        cp -dfr /etc/sysconfig/network-scripts/. ${BACKUPS_DIR}/etc/sysconfig/network-scripts/\n        cp -f /usr/sbin/dhclient-script ${BACKUPS_DIR}/usr/sbin/\n    }\n\n    # make dnsmasq the DNS provider\n    rm -f /etc/resolv.conf\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/resolv.conf /etc/resolv.conf\n\n    # make all the dhclient scripts use a different resolv.conf location\n    sed -i 's%/etc/resolv\\.conf%/var/lib/dhclient/resolv.conf%g' /usr/sbin/dhclient-script\n    find /etc/sysconfig/network-scripts/ -type f -exec sed -i 's%/etc/resolv\\.conf%/var/lib/dhclient/resolv.conf%g' {} +\n\n    # make sure the dhclient resolv.conf is recreated in the new location\n    dhclient -r && dhclient\n\n    # tell dnsmasq to grab dynamic DNS servers from dhclient\n    export DNSMASQ_RESOLV_FILE=\"/var/lib/dhclient/resolv.conf\"\n    envsubst <${DSIP_PROJECT_DIR}/dnsmasq/configs/dnsmasq_sh.conf >/etc/dnsmasq.conf\n\n    return 0\n}\n\nfunction uninstall {\n    # stop and disable services\n    systemctl disable dnsmasq\n    systemctl stop dnsmasq\n\n    # uninstall packages\n    yum remove -y dnsmasq\n\n    # restore dhclient scripts\n    cp -dfr ${BACKUPS_DIR}/etc/sysconfig/network-scripts/. /etc/sysconfig/network-scripts/\n    cp -f ${BACKUPS_DIR}/usr/sbin/dhclient-script /usr/sbin/dhclient-script\n\n    # restore original resolv.conf\n    cp -df ${BACKUPS_DIR}/etc/resolv.conf /etc/resolv.conf\n\n    # update resolv.conf if needed\n    dhclient -r && dhclient\n\n    # cleanup backup files\n    rm -rf ${BACKUPS_DIR}/etc/sysconfig/network-scripts/\n    rm -f ${BACKUPS_DIR}/{etc/resolv.conf,usr/sbin/dhclient-script}\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dnsmasq/centos/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # mask the service before running package manager to avoid faulty startup errors\n    systemctl mask dnsmasq.service\n\n    if (( ${DISTRO_VER} >= 8 )); then\n        dnf install -y dnsmasq\n    else\n        yum install -y dnsmasq\n    fi\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # make sure we unmask before configuring the service ourselves\n    systemctl unmask dnsmasq.service\n\n    # configure dnsmasq systemd service\n    if (( ${DISTRO_VER} > 7 )); then\n        cp -f ${DSIP_PROJECT_DIR}/dnsmasq/systemd/dnsmasq-v2.service /lib/systemd/system/dnsmasq.service\n    else\n        cp -f ${DSIP_PROJECT_DIR}/dnsmasq/systemd/dnsmasq-v3.service /lib/systemd/system/dnsmasq.service\n    fi\n    chmod 644 /lib/systemd/system/dnsmasq.service\n    systemctl daemon-reload\n    systemctl enable dnsmasq\n\n    # backup the original resolv.conf\n    [[ ! -e \"${BACKUPS_DIR}/etc/resolv.conf\" ]] && {\n        mkdir -p ${BACKUPS_DIR}/etc/\n        cp -df /etc/resolv.conf ${BACKUPS_DIR}/etc/resolv.conf\n    }\n\n    # make dnsmasq the DNS provider\n    # centos uses a static resolv.conf by default, which dnsmasq will use for its upstream DNS servers\n    [[ ! -e /etc/dnsmasq_resolv.conf ]] && {\n        cp -df /etc/resolv.conf /etc/dnsmasq_resolv.conf\n    }\n    rm -f /etc/resolv.conf\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/resolv.conf /etc/resolv.conf\n\n    # tell NetworkManager we will manage the DNS servers\n    mkdir -p /etc/NetworkManager/conf.d/\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/networkmanager/dsiprouter.conf /etc/NetworkManager/conf.d/99-dsiprouter.conf\n\n    # make sure the NetworkManager resolv.conf is recreated with the new configuration options\n    systemctl restart NetworkManager\n\n    # tell dnsmasq to grab dynamic DNS servers from dhclient\n    export DNSMASQ_RESOLV_FILE=\"/etc/dnsmasq_resolv.conf\"\n    envsubst <${DSIP_PROJECT_DIR}/dnsmasq/configs/dnsmasq_sh.conf >/etc/dnsmasq.conf\n\n    return 0\n}\n\nfunction uninstall {\n    # stop and disable services\n    systemctl disable dnsmasq\n    systemctl stop dnsmasq\n\n    # uninstall packages\n    if (( ${DISTRO_VER} >= 8 )); then\n        dnf remove -y dnsmasq\n    else\n        yum remove -y dnsmasq\n    fi\n\n    # remove our NetworkManager configurations\n    rm -f /etc/NetworkManager/conf.d/99-dsiprouter.conf\n\n    # restore original resolv.conf\n    cp -df ${BACKUPS_DIR}/etc/resolv.conf /etc/resolv.conf\n\n    # update resolv.conf / restart NetworkManager with new configs\n    systemctl restart NetworkManager\n\n    # cleanup backup files\n    rm -f ${BACKUPS_DIR}/etc/resolv.conf\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dnsmasq/configs/dnsmasq_sh.conf",
    "content": "# Configuration file for dnsmasq.\n#\n# Format is one option per line, legal options are the same\n# as the long options legal on the command line. See\n# \"/usr/sbin/dnsmasq --help\" or \"man 8 dnsmasq\" for details.\n\n# Listen on this specific port instead of the standard DNS port\n# (53). Setting this to zero completely disables DNS function,\n# leaving only DHCP and/or TFTP.\nport=53\n\n# The following two options make you a better netizen, since they\n# tell dnsmasq to filter out queries which the public DNS cannot\n# answer, and which load the servers (especially the root servers)\n# unnecessarily. If you have a dial-on-demand link they also stop\n# these requests from bringing up the link unnecessarily.\n\n# Never forward plain names (without a dot or domain part)\ndomain-needed\n# Never forward addresses in the non-routed address spaces.\nbogus-priv\n\n# Uncomment these to enable DNSSEC validation and caching:\n# (Requires dnsmasq to be built with DNSSEC option.)\n#conf-file=%%PREFIX%%/share/dnsmasq/trust-anchors.conf\n#dnssec\n\n# Replies which are not DNSSEC signed may be legitimate, because the domain\n# is unsigned, or may be forgeries. Setting this option tells dnsmasq to\n# check that an unsigned reply is OK, by finding a secure proof that a DS\n# record somewhere between the root and the domain does not exist.\n# The cost of setting this is that even queries in unsigned domains will need\n# one or more extra DNS queries to verify.\n#dnssec-check-unsigned\n\n# Uncomment this to filter useless windows-originated DNS requests\n# which can trigger dial-on-demand links needlessly.\n# Note that (amongst other things) this blocks all SRV requests,\n# so don't use it if you use eg Kerberos, SIP, XMMP or Google-talk.\n# This option only affects forwarding, SRV records originating for\n# dnsmasq (via srv-host= lines) are not suppressed by it.\n#filterwin2k\n\n# Change this line if you want dns to get its upstream servers from\n# somewhere other that /etc/resolv.conf\nresolv-file=${DNSMASQ_RESOLV_FILE}\n\n# By  default,  dnsmasq  will  send queries to any of the upstream\n# servers it knows about and tries to favour servers to are  known\n# to  be  up.  Uncommenting this forces dnsmasq to try each query\n# with  each  server  strictly  in  the  order  they   appear   in\n# /etc/resolv.conf\nstrict-order\n\n# If you don't want dnsmasq to read /etc/resolv.conf or any other\n# file, getting its servers from this file instead (see below), then\n# uncomment this.\n#no-resolv\n\n# If you don't want dnsmasq to poll /etc/resolv.conf or other resolv\n# files for changes and re-read them then uncomment this.\n#no-poll\n\n# Add other name servers here, with domain specs if they are for\n# non-public domains.\n#server=/localnet/192.168.0.1\n\n# Example of routing PTR queries to nameservers: this will send all\n# address->name queries for 192.168.3/24 to nameserver 10.1.2.3\n#server=/3.168.192.in-addr.arpa/10.1.2.3\n\n# Add local-only domains here, queries in these domains are answered\n# from /etc/hosts or DHCP only.\n#local=/localnet/\n\n# Add domains which you want to force to an IP address here.\n# The example below send any host in double-click.net to a local\n# web-server.\n#address=/double-click.net/127.0.0.1\n\n# --address (and --server) work with IPv6 addresses too.\n#address=/www.thekelleys.org.uk/fe80::20d:60ff:fe36:f83\n\n# Add the IPs of all queries to yahoo.com, google.com, and their\n# subdomains to the vpn and search ipsets:\n#ipset=/yahoo.com/google.com/vpn,search\n\n# Add the IPs of all queries to yahoo.com, google.com, and their\n# subdomains to netfilters sets, which is equivalent to\n# 'nft add element ip test vpn { ... }; nft add element ip test search { ... }'\n#nftset=/yahoo.com/google.com/ip#test#vpn,ip#test#search\n\n# Use netfilters sets for both IPv4 and IPv6:\n# This adds all addresses in *.yahoo.com to vpn4 and vpn6 for IPv4 and IPv6 addresses.\n#nftset=/yahoo.com/4#ip#test#vpn4\n#nftset=/yahoo.com/6#ip#test#vpn6\n\n# You can control how dnsmasq talks to a server: this forces\n# queries to 10.1.2.3 to be routed via eth1\n# server=10.1.2.3@eth1\n\n# and this sets the source (ie local) address used to talk to\n# 10.1.2.3 to 192.168.1.1 port 55 (there must be an interface with that\n# IP on the machine, obviously).\n# server=10.1.2.3@192.168.1.1#55\n\n# If you want dnsmasq to change uid and gid to something other\n# than the default, edit the following lines.\nuser=dnsmasq\ngroup=dnsmasq\n\n# Where dnsmasq will write it's process id.\npid-file=/run/dnsmasq/dnsmasq.pid\n\n# If you want dnsmasq to listen for DHCP and DNS requests only on\n# specified interfaces (and the loopback) give the name of the\n# interface (eg eth0) here.\n# Repeat the line for more than one interface.\ninterface=lo\n# Or you can specify which interface _not_ to listen on\n#except-interface=\n# Or which to listen on by address (remember to include 127.0.0.1 if\n# you use this.)\n#listen-address=\n# If you want dnsmasq to provide only DNS service on an interface,\n# configure it as shown above, and then use the following line to\n# disable DHCP and TFTP on it.\n#no-dhcp-interface=\n\n# On systems which support it, dnsmasq binds the wildcard address,\n# even when it is listening on only some interfaces. It then discards\n# requests that it shouldn't reply to. This has the advantage of\n# working even when interfaces come and go and change address. If you\n# want dnsmasq to really bind only the interfaces it is listening on,\n# uncomment this option. About the only time you may need this is when\n# running another nameserver on the same machine.\nbind-interfaces\n\n# If you don't want dnsmasq to read /etc/hosts, uncomment the\n# following line.\n#no-hosts\n# or if you want it to read another file, as well as /etc/hosts, use\n# this.\n#addn-hosts=/etc/banner_add_hosts\n\n# Set this (and domain: see below) if you want to have a domain\n# automatically added to simple names in a hosts-file.\n#expand-hosts\n\n# Set the domain for dnsmasq. this is optional, but if it is set, it\n# does the following things.\n# 1) Allows DHCP hosts to have fully qualified domain names, as long\n#     as the domain part matches this setting.\n# 2) Sets the \"domain\" DHCP option thereby potentially setting the\n#    domain of all systems configured by DHCP\n# 3) Provides the domain part for \"expand-hosts\"\n#domain=thekelleys.org.uk\n\n# Set a different domain for a particular subnet\n#domain=wireless.thekelleys.org.uk,192.168.2.0/24\n\n# Same idea, but range rather then subnet\n#domain=reserved.thekelleys.org.uk,192.68.3.100,192.168.3.200\n\n# Uncomment this to enable the integrated DHCP server, you need\n# to supply the range of addresses available for lease and optionally\n# a lease time. If you have more than one network, you will need to\n# repeat this for each network on which you want to supply DHCP\n# service.\n#dhcp-range=192.168.0.50,192.168.0.150,12h\n\n# This is an example of a DHCP range where the netmask is given. This\n# is needed for networks we reach the dnsmasq DHCP server via a relay\n# agent. If you don't know what a DHCP relay agent is, you probably\n# don't need to worry about this.\n#dhcp-range=192.168.0.50,192.168.0.150,255.255.255.0,12h\n\n# This is an example of a DHCP range which sets a tag, so that\n# some DHCP options may be set only for this network.\n#dhcp-range=set:red,192.168.0.50,192.168.0.150\n\n# Use this DHCP range only when the tag \"green\" is set.\n#dhcp-range=tag:green,192.168.0.50,192.168.0.150,12h\n\n# Specify a subnet which can't be used for dynamic address allocation,\n# is available for hosts with matching --dhcp-host lines. Note that\n# dhcp-host declarations will be ignored unless there is a dhcp-range\n# of some type for the subnet in question.\n# In this case the netmask is implied (it comes from the network\n# configuration on the machine running dnsmasq) it is possible to give\n# an explicit netmask instead.\n#dhcp-range=192.168.0.0,static\n\n# Enable DHCPv6. Note that the prefix-length does not need to be specified\n# and defaults to 64 if missing/\n#dhcp-range=1234::2, 1234::500, 64, 12h\n\n# Do Router Advertisements, BUT NOT DHCP for this subnet.\n#dhcp-range=1234::, ra-only\n\n# Do Router Advertisements, BUT NOT DHCP for this subnet, also try and\n# add names to the DNS for the IPv6 address of SLAAC-configured dual-stack\n# hosts. Use the DHCPv4 lease to derive the name, network segment and\n# MAC address and assume that the host will also have an\n# IPv6 address calculated using the SLAAC algorithm.\n#dhcp-range=1234::, ra-names\n\n# Do Router Advertisements, BUT NOT DHCP for this subnet.\n# Set the lifetime to 46 hours. (Note: minimum lifetime is 2 hours.)\n#dhcp-range=1234::, ra-only, 48h\n\n# Do DHCP and Router Advertisements for this subnet. Set the A bit in the RA\n# so that clients can use SLAAC addresses as well as DHCP ones.\n#dhcp-range=1234::2, 1234::500, slaac\n\n# Do Router Advertisements and stateless DHCP for this subnet. Clients will\n# not get addresses from DHCP, but they will get other configuration information.\n# They will use SLAAC for addresses.\n#dhcp-range=1234::, ra-stateless\n\n# Do stateless DHCP, SLAAC, and generate DNS names for SLAAC addresses\n# from DHCPv4 leases.\n#dhcp-range=1234::, ra-stateless, ra-names\n\n# Do router advertisements for all subnets where we're doing DHCPv6\n# Unless overridden by ra-stateless, ra-names, et al, the router\n# advertisements will have the M and O bits set, so that the clients\n# get addresses and configuration from DHCPv6, and the A bit reset, so the\n# clients don't use SLAAC addresses.\n#enable-ra\n\n# Supply parameters for specified hosts using DHCP. There are lots\n# of valid alternatives, so we will give examples of each. Note that\n# IP addresses DO NOT have to be in the range given above, they just\n# need to be on the same network. The order of the parameters in these\n# do not matter, it's permissible to give name, address and MAC in any\n# order.\n\n# Always allocate the host with Ethernet address 11:22:33:44:55:66\n# The IP address 192.168.0.60\n#dhcp-host=11:22:33:44:55:66,192.168.0.60\n\n# Always set the name of the host with hardware address\n# 11:22:33:44:55:66 to be \"fred\"\n#dhcp-host=11:22:33:44:55:66,fred\n\n# Always give the host with Ethernet address 11:22:33:44:55:66\n# the name fred and IP address 192.168.0.60 and lease time 45 minutes\n#dhcp-host=11:22:33:44:55:66,fred,192.168.0.60,45m\n\n# Give a host with Ethernet address 11:22:33:44:55:66 or\n# 12:34:56:78:90:12 the IP address 192.168.0.60. Dnsmasq will assume\n# that these two Ethernet interfaces will never be in use at the same\n# time, and give the IP address to the second, even if it is already\n# in use by the first. Useful for laptops with wired and wireless\n# addresses.\n#dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.0.60\n\n# Give the machine which says its name is \"bert\" IP address\n# 192.168.0.70 and an infinite lease\n#dhcp-host=bert,192.168.0.70,infinite\n\n# Always give the host with client identifier 01:02:02:04\n# the IP address 192.168.0.60\n#dhcp-host=id:01:02:02:04,192.168.0.60\n\n# Always give the InfiniBand interface with hardware address\n# 80:00:00:48:fe:80:00:00:00:00:00:00:f4:52:14:03:00:28:05:81 the\n# ip address 192.168.0.61. The client id is derived from the prefix\n# ff:00:00:00:00:00:02:00:00:02:c9:00 and the last 8 pairs of\n# hex digits of the hardware address.\n#dhcp-host=id:ff:00:00:00:00:00:02:00:00:02:c9:00:f4:52:14:03:00:28:05:81,192.168.0.61\n\n# Always give the host with client identifier \"marjorie\"\n# the IP address 192.168.0.60\n#dhcp-host=id:marjorie,192.168.0.60\n\n# Enable the address given for \"judge\" in /etc/hosts\n# to be given to a machine presenting the name \"judge\" when\n# it asks for a DHCP lease.\n#dhcp-host=judge\n\n# Never offer DHCP service to a machine whose Ethernet\n# address is 11:22:33:44:55:66\n#dhcp-host=11:22:33:44:55:66,ignore\n\n# Ignore any client-id presented by the machine with Ethernet\n# address 11:22:33:44:55:66. This is useful to prevent a machine\n# being treated differently when running under different OS's or\n# between PXE boot and OS boot.\n#dhcp-host=11:22:33:44:55:66,id:*\n\n# Send extra options which are tagged as \"red\" to\n# the machine with Ethernet address 11:22:33:44:55:66\n#dhcp-host=11:22:33:44:55:66,set:red\n\n# Send extra options which are tagged as \"red\" to\n# any machine with Ethernet address starting 11:22:33:\n#dhcp-host=11:22:33:*:*:*,set:red\n\n# Give a fixed IPv6 address and name to client with\n# DUID 00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2\n# Note the MAC addresses CANNOT be used to identify DHCPv6 clients.\n# Note also that the [] around the IPv6 address are obligatory.\n#dhcp-host=id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1234::5]\n\n# Ignore any clients which are not specified in dhcp-host lines\n# or /etc/ethers. Equivalent to ISC \"deny unknown-clients\".\n# This relies on the special \"known\" tag which is set when\n# a host is matched.\n#dhcp-ignore=tag:!known\n\n# Send extra options which are tagged as \"red\" to any machine whose\n# DHCP vendorclass string includes the substring \"Linux\"\n#dhcp-vendorclass=set:red,Linux\n\n# Send extra options which are tagged as \"red\" to any machine one\n# of whose DHCP userclass strings includes the substring \"accounts\"\n#dhcp-userclass=set:red,accounts\n\n# Send extra options which are tagged as \"red\" to any machine whose\n# MAC address matches the pattern.\n#dhcp-mac=set:red,00:60:8C:*:*:*\n\n# If this line is uncommented, dnsmasq will read /etc/ethers and act\n# on the ethernet-address/IP pairs found there just as if they had\n# been given as --dhcp-host options. Useful if you keep\n# MAC-address/host mappings there for other purposes.\n#read-ethers\n\n# Send options to hosts which ask for a DHCP lease.\n# See RFC 2132 for details of available options.\n# Common options can be given to dnsmasq by name:\n# run \"dnsmasq --help dhcp\" to get a list.\n# Note that all the common settings, such as netmask and\n# broadcast address, DNS server and default route, are given\n# sane defaults by dnsmasq. You very likely will not need\n# any dhcp-options. If you use Windows clients and Samba, there\n# are some options which are recommended, they are detailed at the\n# end of this section.\n\n# Override the default route supplied by dnsmasq, which assumes the\n# router is the same machine as the one running dnsmasq.\n#dhcp-option=3,1.2.3.4\n\n# Do the same thing, but using the option name\n#dhcp-option=option:router,1.2.3.4\n\n# Override the default route supplied by dnsmasq and send no default\n# route at all. Note that this only works for the options sent by\n# default (1, 3, 6, 12, 28) the same line will send a zero-length option\n# for all other option numbers.\n#dhcp-option=3\n\n# Set the NTP time server addresses to 192.168.0.4 and 10.10.0.5\n#dhcp-option=option:ntp-server,192.168.0.4,10.10.0.5\n\n# Send DHCPv6 option. Note [] around IPv6 addresses.\n#dhcp-option=option6:dns-server,[1234::77],[1234::88]\n\n# Send DHCPv6 option for namservers as the machine running\n# dnsmasq and another.\n#dhcp-option=option6:dns-server,[::],[1234::88]\n\n# Ask client to poll for option changes every six hours. (RFC4242)\n#dhcp-option=option6:information-refresh-time,6h\n\n# Set option 58 client renewal time (T1). Defaults to half of the\n# lease time if not specified. (RFC2132)\n#dhcp-option=option:T1,1m\n\n# Set option 59 rebinding time (T2). Defaults to 7/8 of the\n# lease time if not specified. (RFC2132)\n#dhcp-option=option:T2,2m\n\n# Set the NTP time server address to be the same machine as\n# is running dnsmasq\n#dhcp-option=42,0.0.0.0\n\n# Set the NIS domain name to \"welly\"\n#dhcp-option=40,welly\n\n# Set the default time-to-live to 50\n#dhcp-option=23,50\n\n# Set the \"all subnets are local\" flag\n#dhcp-option=27,1\n\n# Send the etherboot magic flag and then etherboot options (a string).\n#dhcp-option=128,e4:45:74:68:00:00\n#dhcp-option=129,NIC=eepro100\n\n# Specify an option which will only be sent to the \"red\" network\n# (see dhcp-range for the declaration of the \"red\" network)\n# Note that the tag: part must precede the option: part.\n#dhcp-option = tag:red, option:ntp-server, 192.168.1.1\n\n# The following DHCP options set up dnsmasq in the same way as is specified\n# for the ISC dhcpcd in\n# http://www.samba.org/samba/ftp/docs/textdocs/DHCP-Server-Configuration.txt\n# adapted for a typical dnsmasq installation where the host running\n# dnsmasq is also the host running samba.\n# you may want to uncomment some or all of them if you use\n# Windows clients and Samba.\n#dhcp-option=19,0           # option ip-forwarding off\n#dhcp-option=44,0.0.0.0     # set netbios-over-TCP/IP nameserver(s) aka WINS server(s)\n#dhcp-option=45,0.0.0.0     # netbios datagram distribution server\n#dhcp-option=46,8           # netbios node type\n\n# Send an empty WPAD option. This may be REQUIRED to get windows 7 to behave.\n#dhcp-option=252,\"\\n\"\n\n# Send RFC-3397 DNS domain search DHCP option. WARNING: Your DHCP client\n# probably doesn't support this......\n#dhcp-option=option:domain-search,eng.apple.com,marketing.apple.com\n\n# Send RFC-3442 classless static routes (note the netmask encoding)\n#dhcp-option=121,192.168.1.0/24,1.2.3.4,10.0.0.0/8,5.6.7.8\n\n# Send vendor-class specific options encapsulated in DHCP option 43.\n# The meaning of the options is defined by the vendor-class so\n# options are sent only when the client supplied vendor class\n# matches the class given here. (A substring match is OK, so \"MSFT\"\n# matches \"MSFT\" and \"MSFT 5.0\"). This example sets the\n# mtftp address to 0.0.0.0 for PXEClients.\n#dhcp-option=vendor:PXEClient,1,0.0.0.0\n\n# Send microsoft-specific option to tell windows to release the DHCP lease\n# when it shuts down. Note the \"i\" flag, to tell dnsmasq to send the\n# value as a four-byte integer - that's what microsoft wants. See\n# http://technet2.microsoft.com/WindowsServer/en/library/a70f1bb7-d2d4-49f0-96d6-4b7414ecfaae1033.mspx?mfr=true\n#dhcp-option=vendor:MSFT,2,1i\n\n# Send the Encapsulated-vendor-class ID needed by some configurations of\n# Etherboot to allow is to recognise the DHCP server.\n#dhcp-option=vendor:Etherboot,60,\"Etherboot\"\n\n# Send options to PXELinux. Note that we need to send the options even\n# though they don't appear in the parameter request list, so we need\n# to use dhcp-option-force here.\n# See http://syslinux.zytor.com/pxe.php#special for details.\n# Magic number - needed before anything else is recognised\n#dhcp-option-force=208,f1:00:74:7e\n# Configuration file name\n#dhcp-option-force=209,configs/common\n# Path prefix\n#dhcp-option-force=210,/tftpboot/pxelinux/files/\n# Reboot time. (Note 'i' to send 32-bit value)\n#dhcp-option-force=211,30i\n\n# Set the boot filename for netboot/PXE. You will only need\n# this if you want to boot machines over the network and you will need\n# a TFTP server; either dnsmasq's built-in TFTP server or an\n# external one. (See below for how to enable the TFTP server.)\n#dhcp-boot=pxelinux.0\n\n# The same as above, but use custom tftp-server instead machine running dnsmasq\n#dhcp-boot=pxelinux,server.name,192.168.1.100\n\n# Boot for iPXE. The idea is to send two different\n# filenames, the first loads iPXE, and the second tells iPXE what to\n# load. The dhcp-match sets the ipxe tag for requests from iPXE.\n#dhcp-boot=undionly.kpxe\n#dhcp-match=set:ipxe,175 # iPXE sends a 175 option.\n#dhcp-boot=tag:ipxe,http://boot.ipxe.org/demo/boot.php\n\n# Encapsulated options for iPXE. All the options are\n# encapsulated within option 175\n#dhcp-option=encap:175, 1, 5b         # priority code\n#dhcp-option=encap:175, 176, 1b       # no-proxydhcp\n#dhcp-option=encap:175, 177, string   # bus-id\n#dhcp-option=encap:175, 189, 1b       # BIOS drive code\n#dhcp-option=encap:175, 190, user     # iSCSI username\n#dhcp-option=encap:175, 191, pass     # iSCSI password\n\n# Test for the architecture of a netboot client. PXE clients are\n# supposed to send their architecture as option 93. (See RFC 4578)\n#dhcp-match=peecees, option:client-arch, 0 #x86-32\n#dhcp-match=itanics, option:client-arch, 2 #IA64\n#dhcp-match=hammers, option:client-arch, 6 #x86-64\n#dhcp-match=mactels, option:client-arch, 7 #EFI x86-64\n\n# Do real PXE, rather than just booting a single file, this is an\n# alternative to dhcp-boot.\n#pxe-prompt=\"What system shall I netboot?\"\n# or with timeout before first available action is taken:\n#pxe-prompt=\"Press F8 for menu.\", 60\n\n# Available boot services. for PXE.\n#pxe-service=x86PC, \"Boot from local disk\"\n\n# Loads <tftp-root>/pxelinux.0 from dnsmasq TFTP server.\n#pxe-service=x86PC, \"Install Linux\", pxelinux\n\n# Loads <tftp-root>/pxelinux.0 from TFTP server at 1.2.3.4.\n# Beware this fails on old PXE ROMS.\n#pxe-service=x86PC, \"Install Linux\", pxelinux, 1.2.3.4\n\n# Use bootserver on network, found my multicast or broadcast.\n#pxe-service=x86PC, \"Install windows from RIS server\", 1\n\n# Use bootserver at a known IP address.\n#pxe-service=x86PC, \"Install windows from RIS server\", 1, 1.2.3.4\n\n# If you have multicast-FTP available,\n# information for that can be passed in a similar way using options 1\n# to 5. See page 19 of\n# http://download.intel.com/design/archives/wfm/downloads/pxespec.pdf\n\n\n# Enable dnsmasq's built-in TFTP server\n#enable-tftp\n\n# Set the root directory for files available via FTP.\n#tftp-root=/var/ftpd\n\n# Do not abort if the tftp-root is unavailable\n#tftp-no-fail\n\n# Make the TFTP server more secure: with this set, only files owned by\n# the user dnsmasq is running as will be send over the net.\n#tftp-secure\n\n# This option stops dnsmasq from negotiating a larger blocksize for TFTP\n# transfers. It will slow things down, but may rescue some broken TFTP\n# clients.\n#tftp-no-blocksize\n\n# Set the boot file name only when the \"red\" tag is set.\n#dhcp-boot=tag:red,pxelinux.red-net\n\n# An example of dhcp-boot with an external TFTP server: the name and IP\n# address of the server are given after the filename.\n# Can fail with old PXE ROMS. Overridden by --pxe-service.\n#dhcp-boot=/var/ftpd/pxelinux.0,boothost,192.168.0.3\n\n# If there are multiple external tftp servers having a same name\n# (using /etc/hosts) then that name can be specified as the\n# tftp_servername (the third option to dhcp-boot) and in that\n# case dnsmasq resolves this name and returns the resultant IP\n# addresses in round robin fashion. This facility can be used to\n# load balance the tftp load among a set of servers.\n#dhcp-boot=/var/ftpd/pxelinux.0,boothost,tftp_server_name\n\n# Set the limit on DHCP leases, the default is 150\n#dhcp-lease-max=150\n\n# The DHCP server needs somewhere on disk to keep its lease database.\n# This defaults to a sane location, but if you want to change it, use\n# the line below.\n#dhcp-leasefile=/var/lib/misc/dnsmasq.leases\n\n# Set the DHCP server to authoritative mode. In this mode it will barge in\n# and take over the lease for any client which broadcasts on the network,\n# whether it has a record of the lease or not. This avoids long timeouts\n# when a machine wakes up on a new network. DO NOT enable this if there's\n# the slightest chance that you might end up accidentally configuring a DHCP\n# server for your campus/company accidentally. The ISC server uses\n# the same option, and this URL provides more information:\n# http://www.isc.org/files/auth.html\n#dhcp-authoritative\n\n# Set the DHCP server to enable DHCPv4 Rapid Commit Option per RFC 4039.\n# In this mode it will respond to a DHCPDISCOVER message including a Rapid Commit\n# option with a DHCPACK including a Rapid Commit option and fully committed address\n# and configuration information. This must only be enabled if either the server is\n# the only server for the subnet, or multiple servers are present and they each\n# commit a binding for all clients.\n#dhcp-rapid-commit\n\n# Run an executable when a DHCP lease is created or destroyed.\n# The arguments sent to the script are \"add\" or \"del\",\n# then the MAC address, the IP address and finally the hostname\n# if there is one.\n#dhcp-script=/bin/echo\n\n# Set the cachesize here.\n#cache-size=150\n\n# If you want to disable negative caching, uncomment this.\n#no-negcache\n\n# Normally responses which come from /etc/hosts and the DHCP lease\n# file have Time-To-Live set as zero, which conventionally means\n# do not cache further. If you are happy to trade lower load on the\n# server for potentially stale date, you can set a time-to-live (in\n# seconds) here.\n#local-ttl=\n\n# If you want dnsmasq to detect attempts by Verisign to send queries\n# to unregistered .com and .net hosts to its sitefinder service and\n# have dnsmasq instead return the correct NXDOMAIN response, uncomment\n# this line. You can add similar lines to do the same for other\n# registries which have implemented wildcard A records.\n#bogus-nxdomain=64.94.110.11\n\n# If you want to fix up DNS results from upstream servers, use the\n# alias option. This only works for IPv4.\n# This alias makes a result of 1.2.3.4 appear as 5.6.7.8\n#alias=1.2.3.4,5.6.7.8\n# and this maps 1.2.3.x to 5.6.7.x\n#alias=1.2.3.0,5.6.7.0,255.255.255.0\n# and this maps 192.168.0.10->192.168.0.40 to 10.0.0.10->10.0.0.40\n#alias=192.168.0.10-192.168.0.40,10.0.0.0,255.255.255.0\n\n# Change these lines if you want dnsmasq to serve MX records.\n\n# Return an MX record named \"maildomain.com\" with target\n# servermachine.com and preference 50\n#mx-host=maildomain.com,servermachine.com,50\n\n# Set the default target for MX records created using the localmx option.\n#mx-target=servermachine.com\n\n# Return an MX record pointing to the mx-target for all local\n# machines.\n#localmx\n\n# Return an MX record pointing to itself for all local machines.\n#selfmx\n\n# Change the following lines if you want dnsmasq to serve SRV\n# records.  These are useful if you want to serve ldap requests for\n# Active Directory and other windows-originated DNS requests.\n# See RFC 2782.\n# You may add multiple srv-host lines.\n# The fields are <name>,<target>,<port>,<priority>,<weight>\n# If the domain part if missing from the name (so that is just has the\n# service and protocol sections) then the domain given by the domain=\n# config option is used. (Note that expand-hosts does not need to be\n# set for this to work.)\n\n# A SRV record sending LDAP for the example.com domain to\n# ldapserver.example.com port 389\n#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389\n\n# A SRV record sending LDAP for the example.com domain to\n# ldapserver.example.com port 389 (using domain=)\n#domain=example.com\n#srv-host=_ldap._tcp,ldapserver.example.com,389\n\n# Two SRV records for LDAP, each with different priorities\n#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389,1\n#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389,2\n\n# A SRV record indicating that there is no LDAP server for the domain\n# example.com\n#srv-host=_ldap._tcp.example.com\n\n# The following line shows how to make dnsmasq serve an arbitrary PTR\n# record. This is useful for DNS-SD. (Note that the\n# domain-name expansion done for SRV records _does_not\n# occur for PTR records.)\n#ptr-record=_http._tcp.dns-sd-services,\"New Employee Page._http._tcp.dns-sd-services\"\n\n# Change the following lines to enable dnsmasq to serve TXT records.\n# These are used for things like SPF and zeroconf. (Note that the\n# domain-name expansion done for SRV records _does_not\n# occur for TXT records.)\n\n#Example SPF.\n#txt-record=example.com,\"v=spf1 a -all\"\n\n#Example zeroconf\n#txt-record=_http._tcp.example.com,name=value,paper=A4\n\n# Provide an alias for a \"local\" DNS name. Note that this _only_ works\n# for targets which are names from DHCP or /etc/hosts. Give host\n# \"bert\" another name, bertrand\n#cname=bertand,bert\n\n# For debugging purposes, log each DNS query as it passes through\n# dnsmasq.\n#log-queries\n\n# Log lots of extra information about DHCP transactions.\n#log-dhcp\n\n# Include another lot of configuration options.\n#conf-file=/etc/dnsmasq.more.conf\n#conf-dir=/etc/dnsmasq.d\n\n# Include all the files in a directory except those ending in .bak\n#conf-dir=/etc/dnsmasq.d,.bak\n\n# Include all files in a directory which end in .conf\n#conf-dir=/etc/dnsmasq.d/,*.conf\n\n# If a DHCP client claims that its name is \"wpad\", ignore that.\n# This fixes a security hole. see CERT Vulnerability VU#598349\n#dhcp-name-match=set:wpad-ignore,wpad\n#dhcp-ignore-names=tag:wpad-ignore\n"
  },
  {
    "path": "dnsmasq/configs/ifupdown/default.conf",
    "content": "# Configuration for networking init script being run during\n# the boot sequence\n\n# Set to 'no' to skip interfaces configuration on boot\n#CONFIGURE_INTERFACES=yes\n\n# Don't configure these interfaces. Shell wildcards supported/\n#EXCLUDE_INTERFACES=\n\n# Set to 'yes' to enable additional verbosity\n#VERBOSE=no\n\n# Method to wait for the network to become online,\n# for services that depend on a working network:\n# - ifup: wait for ifup to have configured an interface.\n# - route: wait for a route to a given address to appear.\n# - ping/ping6: wait for a host to respond to ping packets.\n# - none: don't wait.\n#WAIT_ONLINE_METHOD=ifup\n\n# Which interface to wait for.\n# If none given, wait for all auto interfaces, or if there are none,\n# wait for at least one hotplug interface.\n#WAIT_ONLINE_IFACE=\n\n# Which address to wait for for route, ping and ping6 methods.\n# If none is given for route, it waits for a default gateway.\n#WAIT_ONLINE_ADDRESS=\n\n# Timeout in seconds for waiting for the network to come online.\nWAIT_ONLINE_TIMEOUT=30"
  },
  {
    "path": "dnsmasq/configs/ifupdown/networking-pre.sh",
    "content": "#!/bin/sh\n\nmkdir -p /run/network/\nrm -f /run/network/ifupdown.conf 2>/dev/null\n\n# check whether nmcli reports that network manager is managing the interface\n# ref: https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/main/src/libnmc-base/nm-client-utils.c?ref_type=heads#L288\nNM_IFACES=$(nmcli -t -m tabular device status | awk -F ':' '$3 !~ /unmanaged|[^ ]+ \\(externally\\)/ {print $1}')\n# check whether networkctl reports that systemd-networkd is manaing the interface\n# ref: https://github.com/systemd/systemd/blob/e603a438a7918a2fcc35d7683fd755d3837b7024/src/network/networkd-link.c#L2918\nSN_IFACES=$(networkctl list --json=short | jq -r -e '.Interfaces[] | select(.AdministrativeState!=\"unmanaged\").Name')\n# blacklist those managed interfaces from being managed by ifupdown\nEXCLUSIONS=''\nfor IFACE in $NM_IFACES $SN_IFACES; do\n    EXCLUSIONS=\"$EXCLUSIONS $IFACE ${IFACE}:*\"\ndone\n\n[ -n \"$EXCLUSIONS\" ] && echo \"EXCLUDE_INTERFACES='$EXCLUSIONS'\" >/run/network/ifupdown.conf\n\nexit 0"
  },
  {
    "path": "dnsmasq/configs/ifupdown/override.conf",
    "content": "[Unit]\nAfter=systemd-networkd.service\n\n[Service]\nEnvironmentFile=-/run/network/ifupdown.conf\nExecStartPre=/usr/lib/ifupdown/networking-pre\n#ExecStartPre=/bin/bash -c '[ ! -e /run/network/ifclean ] && { ifdown -f -a --read-environment -X lo; touch /run/network/restart-hotplug /run/network/ifclean; true; } || true'"
  },
  {
    "path": "dnsmasq/configs/networkmanager/dsiprouter.conf",
    "content": "# dSIPRouter does not utilize the NetworkManager dnsmasq plugin.\n# Instead DNSmasq is managed as a separate service that pulls DNS servers\n# from the upstream DNS provider (NetworkManager/dhclient/systemd-resolved).\n# Currently the only plugin utilized by dSIPRouter is the keyfile plugin.\n# The ifupdown plugin should not be loaded here as it is loaded via the\n# systemd service files with interfaces conditionally managed..\n# Note: the \"no-auto-default\" setting tells network manager not to automatically\n# create profiles for connected ethernet devices, only allowing explicit profiles.\n# Ref: https://people.freedesktop.org/~lkundrak/nm-docs/NetworkManager.conf.html\n[main]\ndns=none\nno-auto-default=*\nplugins=keyfile\n\n[keyfile]\nunmanaged-devices=interface-name:docker*"
  },
  {
    "path": "dnsmasq/configs/networkmanager/wait-override.conf",
    "content": "[Service]\nEnvironment=\nEnvironment=NM_ONLINE_TIMEOUT=30"
  },
  {
    "path": "dnsmasq/configs/resolv.conf",
    "content": "# DNS servers are being managed by dSIPRouter dynamically, DO NOT CHANGE THIS FILE\n#\n# The DNSmasq stub resolver will handle lookups in the following order:\n# 1. entries from /etc/hosts\n# 2. the system provided DNS servers (via DHCP)\n#\n# To change the upstream DNS servers DNSmasq forwards to update resolv-file in\n# /etc/dnsmasq.conf to your resolve file of choice.\n# For more information on configuring DNSmasq refer to dnsmasq(8) manpage.\n#\nnameserver 127.0.0.1\n\n#####DSIP_CONFIG_START\n#####DSIP_CONFIG_END"
  },
  {
    "path": "dnsmasq/configs/resolvconf_def",
    "content": "REPORT_ABSENT_SYMLINK=no\nTRUNCATE_NAMESERVER_LIST_AFTER_LOOPBACK_ADDRESS=yes"
  },
  {
    "path": "dnsmasq/configs/resolvconf_upd",
    "content": "#!/bin/sh\n#\n# Script to update the resolver list for dnsmasq\n#\n# N.B. Resolvconf may run us even if dnsmasq is not (yet) running.\n# If dnsmasq is installed then we go ahead and update the resolver list\n# in case dnsmasq is started later.\n#\n# Assumption: On entry, PWD contains the resolv.conf-type files.\n#\n# This is a modified version of the file from the dnsmasq package.\n#\n\nset -e\n\nRUN_DIR=\"/run/dnsmasq\"\nRSLVRLIST_FILE=\"${RUN_DIR}/resolv.conf\"\nTMP_FILE=\"${RSLVRLIST_FILE}_new.$$\"\nMY_NAME_FOR_RESOLVCONF=\"dnsmasq\"\nRESOLVCONF_GEN_FILE=\"/run/resolvconf/resolv.conf\"\n\n[ -x /usr/sbin/dnsmasq ] || exit 0\n[ -x /lib/resolvconf/list-records ] || exit 1\n\nPATH=/bin:/sbin\n\nreport_err() { echo \"$0: Error: $*\" >&2 ; }\n\n# Stores arguments (minus duplicates) in RSLT, separated by spaces\n# Doesn't work properly if an argument itself contains whitespace\nuniquify() {\n\tRSLT=\"\"\n\twhile [ \"$1\" ] ; do\n\t\tfor E in $RSLT ; do\n\t\t\t[ \"$1\" = \"$E\" ] && { shift ; continue 2 ; }\n\t\tdone\n\t\tRSLT=\"${RSLT:+$RSLT }$1\"\n\t\tshift\n\tdone\n}\n\nfilterdnsmasq() {\n    while read ADDR; do\n        for DNSMASQ_ADDR in $@; do\n            [ \"x$ADDR\" = \"x$DNSMASQ_ADDR\" ] && continue 2\n        done\n        echo \"$ADDR\"\n    done\n}\n\nif [ ! -d \"$RUN_DIR\" ] && ! mkdir --parents --mode=0755 \"$RUN_DIR\" ; then\n\treport_err \"Failed trying to create directory $RUN_DIR\"\n\texit 1\nfi\n\nRSLVCNFFILES=\"$RESOLVCONF_GEN_FILE\"\nfor F in $(/lib/resolvconf/list-records) ; do\n\tcase \"$F\" in\n\t    \"lo.$MY_NAME_FOR_RESOLVCONF\")\n\t\tDNSMASQ_ADDRS=\"$(sed -n -e 's/^[[:space:]]*nameserver[[:space:]]\\+//p' lo.$MY_NAME_FOR_RESOLVCONF)\"\n\t\t;;\n\t  *)\n\t\tRSLVCNFFILES=\"${RSLVCNFFILES:+$RSLVCNFFILES }$F\"\n\t\t;;\n\tesac\ndone\n\nNMSRVRS=\"\"\nif [ \"$RSLVCNFFILES\" ] ; then\n\tuniquify $(\n\t    sed -n -e 's/^[[:space:]]*nameserver[[:space:]]\\+//p' $RSLVCNFFILES |\n\t    filterdnsmasq $DNSMASQ_ADDRS\n    )\n\tNMSRVRS=\"$RSLT\"\nfi\n\n# Dnsmasq uses the mtime of $RSLVRLIST_FILE, with a resolution of one second,\n# to detect changes in the file. This means that if a resolvconf update occurs\n# within one second of the previous one then dnsmasq may fail to notice the\n# more recent change. To work around this problem we sleep one second here\n# if necessary in order to ensure that the new mtime is different.\nif [ -f \"$RSLVRLIST_FILE\" ] && [ \"$(ls -go --time-style='+%s' \"$RSLVRLIST_FILE\" | { read p h s t n ; echo \"$t\" ; })\" = \"$(date +%s)\" ] ; then\n\tsleep 1\nfi\n\nclean_up() { rm -f \"$TMP_FILE\" ; }\ntrap clean_up EXIT\n: >| \"$TMP_FILE\"\nfor N in $NMSRVRS ; do echo \"nameserver $N\" >> \"$TMP_FILE\" ; done\nmv -f \"$TMP_FILE\" \"$RSLVRLIST_FILE\"\n"
  },
  {
    "path": "dnsmasq/configs/systemdnetworkd/docker.network",
    "content": "[Match]\nName=docker*\n\n[Link]\nUnmanaged=yes"
  },
  {
    "path": "dnsmasq/configs/systemdnetworkd/dsiprouter.network",
    "content": "[Match]\nName=*\n\n[Network]\nDHCP=yes\n\n[DHCP]\nUseDNS=true"
  },
  {
    "path": "dnsmasq/configs/systemdnetworkd/networkd-pre.sh",
    "content": "#!/bin/sh\n\nmkdir -p /run/systemd/network/\nrm -f /run/systemd/network/00-nm_managed-*.network 2>/dev/null\n\n# check whether nmcli reports that network manager is managing the interface\n# ref: https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/main/src/libnmc-base/nm-client-utils.c?ref_type=heads#L288\n# blacklist those managed interfaces from being managed by systemd-networkd\nfor IFACE in $(nmcli -t -m tabular device status | awk -F ':' '$3 !~ /unmanaged|[^ ]+ \\(externally\\)/ {print $1}'); do\n    cat <<EOF >\"/run/systemd/network/00-nm_managed-$IFACE.network\"\n[Match]\nName=$IFACE\n\n[Link]\nUnmanaged=yes\nEOF\ndone\n\nexit 0"
  },
  {
    "path": "dnsmasq/configs/systemdnetworkd/override.conf",
    "content": "[Unit]\nAfter=NetworkManager.service\n\n[Service]\nExecStartPre=!/usr/lib/systemd/networkd-pre"
  },
  {
    "path": "dnsmasq/configs/systemdnetworkd/wait-override.conf",
    "content": "[Service]\nExecStart=/lib/systemd/systemd-networkd-wait-online --timeout=30"
  },
  {
    "path": "dnsmasq/configs/systemdresolved/dsiprouter.conf",
    "content": "[Resolve]\nMulticastDNS=no\nLLMNR=no\nCache=no\nCacheFromLocalhost=no\nDNSStubListener=no\nReadEtcHosts=no"
  },
  {
    "path": "dnsmasq/debian/12.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    if ! systemctl is-active -q systemd-resolved; then\n        printerr 'systemd-resolved is required for this deployment'\n        echo 'install systemd-resolved and then retry installation'\n        return 1\n    fi\n\n    apt-get install -y dnsmasq\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing new dns stack'\n        return 1\n    fi\n\n    # configure init.d daemon\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/init.d/dnsmasq /etc/init.d/dnsmasq\n    chmod 755 /etc/init.d/dnsmasq\n    touch /usr/share/dnsmasq/installed-marker\n\n    # configure dnsmasq systemd service\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/systemd/dnsmasq-v1.service /lib/systemd/system/dnsmasq.service\n    chmod 644 /lib/systemd/system/dnsmasq.service\n    systemctl daemon-reload\n    systemctl enable dnsmasq\n\n    # backup the original resolv.conf\n    [[ ! -e \"${BACKUPS_DIR}/etc/resolv.conf\" ]] && {\n        mkdir -p ${BACKUPS_DIR}/etc/\n        cp -df /etc/resolv.conf ${BACKUPS_DIR}/etc/resolv.conf\n    }\n\n    # make dnsmasq the DNS provider\n    rm -f /etc/resolv.conf\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/resolv.conf /etc/resolv.conf\n\n    # for some reason the defaults on systemd-networkd are not followed after changing the above\n    # so we give the interfaces explicit rules to make sure DNS servers are resolved via DHCP on the ifaces\n    # see systemd.network and systemd.networkd for more information\n    mkdir -p /etc/systemd/network/\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/systemd.network /etc/systemd/network/99-dsiprouter.network\n\n    # restart systemd network services\n    systemctl restart systemd-networkd \n    if (( $? != 0 )); then\n        printerr 'failed loading new systemd network configurations..'\n        printwarn 'reverting network changes'\n        cp -df ${BACKUPS_DIR}/etc/resolv.conf /etc/resolv.conf\n        rm -f /etc/systemd/network/99-dsiprouter.network\n        systemctl restart systemd-networkd\n        return 1\n    fi\n    \n    # tell dnsmasq where to find upstream DNS servers\n    # we only need the dhcp dynamic dns servers feature of systemd-resolved, everything else is turned off\n    mkdir -p /etc/systemd/resolved.conf.d/\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/systemdresolved/dsiprouter.conf /etc/systemd/resolved.conf.d/dsiprouter.conf\n    systemctl restart systemd-resolved\n    # tell dnsmasq to grab dns servers from systemd-resolved\n    export DNSMASQ_RESOLV_FILE=\"/run/systemd/resolve/resolv.conf\"\n\n    envsubst <${DSIP_PROJECT_DIR}/dnsmasq/configs/dnsmasq_sh.conf >/etc/dnsmasq.conf\n\n    return 0\n}\n\nfunction uninstall() {\n    # stop and disable services\n    systemctl disable dnsmasq\n    systemctl stop dnsmasq\n\n    # uninstall packages\n    apt-get remove -y --purge dnsmasq\n\n    # remove our systemd-resolved configurations\n    rm -f /etc/systemd/resolved.conf.d/99-dsiprouter.conf\n\n    # remove the systemd.network rules\n    rm -f /etc/systemd/network/99-dsiprouter.network\n\n    # restore original resolv.conf\n    cp -df ${BACKUPS_DIR}/etc/resolv.conf /etc/resolv.conf\n\n    # restart systemd.networkd with the original rules\n    systemctl restart systemd-networkd\n\n    # update resolv.conf / restart systemd-resolved with new configs\n    systemctl restart systemd-resolved\n\n    # cleanup backup files\n    rm -f ${BACKUPS_DIR}/etc/resolv.conf\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dnsmasq/debian/install.sh",
    "content": "#!/usr/bin/env bash\n#\n# dSIPRouter DNS Resolution - How it Works\n#\n# Many of the cluster features require a multiple IP addresses to be associated with a local hostname.\n# To make these checks performant and not rely on external DNS, a local stub resolver supporting\n# multiple IPs per host is required (dnsmasq). This is equivalent to having multiple A / AAAA records\n# on an external DNS server. The difference here is that the entries are read locally from /etc/hosts.\n# A hostname is first checked locally, and before trying to resolve via the external DNS servers.\n#\n# By default DNS resolution via other applications is bypassed. DNSMasq is therefore the primary DNS\n# resolver for the entire system (even for glibc).\n# Upstream DNS resolvers (external, other stub resolvers, etc..) are attempted only after the DNSMasq\n# stub resolver checks local records from /etc/hosts.\n#\n# References:\n# dnsmasq(8)        https://manpages.debian.org/stable/dnsmasq-base/dnsmasq.8.en.html\n# resolv.conf(5)    https://man7.org/linux/man-pages/man5/resolv.conf.5.html\n# hosts(5)          https://man7.org/linux/man-pages/man5/hosts.5.html\n#\n# dSIPRouter Network Configuration - How it Works\n#\n# To support a variety of deployment environments the network stack is strictly configured on install.\n# The goal is to make builds as reproducible as possible in any environment, without concern for the\n# OS provider's (downstream, VM image, etc..) chosen network stack.\n# Therefore, to customize your network configuration, make sure your network configurations operate on\n# the supported network applications outlined here.\n#\n# Here is a summary of how the installed network stack works on debian-based OS:\n# 1. ignore cloud-init network configurations\n# 2. try configuring the network via network-manager\n# 3. try configuring the network via systemd-networkd\n# 4. try configuring the network via ifupdown\n#\n# By default network-manager / systemd-networkd will try to assign IPs based on DHCP.\n# By default ifupdown is left unaltered.\n#\n# References:\n# cloud-init(1)             https://manpages.debian.org/stable/cloud-init/cloud-init.1.en.html\n# systemd-networkd(8)       https://man7.org/linux/man-pages/man8/systemd-networkd.service.8.html\n# networkd-dispatcher(8)    https://manpages.debian.org/stable/networkd-dispatcher/networkd-dispatcher.8.en.html\n# interfaces(5)             https://manpages.debian.org/stable/ifupdown/interfaces.5.en.html\n#\n# TODO: Currently this is implemented with systemd service drop-ins but this is very hacky and does not allow\n#       fine grain control over timing and service status / network availability checks throughout the startup\n#       ordering chain.\n#       The preferred method we will implement in the future, will be using network-manager to load the rest of\n#       the possible network management services.\n#       I.E. instead of trying network-manager then waiting for it to timeout and trying systemd-networkd, the\n#       network manager would try each plugin (plugins=keyfile,networkd,ifupdown) in order, until one is successful.\n#       The network manager project currently only supports keyfile.ifupdown above. networkd support needs implemented.\n#       Other plugins can be used as an example for the new implementation:\n#       https://github.com/NetworkManager/NetworkManager/tree/main/src/core/settings/plugins\n#\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # backup the configuration files we will replace\n    [[ ! -e \"${BACKUPS_DIR}/network/\" ]] && {\n        mkdir -p ${BACKUPS_DIR}/network/\n        cp -df /etc/resolv.conf ${BACKUPS_DIR}/network/resolv.conf\n        cp -df /etc/default/networking ${BACKUPS_DIR}/network/networking\n    }\n\n    # make sure the dns stack is installed (minimal images do not include these packages)\n    # debian used resolvconf up to debian12 when they switch to systemd-resolved\n    if (( $DISTRO_VER < 12 )); then\n        apt-get purge -y systemd-resolved libnss-resolve\n        apt-get install -y resolvconf ifupdown network-manager\n\n        resolvconf -u\n    else\n        apt-get purge -y resolvconf\n        apt-get install -y systemd-resolved libnss-resolve ifupdown network-manager\n\n        # we only need the dhcp dynamic dns servers feature of systemd-resolved, everything else is turned off\n        mkdir -p /etc/systemd/resolved.conf.d/\n        cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/systemdresolved/dsiprouter.conf /etc/systemd/resolved.conf.d/99-dsiprouter.conf\n\n        systemctl restart systemd-resolved\n    fi\n\n    # we give the interfaces explicit rules to make sure DNS servers are resolved via DHCP on the interfaces\n    # docker interfaces are managed by docker services so we also make sure systemd-networkd does not manage those\n    # see systemd.network and systemd.networkd for more information\n    mkdir -p /etc/systemd/network/\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/systemdnetworkd/docker.network /etc/systemd/network/98-docker.network\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/systemdnetworkd/dsiprouter.network /etc/systemd/network/99-dsiprouter.network\n\n    # configure NetworkManager\n    mkdir -p /etc/NetworkManager/conf.d/\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/networkmanager/dsiprouter.conf /etc/NetworkManager/conf.d/99-dsiprouter.conf\n\n    # systemd-networkd and networking service customizations\n    mkdir -p /etc/systemd/system/systemd-networkd.service.d/\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/systemdnetworkd/override.conf /etc/systemd/system/systemd-networkd.service.d/00-dsiprouter.conf\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/systemdnetworkd/networkd-pre.sh /usr/lib/systemd/networkd-pre\n    chmod +x /usr/lib/systemd/networkd-pre\n    mkdir -p /etc/systemd/system/networking.service.d/\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/ifupdown/override.conf /etc/systemd/system/networking.service.d/00-dsiprouter.conf\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/ifupdown/networking-pre.sh /usr/lib/ifupdown/networking-pre\n    chmod +x /usr/lib/ifupdown/networking-pre\n\n    # adjusting the service timeouts \"online\"\n    mkdir -p /etc/systemd/system/systemd-networkd-wait-online.service.d/\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/systemdnetworkd/wait-override.conf /etc/systemd/system/systemd-networkd-wait-online.service.d/00-dsiprouter.conf\n    mkdir -p /etc/systemd/system/NetworkManager-wait-online.service.d/\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/networkmanager/wait-override.conf /etc/systemd/system/NetworkManager-wait-online.service.d/00-dsiprouter.conf\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/ifupdown/default.conf /etc/default/networking\n\n    systemctl daemon-reload\n    systemctl enable NetworkManager\n    systemctl enable systemd-networkd\n    systemctl enable networking\n\n    # restart network services. if we fail, revert and exit.\n    # TODO: we can not ensure the network stack is properly reverted without tracking the original set of packages and reverting them as well\n    systemctl restart NetworkManager &&\n    systemctl restart systemd-networkd &&\n    systemctl restart networking || {\n        printerr 'failed loading updated network configurations..'\n        printwarn 'reverting network changes and aborting dnsmasq install'\n        cp -df ${BACKUPS_DIR}/etc/resolv.conf /etc/resolv.conf\n        cp -df ${BACKUPS_DIR}/network/networking /etc/default/networking\n        rm -f /etc/systemd/resolved.conf.d/99-dsiprouter.conf\n        rm -f /etc/systemd/network/98-docker.network\n        rm -f /etc/systemd/network/99-dsiprouter.network\n        rm -f /etc/NetworkManager/conf.d/99-dsiprouter.conf\n        rm -f /etc/systemd/system/systemd-networkd.service.d/00-dsiprouter.conf\n        rm -f /etc/systemd/system/networking.service.d/00-dsiprouter.conf\n        rm -f /etc/systemd/system/NetworkManager-wait-online.service.d/00-dsiprouter.conf\n        rm -f /usr/lib/systemd/networkd-pre\n        rm -f /usr/lib/ifupdown/networking-pre\n        systemctl daemon-reload\n        systemctl revert NetworkManager\n        systemctl revert systemd-networkd\n        systemctl revert networking\n        systemctl restart NetworkManager || systemctl restart systemd-networkd || systemctl restart networking\n        return 1\n    }\n\n    # mask the service before running package manager to avoid faulty startup errors\n    systemctl mask dnsmasq.service\n\n    apt-get install -y dnsmasq\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing new dns stack'\n        return 1\n    fi\n\n    # make sure we unmask before configuring the service ourselves\n    systemctl unmask dnsmasq.service\n\n    # configure dnsmasq systemd service\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/systemd/dnsmasq-v1.service /lib/systemd/system/dnsmasq.service\n    chmod 644 /lib/systemd/system/dnsmasq.service\n    systemctl daemon-reload\n    systemctl enable dnsmasq\n\n    # make dnsmasq the DNS provider\n    rm -f /etc/resolv.conf\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/resolv.conf /etc/resolv.conf\n\n    # update the dnsmasq settings\n    if (( $DISTRO_VER < 12 )); then\n        export DNSMASQ_RESOLV_FILE=\"/run/dnsmasq/resolv.conf\"\n\n        # setup resolvconf to work with dnsmasq\n        cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/resolvconf_def /etc/default/resolvconf &&\n        rm -f /etc/resolvconf/update.d/dnsmasq &&\n        cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/resolvconf_upd /etc/resolvconf/update.d/dnsmasq &&\n        chmod +x /etc/resolvconf/update.d/dnsmasq &&\n        resolvconf -u || {\n            printerr 'failed loading new resolvconf network configurations..'\n        }\n    else\n        export DNSMASQ_RESOLV_FILE=\"/run/systemd/resolve/resolv.conf\"\n    fi\n\n    # tell dnsmasq to grab dns servers found via dhcp\n    envsubst <${DSIP_PROJECT_DIR}/dnsmasq/configs/dnsmasq_sh.conf >/etc/dnsmasq.conf\n\n    return 0\n}\n\nfunction uninstall() {\n    # stop and disable services\n    systemctl disable dnsmasq\n    systemctl stop dnsmasq\n\n    # uninstall packages\n    apt-get remove -y --purge dnsmasq\n\n    # remove network manager config\n    rm -f /etc/NetworkManager/conf.d/99-dsiprouter.conf\n\n    # remove our systemd-resolved configurations\n    rm -f /etc/systemd/resolved.conf.d/99-dsiprouter.conf\n\n    # remove the systemd.network rules\n    rm -f /etc/systemd/network/99-dsiprouter.network\n\n    # restore original resolv.conf\n    cp -df ${BACKUPS_DIR}/etc/resolv.conf /etc/resolv.conf\n\n    # restart related services\n    if (( $DISTRO_VER < 12 )); then\n        resolvconf -u\n    else\n        systemctl restart systemd-networkd\n        systemctl restart systemd-resolved\n    fi\n    if systemctl is-active -q NetworkManager &>/dev/null; then\n        systemctl restart NetworkManager\n    fi\n\n    # cleanup backup files\n    rm -f ${BACKUPS_DIR}/etc/resolv.conf\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dnsmasq/init.d/dnsmasq",
    "content": "#!/bin/sh\n### BEGIN INIT INFO\n# Provides:       dnsmasq\n# Required-Start: $network $remote_fs $syslog\n# Required-Stop:  $network $remote_fs $syslog\n# Default-Start:  2 3 4 5\n# Default-Stop:   0 1 6\n# Description:    DHCP and DNS server\n### END INIT INFO\n\n# Don't exit on error status\nset +e\n\nPATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\nDAEMON=/usr/sbin/dnsmasq\nNAME=dnsmasq\nDESC=\"DNS forwarder and DHCP server\"\nINSTANCE=\"${2}\"\n\n# Most configuration options in /etc/default/dnsmasq are deprecated\n# but still honoured.\nENABLED=1\nif [ -r /etc/default/${NAME}${INSTANCE:+.${INSTANCE}} ]; then\n    . /etc/default/${NAME}${INSTANCE:+.${INSTANCE}}\nfi\n\n# Get the system locale, so that messages are in the correct language, and the\n# charset for IDN is correct\nif [ -r /etc/default/locale ]; then\n    . /etc/default/locale\n    export LANG\nfi\n\n# The following test ensures the dnsmasq service is not started, when the\n# package 'dnsmasq' is removed but not purged, even if the dnsmasq-base\n# package is still in place.\ntest -e /usr/share/dnsmasq/installed-marker || exit 0\n\ntest -x ${DAEMON} || exit 0\n\n# Provide skeleton LSB log functions for backports which don't have LSB functions.\nif [ -f /lib/lsb/init-functions ]; then\n    . /lib/lsb/init-functions\nelse\n    log_warning_msg () {\n        echo \"${@}.\"\n    }\n\n    log_success_msg () {\n        echo \"${@}.\"\n    }\n\n    log_daemon_msg () {\n        echo -n \"${1}: ${2}\"\n    }\n\n    log_end_msg () {\n        if [ \"${1}\" -eq 0 ]; then\n            echo \".\"\n        elif [ \"${1}\" -eq 255 ]; then\n            /bin/echo -e \" (warning).\"\n        else\n            /bin/echo -e \" failed!\"\n        fi\n    }\nfi\n\n# RESOLV_CONF:\n# If the resolvconf package is installed then use the resolv conf file\n# that it provides as the default.  Otherwise use /etc/resolv.conf as\n# the default.\n#\n# If IGNORE_RESOLVCONF is set in /etc/default/dnsmasq or an explicit\n# filename is set there then this inhibits the use of the resolvconf-provided\n# information.\n#\n# Note that if the resolvconf package is installed it is not possible to\n# override it just by configuration in /etc/dnsmasq.conf, it is necessary\n# to set IGNORE_RESOLVCONF=yes in /etc/default/dnsmasq.\n\nif [ ! \"${RESOLV_CONF}\" ] &&\n   [ \"${IGNORE_RESOLVCONF}\" != \"yes\" ] &&\n   [ -x /sbin/resolvconf ]\nthen\n    RESOLV_CONF=/run/dnsmasq/resolv.conf\nfi\n\nfor INTERFACE in ${DNSMASQ_INTERFACE}; do\n    DNSMASQ_INTERFACES=\"${DNSMASQ_INTERFACES} -i ${INTERFACE}\"\ndone\n\nfor INTERFACE in ${DNSMASQ_EXCEPT}; do\n    DNSMASQ_INTERFACES=\"${DNSMASQ_INTERFACES} -I ${INTERFACE}\"\ndone\n\nif [ ! \"${DNSMASQ_USER}\" ]; then\n   DNSMASQ_USER=\"dnsmasq\"\nfi\n\n# This tells dnsmasq to ignore DNS requests that don't come from a local network.\n# It's automatically ignored if --interface --except-interface, --listen-address\n# or --auth-server exist in the configuration, so for most installations, it will\n# have no effect, but for otherwise-unconfigured installations, it stops dnsmasq\n# from being vulnerable to DNS-reflection attacks.\n\nDNSMASQ_OPTS=\"${DNSMASQ_OPTS} --local-service\"\n\n# If the dns-root-data package is installed, then the trust anchors will be\n# available in ROOT_DS, in BIND zone-file format. Reformat as dnsmasq\n# --trust-anchor options.\n\nROOT_DS=\"/usr/share/dns/root.ds\"\n\nif [ -f ${ROOT_DS} ]; then\n    DNSMASQ_OPTS=\"$DNSMASQ_OPTS `env LC_ALL=C sed -rne \"s/^([.a-zA-Z0-9]+)([[:space:]]+[0-9]+)*([[:space:]]+IN)*[[:space:]]+DS[[:space:]]+/--trust-anchor=\\1,/;s/[[:space:]]+/,/gp\" $ROOT_DS | tr '\\n' ' '`\"\nfi\n\nstart()\n{\n    # Return\n    #   0 if daemon has been started\n    #   1 if daemon was already running\n    #   2 if daemon could not be started\n\n    # /run may be volatile, so we need to ensure that\n    # /run/dnsmasq exists here as well as in postinst\n    if [ ! -d /run/dnsmasq ]; then\n        mkdir /run/dnsmasq || { [ -d /run/dnsmasq ] || return 2 ; }\n        chown dnsmasq:nogroup /run/dnsmasq || return 2\n    fi\n    [ -x /sbin/restorecon ] && /sbin/restorecon /run/dnsmasq\n\n    start-stop-daemon --start --quiet --pidfile /run/dnsmasq/${NAME}${INSTANCE:+.${INSTANCE}}.pid --exec ${DAEMON} --test > /dev/null || return 1\n    start-stop-daemon --start --quiet --pidfile /run/dnsmasq/${NAME}${INSTANCE:+.${INSTANCE}}.pid --exec ${DAEMON} -- \\\n        -x /run/dnsmasq/${NAME}${INSTANCE:+.${INSTANCE}}.pid \\\n        ${MAILHOSTNAME:+ -m ${MAILHOSTNAME}} \\\n        ${MAILTARGET:+ -t ${MAILTARGET}} \\\n        ${DNSMASQ_USER:+ -u ${DNSMASQ_USER}} \\\n        ${DNSMASQ_INTERFACES:+ ${DNSMASQ_INTERFACES}} \\\n        ${DHCP_LEASE:+ -l ${DHCP_LEASE}} \\\n        ${DOMAIN_SUFFIX:+ -s ${DOMAIN_SUFFIX}} \\\n        ${RESOLV_CONF:+ -r ${RESOLV_CONF}} \\\n        ${CACHESIZE:+ -c ${CACHESIZE}} \\\n        ${CONFIG_DIR:+ -7 ${CONFIG_DIR}} \\\n        ${DNSMASQ_OPTS:+ ${DNSMASQ_OPTS}} \\\n        || return 2\n}\n\nstart_resolvconf()\n{\n# If interface \"lo\" is explicitly disabled in /etc/default/dnsmasq\n# Then dnsmasq won't be providing local DNS, so don't add it to\n# the resolvconf server set.\n    for interface in ${DNSMASQ_EXCEPT}; do\n        [ ${interface} = lo ] && return\n    done\n\n    # Also skip this if DNS functionality is disabled in /etc/dnsmasq.conf\n    if grep -qs '^port=0' /etc/dnsmasq.conf; then\n        return\n    fi\n\n    if [ -x /sbin/resolvconf ] ; then\n        echo \"nameserver 127.0.0.1\" | /sbin/resolvconf -a lo.${NAME}${INSTANCE:+.${INSTANCE}}\n    fi\n    return 0\n}\n\nstop()\n{\n    # Return\n    #   0 if daemon has been stopped\n    #   1 if daemon was already stopped\n    #   2 if daemon could not be stopped\n    #   other if a failure occurred\n    start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile /run/dnsmasq/${NAME}${INSTANCE:+.${INSTANCE}}.pid --name ${NAME}\n}\n\nstop_resolvconf()\n{\n    if [ -x /sbin/resolvconf ] ; then\n        /sbin/resolvconf -d lo.${NAME}${INSTANCE:+.${INSTANCE}}\n    fi\n    return 0\n}\n\nstatus()\n{\n    # Return\n    #   0 if daemon is running\n    #   1 if daemon is dead and pid file exists\n    #   3 if daemon is not running\n    #   4 if daemon status is unknown\n    start-stop-daemon --start --quiet --pidfile /run/dnsmasq/${NAME}${INSTANCE:+.${INSTANCE}}.pid --exec ${DAEMON} --test > /dev/null\n    case \"${?}\" in\n      0) [ -e \"/run/dnsmasq/${NAME}${INSTANCE:+.${INSTANCE}}.pid\" ] && return 1 ; return 3 ;;\n      1) return 0 ;;\n      *) return 4 ;;\n    esac\n}\n\ncase \"${1}\" in\n  start)\n    test \"${ENABLED}\" != \"0\" || exit 0\n    log_daemon_msg \"Starting ${DESC}\" \"${NAME}${INSTANCE:+.${INSTANCE}}\"\n    start\n    case \"${?}\" in\n      0)\n        log_end_msg 0\n        start_resolvconf\n        exit 0\n        ;;\n      1)\n        log_success_msg \"(already running)\"\n        exit 0\n        ;;\n      *)\n        log_end_msg 1\n        exit 1\n        ;;\n    esac\n    ;;\n  stop)\n    stop_resolvconf\n    if [ \"${ENABLED}\" != \"0\" ]; then\n        log_daemon_msg \"Stopping ${DESC}\" \"${NAME}${INSTANCE:+.${INSTANCE}}\"\n    fi\n    stop\n    RETVAL=\"${?}\"\n    if [ \"${ENABLED}\" = \"0\" ]; then\n        case \"${RETVAL}\" in\n          0) log_daemon_msg \"Stopping ${DESC}\" \"${NAME}${INSTANCE:+.${INSTANCE}}\"; log_end_msg 0 ;;\n        esac\n        exit 0\n    fi\n    case \"${RETVAL}\" in\n      0) log_end_msg 0 ; exit 0 ;;\n      1) log_warning_msg \"(not running)\" ; exit 0 ;;\n      *) log_end_msg 1; exit 1 ;;\n    esac\n    ;;\n  checkconfig)\n    ${DAEMON} --test ${CONFIG_DIR:+ -7 ${CONFIG_DIR}} ${DNSMASQ_OPTS:+ ${DNSMASQ_OPTS}} >/dev/null 2>&1\n    RETVAL=\"${?}\"\n    exit ${RETVAL}\n    ;;\n  restart|force-reload)\n    test \"${ENABLED}\" != \"0\" || exit 1\n    ${DAEMON} --test ${CONFIG_DIR:+ -7 ${CONFIG_DIR}} ${DNSMASQ_OPTS:+ ${DNSMASQ_OPTS}} >/dev/null 2>&1\n    if [ ${?} -ne 0 ]; then\n        NAME=\"configuration syntax check\"\n        RETVAL=\"2\"\n    else\n        stop_resolvconf\n        stop\n        RETVAL=\"${?}\"\n    fi\n    log_daemon_msg \"Restarting ${DESC}\" \"${NAME}${INSTANCE:+.${INSTANCE}}\"\n    case \"${RETVAL}\" in\n      0|1)\n        sleep 2\n        start\n        case \"${?}\" in\n          0)\n            log_end_msg 0\n            start_resolvconf\n            exit 0\n            ;;\n          *)\n            log_end_msg 1\n            exit 1\n            ;;\n        esac\n        ;;\n      *)\n        log_end_msg 1\n        exit 1\n        ;;\n    esac\n    ;;\n  status)\n    log_daemon_msg \"Checking ${DESC}\" \"${NAME}${INSTANCE:+.${INSTANCE}}\"\n    status\n    case \"${?}\" in\n      0) log_success_msg \"(running)\" ; exit 0 ;;\n      1) log_success_msg \"(dead, pid file exists)\" ; exit 1 ;;\n      3) log_success_msg \"(not running)\" ; exit 3 ;;\n      *) log_success_msg \"(unknown)\" ; exit 4 ;;\n    esac\n    ;;\n  dump-stats)\n    kill -s USR1 `cat /run/dnsmasq/${NAME}${INSTANCE:+.${INSTANCE}}.pid`\n    ;;\n  systemd-start-resolvconf)\n    start_resolvconf\n    ;;\n  systemd-stop-resolvconf)\n    stop_resolvconf\n    ;;\n  systemd-exec)\n    # /run may be volatile, so we need to ensure that\n    # /run/dnsmasq exists here as well as in postinst\n    if [ ! -d /run/dnsmasq ]; then\n        mkdir /run/dnsmasq || { [ -d /run/dnsmasq ] || return 2 ; }\n        chown dnsmasq:nogroup /run/dnsmasq || return 2\n    fi\n    exec ${DAEMON} -x /run/dnsmasq/${NAME}${INSTANCE:+.${INSTANCE}}.pid \\\n        ${MAILHOSTNAME:+ -m ${MAILHOSTNAME}} \\\n        ${MAILTARGET:+ -t ${MAILTARGET}} \\\n        ${DNSMASQ_USER:+ -u ${DNSMASQ_USER}} \\\n        ${DNSMASQ_INTERFACES:+ ${DNSMASQ_INTERFACES}} \\\n        ${DHCP_LEASE:+ -l ${DHCP_LEASE}} \\\n        ${DOMAIN_SUFFIX:+ -s ${DOMAIN_SUFFIX}} \\\n        ${RESOLV_CONF:+ -r ${RESOLV_CONF}} \\\n        ${CACHESIZE:+ -c ${CACHESIZE}} \\\n        ${CONFIG_DIR:+ -7 ${CONFIG_DIR}} \\\n        ${DNSMASQ_OPTS:+ ${DNSMASQ_OPTS}}\n    ;;\n  *)\n    echo \"Usage: /etc/init.d/${NAME} {start|stop|restart|force-reload|dump-stats|status}\" >&2\n    exit 3\n    ;;\nesac\n\nexit 0\n"
  },
  {
    "path": "dnsmasq/rhel/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # mask the service before running package manager to avoid faulty startup errors\n    systemctl mask dnsmasq.service\n\n    dnf install -y dnsmasq\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # make sure we unmask before configuring the service ourselves\n    systemctl unmask dnsmasq.service\n\n    # Configure dnsmasq systemd service\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/systemd/dnsmasq-v3.service /lib/systemd/system/dnsmasq.service\n    chmod 644 /lib/systemd/system/dnsmasq.service\n    systemctl daemon-reload\n    systemctl enable dnsmasq\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop and disable services\n    systemctl disable dnsmasq\n    systemctl stop dnsmasq\n\n    # Uninstall packages\n    dnf remove -y dnsmasq\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dnsmasq/rocky/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # mask the service before running package manager to avoid faulty startup errors\n    systemctl mask dnsmasq.service\n\n    yum install -y dnsmasq\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # make sure we unmask before configuring the service ourselves\n    systemctl unmask dnsmasq.service\n\n    # Configure dnsmasq systemd service\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/systemd/dnsmasq-v2.service /lib/systemd/system/dnsmasq.service\n    chmod 644 /lib/systemd/system/dnsmasq.service\n    systemctl daemon-reload\n    systemctl enable dnsmasq\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop and disable services\n    systemctl disable dnsmasq\n    systemctl stop dnsmasq\n\n    # Uninstall packages\n    yum remove -y dnsmasq\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dnsmasq/systemd/dnsmasq-v1.service",
    "content": "[Unit]\nDescription=dnsmasq - A lightweight DHCP and caching DNS server\nRequires=basic.target network.target\nAfter=network.target network-online.target basic.target\nWants=nss-lookup.target\nBefore=nss-lookup.target\nDefaultDependencies=no\n\n[Service]\nType=forking\nPIDFile=/run/dnsmasq/dnsmasq.pid\nEnvironment='RUN_DIR=/run/dnsmasq'\nEnvironment='IGNORE_RESOLVCONF=yes'\n# make sure everything is setup correctly before starting\nExecStartPre=!-/usr/bin/dsiprouter chown -dnsmasq\nExecStartPre=/usr/sbin/dnsmasq --test\n# We run dnsmasq via the /etc/init.d/dnsmasq script which acts as a\n# wrapper picking up extra configuration files and then execs dnsmasq\n# itself, when called with the \"systemd-exec\" function.\nExecStart=/etc/init.d/dnsmasq systemd-exec\n# The systemd-*-resolvconf functions configure (and deconfigure)\n# resolvconf to work with the dnsmasq DNS server. They're called like\n# this to get correct error handling (ie don't start-resolvconf if the\n# dnsmasq daemon fails to start.\nExecStartPost=/etc/init.d/dnsmasq systemd-start-resolvconf\nExecStop=/etc/init.d/dnsmasq systemd-stop-resolvconf\nExecReload=/bin/kill -HUP $MAINPID\n\n[Install]\nWantedBy=multi-user.target"
  },
  {
    "path": "dnsmasq/systemd/dnsmasq-v2.service",
    "content": "[Unit]\nDescription=dnsmasq - A lightweight DHCP and caching DNS server\nRequires=basic.target network.target\nAfter=network.target network-online.target basic.target\nBefore=multi-user.target\nDefaultDependencies=no\n\n[Service]\nType=simple\nPIDFile=/run/dnsmasq/dnsmasq.pid\nEnvironment='RUN_DIR=/run/dnsmasq'\n# make sure everything is setup correctly before starting\nExecStartPre=!-/usr/bin/dsiprouter chown -dnsmasq\nExecStartPre=/usr/sbin/dnsmasq --test\nExecStart=/usr/sbin/dnsmasq -k\nExecReload=/bin/kill -HUP $MAINPID\n\n[Install]\nWantedBy=multi-user.target"
  },
  {
    "path": "dnsmasq/systemd/dnsmasq-v3.service",
    "content": "[Unit]\nDescription=dnsmasq - A lightweight DHCP and caching DNS server\nRequires=basic.target network.target\nAfter=network.target network-online.target basic.target\nBefore=multi-user.target\nDefaultDependencies=no\n\n[Service]\nType=simple\nPermissionsStartOnly=true\nPIDFile=/run/dnsmasq/dnsmasq.pid\nEnvironment='RUN_DIR=/run/dnsmasq'\n# make sure everything is setup correctly before starting\nExecStartPre=/usr/bin/dsiprouter chown -dnsmasq\nExecStartPre=/usr/sbin/dnsmasq --test\nExecStart=/usr/sbin/dnsmasq -k\nExecReload=/bin/kill -HUP $MAINPID\n\n[Install]\nWantedBy=multi-user.target"
  },
  {
    "path": "dnsmasq/ubuntu/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # remove previous dns stack\n    apt-get remove -y libnss-resolve systemd-resolved\n\n    if (( $? != 0 )); then\n        printerr 'Failed removing old dns stack'\n        return 1\n    fi\n\n    # mask the service before running package manager to avoid faulty startup errors\n    systemctl mask dnsmasq.service\n\n    apt-get install -y dnsmasq resolvconf\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing new dns stack'\n        return 1\n    fi\n\n    # make sure we unmask before configuring the service ourselves\n    systemctl unmask dnsmasq.service\n\n    # configure dnsmasq systemd service\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/systemd/dnsmasq-v1.service /lib/systemd/system/dnsmasq.service\n    chmod 644 /lib/systemd/system/dnsmasq.service\n    systemctl daemon-reload\n    systemctl enable dnsmasq\n\n    # tell network manager to use dnsmasq instead\n    cp -f ${DSIP_PROJECT_DIR}/dnsmasq/configs/networkmanager/dsiprouter.conf /etc/NetworkManager/conf.d/99-dsiprouter.conf\n\n    return 0\n}\n\nfunction uninstall() {\n    # remove network manager config\n    rm -f /etc/NetworkManager/conf.d/99-dsiprouter.conf\n\n    # swap old resolvers in as static file so DNS still works while uninstalling\n    mv -f /run/dnsmasq/resolv.conf /etc/resolv.conf\n\n    # uninstall new dns stack\n    apt-get remove -y --purge dnsmasq resolvconf\n\n    # reinstall old dns stack\n    apt-get install -y libnss-resolve systemd-resolved\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "docker/dsiprouter/dockerfile",
    "content": "# Dockerfile\nFROM python:3.6-stretch\n\nMAINTAINER Mack Hendricks <mack@dsiprouter.org>\n\nCOPY ./  /opt/dsiprouter/\nWORKDIR /opt/dsiprouter\nRUN pip install -r ./gui/requirements.txt\nENV DBSERVER ''\nENV DBPORT '3306'\nENV DBUSER ''\nENV DBPASS ''\nENV DBNAME ''\n\nEXPOSE 5000:5000\n\nCMD [\"python\", \"./gui/dsiprouter.py\", \"runserver\"]\n"
  },
  {
    "path": "docker/dsiprouter/wait-for-dsiprouter-mysql.sh",
    "content": "#!/bin/sh\n# wait-for-dsiprouter-mysql.sh\n\nset -e\n\nhost=\"$1\"\nshift\ncmd=\"$@\"\n\nuntil python ./gui/dsiprouter.py runserver; do\n  >&2 echo \"dSIPRouter MySQL is unavailable - can't start - sleeping\"\n  sleep 1\ndone\n\n>&2 echo \"dSIPRouter MySQL is up - started dSIPRouter\"\n"
  },
  {
    "path": "docker/mysql/dockerfile",
    "content": "# Dockerfile\nFROM python:3.6-stretch\n\nMAINTAINER Mack Hendricks <mack@dsiprouter.org>\n\nCOPY ../../  /opt/dsiprouter/\nWORKDIR /opt/dsiprouter\nRUN pip install -r ./gui/requirements.txt\nENV DBSERVER ''\nENV DBPORT '3306'\nENV DBUSER ''\nENV DBPASS ''\nENV DBNAME ''\n\nEXPOSE 5000:5000\n\nCMD [\"python\", \"./gui/dsiprouter.py\"]\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3'\nservices:\n  dsiprouter:\n    build: \n      context: .\n      dockerfile: docker/dsiprouter/dockerfile\n    image: dopensource/dsiprouter\n    volumes:\n      - .:/opt/dsiprouter\n    ports:\n      - \"5000:5000\"\n    environment:\n      - ENV=PROD\n      - DSIP_USER=admin\n      - DSIP_PASS=admin\n      - DSIP_DEBUG=True\n      - KAM_DB_HOST=dsiprouter-mysql\n      - KAM_DB_TYPE=mysql \n      - KAM_DB_PORT=3306\n      - KAM_DB_NAME=kamailio\n      - KAM_DB_USER=kamailio\n      - KAM_DB_PASS=kamailiorw\n    depends_on: \n      - dsiprouter-mysql\n    command: [\"./docker/dsiprouter/wait-for-dsiprouter-mysql.sh\",\"dsiprouter-mysql\",\"python\",\"dsiprouter.py\",\"runserver\"]\n\n  dsiprouter-mysql:\n    image: mysql:5.7\n    volumes:\n      - /tmp/dbdata:/var/lib/mysql\n      - ./testing/sql/v0.60+ent:/docker-entrypoint-initdb.d/\n    ports:\n      - \"3306:3306\"\n    environment:\n      - MYSQL_ALLOW_EMPTY_PASSWORD=yes\n      - MYSQL_USER=kamailio\n      - MYSQL_PASSWORD=kamailiorw\n      - MYSQL_DATABASE=kamailio\n    healthcheck:\n      test: mysql --user=root -e \"select * from kamailio.dr_gateways\"\n      timeout: 45s\n      retries: 5\n\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = /opt/dsiprouter/venv/bin/python -m sphinx\nSOURCEDIR     = source\nBUILDDIR      = build\n\n.PHONY: help Makefile html pdf\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help $(SPHINXOPTS) \"$(SOURCEDIR)\" \"$(BUILDDIR)\"\n\n# Make all available formats\nall: html pdf\n\n# Make building pdf with rinoh easier to run\npdf:\n\t@$(SPHINXBUILD) -b rinoh $(SPHINXOPTS) \"$(SOURCEDIR)\" \"$(BUILDDIR)/pdf\"\n\n# Make building html\nhtml:\n\t$(SPHINXBUILD) -b html $(SPHINXOPTS) \"$(SOURCEDIR)\" \"$(BUILDDIR)/html\"\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ $(SPHINXOPTS) \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(O)"
  },
  {
    "path": "docs/requirements.in",
    "content": "docutils<0.17,>=0.12\nmyst-parser\nrecommonmark\nrequests\nsphinx\nsphinxcontrib-httpdomain\nsphinx-rtd-theme\npiccolo_theme\nUltraDict\n"
  },
  {
    "path": "docs/requirements.txt",
    "content": "#\n# This file is autogenerated by pip-compile with Python 3.8\n# by the following command:\n#\n#    pip-compile --strip-extras docs/requirements.in\n#\nalabaster==0.7.13\n    # via sphinx\nbabel==2.13.1\n    # via sphinx\ncertifi==2023.7.22\n    # via requests\ncharset-normalizer==3.3.2\n    # via requests\ncommonmark==0.9.1\n    # via recommonmark\ndocutils==0.16\n    # via\n    #   -r requirements.in\n    #   myst-parser\n    #   recommonmark\n    #   sphinx\n    #   sphinx-rtd-theme\nidna==3.4\n    # via requests\nimagesize==1.4.1\n    # via sphinx\nimportlib-metadata==6.8.0\n    # via sphinx\njinja2==3.1.3\n    # via\n    #   myst-parser\n    #   sphinx\nmarkdown-it-py==2.2.0\n    # via\n    #   mdit-py-plugins\n    #   myst-parser\nmarkupsafe==2.1.3\n    # via jinja2\nmdit-py-plugins==0.3.5\n    # via myst-parser\nmdurl==0.1.2\n    # via markdown-it-py\nmyst-parser==1.0.0\n    # via -r requirements.in\npackaging==23.2\n    # via sphinx\npiccolo-theme==0.19.0\n    # via -r requirements.in\npygments==2.16.1\n    # via sphinx\npytz==2023.3.post1\n    # via babel\npyyaml==6.0.1\n    # via myst-parser\nrecommonmark==0.7.1\n    # via -r requirements.in\nrequests==2.31.0\n    # via\n    #   -r requirements.in\n    #   sphinx\nsix==1.16.0\n    # via sphinxcontrib-httpdomain\nsnowballstemmer==2.2.0\n    # via sphinx\nsphinx==5.3.0\n    # via\n    #   -r requirements.in\n    #   myst-parser\n    #   piccolo-theme\n    #   recommonmark\n    #   sphinx-rtd-theme\n    #   sphinxcontrib-httpdomain\n    #   sphinxcontrib-jquery\nsphinx-rtd-theme==1.3.0\n    # via -r requirements.in\nsphinxcontrib-applehelp==1.0.4\n    # via sphinx\nsphinxcontrib-devhelp==1.0.2\n    # via sphinx\nsphinxcontrib-htmlhelp==2.0.1\n    # via sphinx\nsphinxcontrib-httpdomain==1.8.1\n    # via -r requirements.in\nsphinxcontrib-jquery==4.1\n    # via sphinx-rtd-theme\nsphinxcontrib-jsmath==1.0.1\n    # via sphinx\nsphinxcontrib-qthelp==1.0.3\n    # via sphinx\nsphinxcontrib-serializinghtml==1.1.5\n    # via sphinx\nultradict==0.0.6\n    # via -r requirements.in\nurllib3==2.0.7\n    # via requests\nzipp==3.17.0\n    # via importlib-metadata\n"
  },
  {
    "path": "docs/source/_static/placeholder",
    "content": ""
  },
  {
    "path": "docs/source/_templates/placeholder",
    "content": ""
  },
  {
    "path": "docs/source/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Configuration file for the Sphinx documentation builder.\n#\n# This file does only contain a selection of the most common options. For a\n# full list see the documentation:\n# http://www.sphinx-doc.org/en/master/config\n\n# -- Path setup --------------------------------------------------------------\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\nimport os, sys\nsys.path.insert(0, os.path.abspath('../../gui'))\nsys.path.insert(0, '/etc/dsiprouter/gui')\nimport settings\nsys.setrecursionlimit(1500)\n\n# -- Project information -----------------------------------------------------\n\nproject = 'dSIPRouter'\ncopyright = 'dOpenSource'\nauthor = 'dOpenSource'\n\n# The short X.Y version\nversion = str(settings.VERSION)\n# The full version, including alpha/beta/rc tags\nrelease = str(settings.VERSION)\n\n\n# -- General configuration ---------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.ifconfig',\n    'sphinx.ext.viewcode',\n    'sphinx.ext.githubpages',\n    'sphinxcontrib.httpdomain',\n    'sphinxcontrib.autohttp.flask',\n    'sphinxcontrib.autohttp.flaskqref',\n#    'rinoh.frontend.sphinx',\n    'myst_parser'\n]\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\nsource_suffix = {\n    '.rst': 'restructuredtext',\n    '.md': 'markdown'\n}\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This pattern also affects html_static_path and html_extra_path.\nexclude_patterns = []\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = 'piccolo_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\nhtml_theme_options = {\n    \"source_url\": f'{settings.GIT_REPO_URL.rsplit(\".\", 1)[0]}/',\n}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n\n# Custom sidebar templates, must be a dictionary that maps document names\n# to template names.\n#\n# The default sidebars (for documents that don't match any pattern) are\n# defined by theme itself.  Builtin themes are using these templates by\n# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',\n# 'searchbox.html']``.\n#\n# html_sidebars = {}\n\n\n# -- Options for HTMLHelp output ---------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'dsiprouterdoc'\n\n\n# -- Options for LaTeX output ------------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    'papersize': 'letterpaper',\n    # The font size ('10pt', '11pt' or '12pt').\n    'pointsize': '10pt',\n    # Additional stuff for the LaTeX preamble.\n    'preamble': '',\n    # Latex figure (float) alignment\n    'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'dSIPRouter.tex', 'dSIPRouter Documentation',\n     'DevOpSec', 'manual'),\n]\n\n\n# -- Options for manual page output ------------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'dsiprouter', 'dSIPRouter Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output ----------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'dSIPRouter', 'dSIPRouter Documentation',\n     author, 'dSIPRouter', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n# -- Options for Epub output -------------------------------------------------\n\n# Bibliographic Dublin Core info.\nepub_title = project\n\n# The unique identifier of the text. This can be a ISBN number\n# or the project homepage.\n#\n# epub_identifier = ''\n\n# A unique identification for the text.\n#\n# epub_uid = ''\n\n# A list of files that should not be packed into the epub file.\nepub_exclude_files = ['search.html']\n\n\n# -- Extension configuration -------------------------------------------------\n\n# -- Options for intersphinx extension ---------------------------------------\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {'https://docs.python.org/': None}\n"
  },
  {
    "path": "docs/source/dev/database.rst",
    "content": "database\n========\n\n.. automodule:: database\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n"
  },
  {
    "path": "docs/source/dev/dsiprouter.rst",
    "content": "dsiprouter\n==========\n\n.. automodule:: dsiprouter\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n"
  },
  {
    "path": "docs/source/dev/index.rst",
    "content": "Developer Hub\n=============\n\nPython Modules\n--------------\n\nThe dSIPRouter project at its core is a WSGI Flask application that serves HTTP content.\nThis section makes the core modules easier to explore for developers.\n\n.. toctree::\n    :titlesonly:\n\n    database <database.rst>\n    dsiprouter <dsiprouter.rst>\n    modules <modules.rst>\n    settings <settings.rst>\n    shared <shared.rst>\n    sysloginit <sysloginit.rst>\n    util <util.rst>\n\n.. TODO: changelog uses github-flavored markdown comments and is not parsed correctly by myst-parser\n\nContibuting Guidelines\n----------------------\n\n.. toctree::\n\n    external_links/CONTRIBUTING.md\n\nCredits\n-------\n\n.. toctree::\n    :titlesonly:\n\n    external_links/CONTRIBUTORS.md\n"
  },
  {
    "path": "docs/source/dev/modules.rst",
    "content": "=======\nmodules\n=======\n\nmodules.api.api_routes\n======================\n\n.. automodule:: modules.api.api_routes\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nmodules.api.cron_functions\n==========================\n\n.. automodule:: modules.api.cron_functions\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nmodules.cdr.cron_functions\n==========================\n\n.. automodule:: modules.cdr.cron_functions\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nmodules.domain.domain_routes\n============================\n\n.. automodule:: modules.domain.domain_routes\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nmodules.flowroute\n=================\n\n.. automodule:: modules.flowroute\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nmodules.frauddetection.fraud\n============================\n\n.. automodule:: modules.frauddetection.fraud\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nmodules.fusionpbx.fusionpbx_sync_functions\n==========================================\n\n.. automodule:: modules.fusionpbx.fusionpbx_sync_functions\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n"
  },
  {
    "path": "docs/source/dev/settings.rst",
    "content": "settings\n========\n\n.. automodule:: settings\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n"
  },
  {
    "path": "docs/source/dev/shared.rst",
    "content": "shared\n======\n\n.. automodule:: shared\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n"
  },
  {
    "path": "docs/source/dev/sysloginit.rst",
    "content": "sysloginit\n==========\n\n.. automodule:: sysloginit\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n"
  },
  {
    "path": "docs/source/dev/util.rst",
    "content": "====\nutil\n====\n\nutil.conversions\n================\n\n.. automodule:: util.conversions\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nutil.cron\n=========\n\n.. automodule:: util.cron\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nutil.file_handling\n==================\n\n.. automodule:: util.file_handling\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nutil.ipc\n========\n\n.. automodule:: util.ipc\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nutil.kamtls\n===========\n\n.. automodule:: util.kamtls\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nutil.letsencrypt\n================\n\n.. automodule:: util.letsencrypt\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nutil.networking\n===============\n\n.. automodule:: util.networking\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nutil.notifications\n==================\n\n.. automodule:: util.notifications\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nutil.parse_json\n===============\n\n.. automodule:: util.parse_json\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nutil.persistence\n================\n\n.. automodule:: util.persistence\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nutil.pyasync\n============\n\n.. automodule:: util.pyasync\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nutil.security\n=============\n\n.. automodule:: util.security\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n\nutil.time_funcs\n===============\n\n.. automodule:: util.time_funcs\n   :members:\n   :undoc-members:\n   :private-members:\n   :special-members:\n"
  },
  {
    "path": "docs/source/index.rst",
    "content": "dSIPRouter Docs\n===============\n\nThere are 3 main sections of documentation:\n\n1. User Documentation\n2. Developer Documentation\n3. Routes Documentation (API/GUI routes)\n\nUse the table of contents on your left to navigate between sections.\n\n\nIndices and tables\n------------------\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n\n.. toctree::\n   :caption: Table of Contents\n   :name: mastertoc\n   :titlesonly:\n   :glob:\n   :hidden:\n\n    User Documentation <user/index.rst>\n    Developer Documentation <dev/index.rst>\n    Routes Documentation <routes/index.rst>\n"
  },
  {
    "path": "docs/source/routes/details.rst",
    "content": "Route Details\n=============\n\n.. autoflask:: dsiprouter:app\n    :undoc-static:\n    :include-empty-docstring:\n"
  },
  {
    "path": "docs/source/routes/index.rst",
    "content": "GUI/API Routes\n==============\n\nThe GUI and API make several routes accessible for usage.\nA summary and description of each route are provided below.\n\nSummary Table\n-------------\n\n.. toctree::\n    :maxdepth: 2\n\n    summary.rst\n\nDetailed Descriptions\n---------------------\n\n.. toctree::\n    :maxdepth: 2\n\n    details.rst\n"
  },
  {
    "path": "docs/source/routes/summary.rst",
    "content": "Route Summary\n=============\n\n.. raw:: html\n\n    <style>div.document > div.documentwrapper {max-width: fit-content;}</style>\n\n.. raw:: html\n\n    <script>\n        document.addEventListener(\"DOMContentLoaded\", (ev) => {\n            var tmp;\n            var elems = document.querySelectorAll('table > tbody tr td:nth-child(2) a.reference.external');\n            for (var i = 0; i < elems.length; i++) {\n                tmp = elems[i].href.split('#');\n                elems[i].setAttribute('href', tmp[0].split('/').slice(0, -1).join('/') + '/details.html#' + tmp[1]);\n            }\n        });\n    </script>\n\n.. qrefflask:: dsiprouter:app\n    :undoc-static:\n    :autoquickref:\n"
  },
  {
    "path": "docs/source/user/api.rst",
    "content": "dSIPRouter API Intro\n====================\n\nThe complete API is defined as a public Postman Workspace, which can be found `here <https://www.postman.com/dopensource/workspace/dsiprouter/collection/4319695-9c09dea3-0b4b-4a20-a615-fb8fc16811af?action=share&creator=4319695>`_ \n\nThe steps to obtain the API Token key and examples of using the API via curl are below, but we highly recommend using Postman for testing the API.\n\nGetting Your Token\n------------------\n\nYour token was provided to you after you installed dSIPRouter.  You can reset your token if you didn't write it down, by executing the following command\n\n.. code-block:: bash\n\n    DSIP_HOSTNAME=<your ip or hostname>\n    DSIP_TOKEN=<your token>\n    dsiprouter setcredentials -ac $DSIP_TOKEN\n\nExecuting Kamailio stats API\n----------------------------\n\n.. code-block:: bash\n\n    curl -k -H \"Authorization: Bearer $DSIP_TOKEN\" -X GET https://$DSIP_HOSTNAME:5000/api/v1/kamailio/stats\n\nExecuting Lease Point API\n-------------------------\n\nCreate a new endpoint lease\n\n.. code-block:: bash\n\n    curl -k -H \"Authorization: Bearer $DSIP_TOKEN\" -H \"Content-Type: application/json\" -X GET \"https://$DSIP_HOSTNAME:5000/api/v1/endpoint/lease?ttl=15&email=mack@dsiprouter.org\"\n\nRevoking and replacing with your own lease ID\n\n.. code-block:: bash\n\n    curl -k -H \"Authorization: Bearer $DSIP_TOKEN\" -H \"Content-Type: application/json\" -X PUT \"https://$DSIP_HOSTNAME:5000/api/v1/endpoint/lease/1/revoke\"\n\nFurther Reading\n+++++++++++++++\n\nAll available routes are documented in the :doc:`routes documentation <../routes/index>`.\n"
  },
  {
    "path": "docs/source/user/carrier_groups.rst",
    "content": ".. _carrier_groups:\n\nCarrier Groups\n^^^^^^^^^^^^^^\n\nThe Carrier Group section of dSIPRouter allows you to define which carriers will be used to provide Internet service (aka ISP) for your VOIP (Voice Over IP) services. Carrier groups support IP Authentication and Username/Password authentication. Below is an example of a carrier groups list.\n\n.. image:: images/carrier_groups.png\n        :align: center\n        \nAdding a Carrier\n^^^^^^^^^^^^^^^^\n\n- Log into dSIPRouter using proper username and password.\n\n- Click \"Add\" to create a Carrier Group.  A carrier group can contain 1 or more SIP endpoints provided by the carrier. A SIP Endpoint represents a device that makes or receives calls via your Gateway. This could be a physical IP phone, a softphone app such as Skype, on a PC or smartphone, an Analog Telephone Adapter (ATA) such as for fax machines, or even a PBX system. \n\n- Select Username/Password Auth, fill in the username, password of your registration server and the registration server name. Then click ADD.\n\n\n.. image:: images/add_carrier_group.png\n        :align: center\n\n\n\nNOTE: Click IP authenication to use only the IP address of your PBX/endpoint.\n\n\n.. image:: images/IP_authenication.png\n       :align: center\n\n\nFor example:   \n\n.. image:: images/username_password.png\n        :align: center\n\n\nAfter you have added the new group, the screen will return back to the List of Carriers Group page. Select the pencil in the blue box to the right to allow editing the Config and Endpoints. \n\n\n\n.. image:: images/carrier_editing.png\n        :align: center\n\n\n\nSelect the Config tab. The Config tab allows you to edit/change the Carrier group name. Then click Update.\n\n.. image:: images/config_pic.png\n        :align: center\n        \n\n\n\nTo add an Endpoint, click the Endpoint tab. \n\n.. image:: images/add_endpoint.png\n        :align: center\n       \nClick ADD, enter the Friendly name (optional), the IP address of the endpoint/device, # of characters to strip from RURI, the character to prefix to a RURI then click ADD again.  For example, if a PBX sends a number over as 914443332222 but the carrier wants the number to be sent as 14443332222 then the # of characters to strip should be defined as 1, which would strip off the 9. Some carriers request added digits (aka Prefixes) in front of the phone number.\n\n\n.. image:: images/add_new_carrier_details.png\n :align: center\n\nEdit and click ADD again to add addtional endpoints. Click the gray X in that box to save the window and close.\n\nYou should now see your added carrier with endpoints in the Carrier Group List.\n\n.. image:: images/carrier_group_list.png\n :align: center\n\n \nBe sure to click the Reload Kamailio button to apply changes.\n   \n\n.. image:: images/reload_button.png\n :align: center\n \n \n \n \n \n \n"
  },
  {
    "path": "docs/source/user/command_line_options.rst",
    "content": "Command Line Options\n====================\n\nExecute \"./dsiprouter.sh\" followed by one of the listed commands.\n**NOTE** Once installed the command will be available globally as *dsiprouter* with tab-completion.\n\n===================================     ======================================================================\nCommand                                 What does it do?\n===================================     ======================================================================\ninstall                                 Installs dSIPRouter and related services\nuninstall                               Uninstall dSIPRouter and related services\nclusterinstall                          Install dSIPRouter (via SSH) on a cluster of nodes\nupgrade                                 Upgrade dSIPRouter platform (requires license)\nstart                                   Starts dSIPRouter\nstop                                    Stops dSIPRouter\nrestart                                 Restarts dSIPRouter\nchown                                   Update file permissions for dSIPRouter and related services\nconfigurekam                            Reconfigures the Kamailio configurations based on dSIPRouter settings\nconfiguredsip                           Reconfigures the dSIPRouter configurations, updating any dynamic settings\nconfigurertp                            Reconfigures the RTPEngine configurations based on dSIPRouter settings\nrenewsslcert                            Renew configured letsencrypt SSL certificate\nconfiguresslcert                        Reconfigures SSL certificate used by Kamailio and dSIPRouter\ninstallmodules                          Install / uninstall dDSIProuter modules\nresetpassword                           Generate new random dSIPRouter admin account password\nsetcredentials                          Set various credentials manually\nversion                                 Show dSIPRouter version\nhelp                                    List all of the options\n===================================     ======================================================================\n\nRefer to :ref:`installing_dsiprouter` to get the complete one line version of the command.\n\n\nTo start dSIPRouter:\n\n.. code-block:: bash\n\n    dsiprouter start\n\nTo stop dSIPRouter:\n\n.. code-block:: bash\n\n    dsiprouter stop\n\nTo restart dSIPRouter:\n\n.. code-block:: bash\n\n    dsiprouter restart\n\nTo uninstall dSIPRouter:\n\n.. code-block:: bash\n\n    dsiprouter uninstall -all\n"
  },
  {
    "path": "docs/source/user/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Configuration file for the Sphinx documentation builder.\n#\n# This file does only contain a selection of the most common options. For a\n# full list see the documentation:\n# http://www.sphinx-doc.org/en/master/config\n\n# -- Path setup --------------------------------------------------------------\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\nimport os, sys\n#sys.path.insert(0, os.path.abspath('../../gui'))\n#sys.path.insert(0, '/etc/dsiprouter/gui')\n#import settings\nsys.setrecursionlimit(1500)\n\n# -- Project information -----------------------------------------------------\n\nproject = 'dSIPRouter'\ncopyright = 'dOpenSource'\nauthor = 'dOpenSource'\n\n# The short X.Y version\n#version = settings.VERSION\n# The full version, including alpha/beta/rc tags\n#release = settings.VERSION\n\n\n# -- General configuration ---------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.ifconfig',\n    'sphinx.ext.viewcode',\n    'sphinx.ext.githubpages',\n#    'sphinxcontrib.httpdomain',\n#    'sphinxcontrib.autohttp.flask',\n#    'sphinxcontrib.autohttp.flaskqref',\n#    'rinoh.frontend.sphinx',\n    'myst_parser'\n]\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\nsource_suffix = {\n    '.rst': 'restructuredtext',\n    '.md': 'markdown'\n}\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This pattern also affects html_static_path and html_extra_path.\nexclude_patterns = []\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\nhtml_theme_options = {\n    \"collapse_navigation\" : False\n}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n\n# Custom sidebar templates, must be a dictionary that maps document names\n# to template names.\n#\n# The default sidebars (for documents that don't match any pattern) are\n# defined by theme itself.  Builtin themes are using these templates by\n# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',\n# 'searchbox.html']``.\n#\n# html_sidebars = {}\n\n\n# -- Options for HTMLHelp output ---------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'dsiprouterdoc'\n\n\n# -- Options for LaTeX output ------------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    'papersize': 'letterpaper',\n    # The font size ('10pt', '11pt' or '12pt').\n    'pointsize': '10pt',\n    # Additional stuff for the LaTeX preamble.\n    'preamble': '',\n    # Latex figure (float) alignment\n    'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'dSIPRouter.tex', 'dSIPRouter Documentation',\n     'DevOpSec', 'manual'),\n]\n\n\n# -- Options for manual page output ------------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'dsiprouter', 'dSIPRouter Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output ----------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'dSIPRouter', 'dSIPRouter Documentation',\n     author, 'dSIPRouter', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n# -- Options for Epub output -------------------------------------------------\n\n# Bibliographic Dublin Core info.\nepub_title = project\n\n# The unique identifier of the text. This can be a ISBN number\n# or the project homepage.\n#\n# epub_identifier = ''\n\n# A unique identification for the text.\n#\n# epub_uid = ''\n\n# A list of files that should not be packed into the epub file.\nepub_exclude_files = ['search.html']\n\n\n# -- Extension configuration -------------------------------------------------\n\n# -- Options for intersphinx extension ---------------------------------------\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {'https://docs.python.org/': None}\n"
  },
  {
    "path": "docs/source/user/configuring.rst",
    "content": "dSIPRouter GUI Intro\n====================\n\n.. toctree::\n  :maxdepth: 3\n\n  carrier_groups.rst\n  pbxs_and_endpoints.rst\n  domains.rst\n  inbound_did_mapping.rst\n  global_outbound_routes.rst\n  \n"
  },
  {
    "path": "docs/source/user/debian_install.rst",
    "content": ".. _debian_install:\n\nInstalling on a Debian-based Distro\n===================================\n\nFor a specific version of dSIPRouter add \"-b <version number>\" to the end of the `git clone` command.\n\nMake sure to **set the hostmane to a fully qualified domain name (FQDN)** that has DNS records pointed at the server (like sbc.yourdomain.com) prior to installation.\nThe average install time is between 9-12 minutes depending on the resources on your vm/server and the options your specify.\n\nSet the Hostname \n----------------\n\n.. code-block:: bash\n  \n  hostnamectl set-hostname <hostname>\n  \n\nInstall (Don't Proxy audio (RTP) traffic)\n-----------------------------------------\n\n.. code-block:: bash\n\n  apt-get update -y\n  apt-get install -y git\n  cd /opt\n  git clone https://github.com/dOpensource/dsiprouter.git\n  cd dsiprouter\n  ./dsiprouter.sh install\n\n\nOne Line Version:\n\n.. code-block:: bash\n\n  apt-get update -y && apt-get install -y git && cd /opt && git clone https://github.com/dOpensource/dsiprouter.git && cd dsiprouter && ./dsiprouter.sh install\n\n\nInstall (Proxy audio (RTP) traffic)\n-----------------------------------\n\nIf you need to proxy RTP traffic then use -all install option. The command to install dSIPRouter and the RTPEngine would be:\n\n\n.. code-block:: bash\n\n  apt-get update -y\n  apt-get install -y git\n  cd /opt\n  git clone https://github.com/dOpensource/dsiprouter.git\n  cd dsiprouter\n  ./dsiprouter.sh install -all\n\n\nOne Line Version:\n\n.. code-block:: bash\n\n  apt-get update -y && apt-get install -y git && cd /opt && git clone https://github.com/dOpensource/dsiprouter.git && cd dsiprouter && ./dsiprouter.sh install -all\n\nThe install script will automatically determine if the server is behind NAT.\nOnce the install is complete, dSIPRouter will automatically start MySQL, Kamailio and the UI.\n"
  },
  {
    "path": "docs/source/user/domains.rst",
    "content": "Adding a Domain\n^^^^^^^^^^^^^^^\n\nTo add a domain click on Domains then click the green add button.\n\n.. image:: images/add_a_domain.png\n        :align: center\n\nFill in the domain name. (Note: You can create 1 or more domains by separating them with commas).\n\n- Select Realtime DB or Local Subscriber table (for multiple domains)\n- Select Pass Thru to PBX (single domain). \n\nNote: Details can be found in Realtime DB if you want to ensure that the Kamailio configuration file is setup to point to the Asterisk Realtime database configuration. Details on how to populate the table can be found in the Local Suscriber table if you want to use the built in subscriber table that's part of Kamailio. Use the pass thru to register info to the FreePBX server so that you don’t have to change how authentication is done.\n\n\n.. image:: images/add_new_domain522.png\n        :align: center\n\n- For the List of backend PBX ID's you should use the ID assigned to each PBX that you want to be part of that domain. Such as naming the ID number thats assigned to media-02.voipmuch.com for example in :doc:`PBX(s) and Enpoints <pbxs_and_endpoints>`.\n\n.. image:: images/add_new_domain_dev522.png\n        :align: center\n        \n-  Click ADD\n\nYou will then be returned back to the List of domains page and you should see your new domain added. You can delete this domain by clicking the red trash can to the right of the page.\n\n\n .. image:: images/list_of_domains1.png\n  :align: center  \n\n\nBe sure to click the Reload Kamailio button to apply changes.\n   \n\n.. image:: images/reload_button.png\n :align: center\n \n"
  },
  {
    "path": "docs/source/user/global_outbound_routes.rst",
    "content": "\n.. _global_outbound_routes:\n\nGlobal Outbound Routes\n^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n\n1) Go to the Dashboard screen.\n\n\n\n.. image:: images//dSIP_dashboard.png\n        :align: center\n\n\n\n2) Click on Global Outbound Routes.\n\n\n\n3) Click on the green Add button.\n\n\n\n.. image:: images//dSIP_Global_Out_Add.png\n        :align: center\n\n\n\n4) \n  a) Enter in the Outbound Route information.\n  b) Click on the green Add button.\n\n.. image:: images//dSIP_Global_Out_Add_Outbound_Route.png\n        :align: center\n        \n        \n5) Click on the blue Reload Kamailio button in order for the changes to be updated.\n\n"
  },
  {
    "path": "docs/source/user/images/DID_test.csv",
    "content": "13134860409\r\n13134860410\r\n13134860411\r\n"
  },
  {
    "path": "docs/source/user/inbound_did_mapping.rst",
    "content": "Inbound DID Mapping\n======================\n\n\n\nTo Import a DID from a CSV file:\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n1) Click on Inbound DID Mapping.\n\n\n\n.. image:: images//dSIP_IN_DID_Map.png\n        :align: center\n\n\n\n2) Click on the green Import DID button underneath List on Inbound Mappings.\n\n\n\n.. image:: images//dSIP_IN_Import_DID.png\n        :align: center\n\n\n\n3) Click the Browse button and select the file that contains the DID numbers that you wish to use.\n\n4) Click the green Add button.\n\n  Click `CSV Example <https://https://raw.githubusercontent.com/dOpensource/dsiprouter/master/gui/static/template/DID_example.csv>`_ to view a sample of the .CSV file\n\n5) Click on the Reload Kamailio button in order for the changes to be updated.\n\n\nTo Manually import a DID:\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\n1) Click on Inbound DID Mapping\n2) Click on the green ADD button.\n\n  - Enter the name of the Inbound mapping\n  - Enter the DID number in the DID field.\n  - Select the Endpoint Group from the drop-down list\n\n      Note: Each endpoint will contain at least two entries.  One that leverages load balancing weights and another that randomly selects an endpoint.\n      The one denoted with a LB is the one that uses the load balancing algorithm.  If FusionPBX Domain Support is enabled you will see an additional\n      entry for routing to the external interface of the FusionPBX server.\n\n  - Click the green Add button.\n\n  .. image:: images//dSIP_IN_DID_Map.png\n          :align: center\n\n\n\n3) Click on the Reload Kamailio button in order for the changes to be updated.\n"
  },
  {
    "path": "docs/source/user/index.rst",
    "content": "The dSIPRouter Project\n======================\n\n.. raw:: html\n\n    <h2>Intro to dSIPRouter</h2>\n\ndSIPRouter allows you to quickly turn `Kamailio <http://www.kamailio.org/>`_ into an easy to use SIP Service Provider platform, which enables the following two basic use cases:\n\n- **SIP Trunking services:** Provide services to customers that have an on-premise PBX such as FreePBX, FusionPBX, Avaya, etc. We have support for IP and credential based authentication.\n\n- **Hosted PBX services:** Proxy SIP Endpoint requests to a multi-tenant PBX such as FusionPBX or single-tenant such as FreePBX. We have an integration with FusionPBX that make is really easy and scalable!\n\n- **Microsoft Teams Direct Routing:** We can provide SBC functionality that allows dSIPRouter to interconnect your existing voice infrastructure or VoIP carrier to your Microsoft Teams environment.\n\n\n.. raw:: html\n\n    <h2>Demo System</h2>\n\nYou can checkout our demo system by clicking the link below and enter the listed username and password:\n\n\nhttp://demo.dsiprouter.net:5000\n\n\nusername: admin\n\npassword: ZmIwMTdmY2I5NjE4\n\nAPI Token: 91RpJidL1f2eEDSyxUDnn83B7jipuDyl\n\n\n.. raw:: html\n\n    <h2>Credits</h2>\n\nI'd like to say thank you to Nicole D., John O. and Courtney G. for their time in fulfilling this document. I'd also like to give a hardy thank you to dOpensource for their monetary support in funding this document.\n\n.. raw:: html\n\n    <h2>Support</h2>\n\nFree support is available via our `group <https://groups.google.com/forum/#!forum/dsiprouter/>`_ and `Slack <https://join.slack.com/t/dsiproutercommunity/shared_invite/zt-1dtqvpyck-H9k~DYgJJ2XIFgh_rWqdPA/>`_\n\nPaid support is available `here <https://dopensource.com/product/dsiprouter-core/>`_\n\nInstalling dSIPRouter\n---------------------\n\n.. toctree::\n    :maxdepth: 2\n\n    installing.rst\n\nCommand Line Options\n--------------------\n\n.. toctree::\n    :maxdepth: 2\n\n    command_line_options.rst\n\nConfiguring dSIPRouter\n----------------------\n\n.. toctree::\n    :maxdepth: 2\n\n    configuring.rst\n\nImplementing Use Cases\n----------------------\n\n.. toctree::\n    :maxdepth: 2\n\n    use-cases.rst\n\nREST API\n--------\n\n.. toctree::\n    :maxdepth: 2\n\n    api.rst\n\nSupported Configurations\n------------------------\n\n.. toctree::\n    :maxdepth: 2\n\n    supported_configurations.rst\n\nTroubleshooting\n---------------\n\n.. toctree::\n    :maxdepth: 2\n\n    troubleshooting.rst\n\nUpgrading dSIPRouter\n--------------------\n\n.. toctree::\n    :maxdepth: 2\n\n    upgrading.rst\n\nExtra Resources\n---------------\n\n.. toctree::\n    :maxdepth: 2\n\n    resources.rst\n"
  },
  {
    "path": "docs/source/user/installing.rst",
    "content": ".. _installing_dsiprouter:\n\nInstalling dSIPRouter\n=====================\n\nThe following video shows you the install process:\n\n.. raw:: html\n\n    <div style=\"position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; height: auto;\">\n        <iframe src=\"https://www.youtube.com/embed/Iu4BQkL1wGc\" frameborder=\"0\" allowfullscreen style=\"position: absolute; top: 0; left: 0; width: 560px; height: 315px;\"></iframe>\n    </div>\n\nWe maintain installation documentation for the following operating systems.  Please open a pull request if you want to add and maintain addtional documentation:\n\n- :ref:`debian_install`\n- :ref:`rhel_install`\n\nInstall times vary by depending on OS and system hardware.\nOn debian/centos, expect a short install time, typically around 12 minutes.\nOn amazon linux, expect long compilation times, typically around 45 minutes.\n\ndSIPRouter should be installed on a clean install of the OS.\nTo upgrade your dSIPRouter platform, see instead :ref:`upgrading`\n\nPrerequisites:\n--------------\n\n- Must run this as the root user (you can use sudo)\n- git needs to be installed\n- Hostname needs to be set to a FQDN (for certbot to get LetsEncrypt certificate)\n- The installer will handle all other dependencies\n\nInstall Options\n----------------\n\n- Proxy SIP Traffic Only (Don't Proxy audio (RTP) traffic)\n- Proxy SIP Traffic, Audio and it configures the system to work properly when the PBX's and dSIPRouter are behind a NAT.\n\nOS Support\n----------\n\n===================================     ================\nOS / Distro                             Current Support\n===================================     ================\nDebian 12 (bookworm)                    STABLE\nDebian 11 (bullseye)                    STABLE\nDebian 10 (buster)                      STABLE\nDebian 9 (stretch)                      DEPRECATED\nCentOS 9 (stream)                       STABLE\nCentOS 8 (stream)                       STABLE\nCentOS 7                                DEPRECATED\nRedHat Linux 9                          STABLE\nRedHat Linux 8                          ALPHA\nAlma Linux 9                            STABLE\nAlma Linux 8                            BETA\nRocky Linux 9                           STABLE\nRocky Linux 8                           BETA\nAmazon Linux 2                          STABLE\nUbuntu 24.04 (noble)                    STABLE\nUbuntu 22.04 (jammy)                    ALPHA\nUbuntu 20.04 (focal)                    DEPRECATED\n===================================     ================\n\nAmazon AMI's\n------------\n\nWe now provide Amazon AMI's (pre-built images) which allows you to get up and going even faster.\nYou can find a list of the images `here <https://aws.amazon.com/marketplace/search/results?x=0&y=0&searchTerms=dsiprouter/>`_.\nThe images are a nominal fee, which goes toward supporting the project.\n"
  },
  {
    "path": "docs/source/user/pbxs_and_endpoints.rst",
    "content": "PBX(s) and Endpoints\n======================\n\n\n\nAllows you to define a PBX or Endpoint that will send or receive calls from dSIPRouter.  The PBX or Endpoint can use IP\nauthentication or a username/password can be defined.\n\n\n\nTo add an Endpoint Group:\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\n1) Click on Endpoints Groups.\n\n\n\n2) Click on the green Add button.\n\n.. image:: images//dSIP_PBX_Add.png\n        :align: center\n\n3) Configure the Endpoint Group\n\n     The Endpoint Tab is where you specify the endpoints that will be signaling\n     with dSIPRouter.  The weight field allows you to define how much SIP traffic\n     is distributed to a particular endpoint. If you don't specify a weight for an endpoint\n     the system will automatically generate a weight.  If you are using FusionPBX Domain\n     Auth then Register and INVITE requests will be distributed to the endpoints based\n     upon the weights.  You will also have the option to route Inbound calls to the\n     endpoints based on the weights by selecting the name of the Endpoint Group with\n     an LB concatenated to the name.  For example, if the name of the Endpoint Group is\n     **PBXCluster** then you would select **PBXCluster LB** from the Inbound Mapping\n     Endpoint Group drop down.\n\n  b) Click the green Add button.\n\n.. image:: images//dSIP_PBX_ADD_New_PBX.png\n        :align: center\n\n\n\n4) Click on the Reload Kamailio button in order for the changes to be updated.\n"
  },
  {
    "path": "docs/source/user/resources.rst",
    "content": "Extra Resources\n===============\n\nUploading CSVs\n--------------\n\n`CSV Example <https://raw.githubusercontent.com/dOpensource/dsiprouter/v0.51/docs/images/DID_test.csv>`_ \n\nProxy FusionPBX UI\n------------------\n\nAdd the following stanza before \"location /images/\" stanza to proxy the FusionPBX UI thru dSIPRouter.  Once the following text \nis added to /opt/dsiprouter/gui/modules/fusionpbx/dsiprouter.nginx.tpl you will be able to access the FusionPBX GUI via:\nhttps://dSIPRouter_IP/ or https://dSIPRouter_IP::\n\n    location / {\n        proxy_pass https://fusionpbx;\n        proxy_redirect off;\n        proxy_next_upstream error timeout http_404 http_403 http_500 http_502 http_503 http_504 non_idempotent;\n    }\n"
  },
  {
    "path": "docs/source/user/rhel_install.rst",
    "content": ".. _rhel_install:\n\nInstalling on a RHEL-based Distro\n=================================\n\nFor a specific version of dSIPRouter add \"-b <version number>\" to the end of the `git clone` command.\n\nMake sure to **set the hostmane to a fully qualified domain name (FQDN)** that has DNS records pointed at the server (like sbc.yourdomain.com) prior to installation.\nThe average install time is between 9-12 minutes depending on the resources on your vm/server and the options your specify.\n\nInstall (Don't Proxy audio (RTP) traffic)\n-----------------------------------------\n\n.. code-block:: bash\n\n  yum install -y git\n  cd /opt\n  git clone https://github.com/dOpensource/dsiprouter.git\n  cd dsiprouter\n  ./dsiprouter.sh install\n\n\nOne Line Version:\n\n.. code-block:: bash\n\n   yum install -y git && cd /opt && git clone https://github.com/dOpensource/dsiprouter.git && cd dsiprouter && ./dsiprouter.sh install -kam -dsip\n\n\nOnce the install is complete, dSIPRouter will automatically start MySQL, Kamailio and the UI.\n\nInstall (Proxy audio (RTP) traffic)\n-----------------------------------\n\nIf you need to proxy RTP traffic then use -all install option. The command to install dSIPRouter and the RTPEngine would be:\n\n\n.. code-block:: bash\n\n  yum install -y git\n  cd /opt\n  git clone https://github.com/dOpensource/dsiprouter.git\n  cd dsiprouter\n  ./dsiprouter.sh install -all\n\n\nOne Line Version:\n\n.. code-block:: bash\n\n  yum install -y git && cd /opt && git clone https://github.com/dOpensource/dsiprouter.git && cd dsiprouter && ./dsiprouter.sh install -all\n\nThe install script will automatically determine if the server is behind NAT.\nOnce the install is complete, dSIPRouter will automatically start MySQL, Kamailio and the UI.\n"
  },
  {
    "path": "docs/source/user/supported_configurations.rst",
    "content": ".. _supported_configurations\n\nSupported Configurations\n========================\n\nPass Thru to PBX Authentication Supported Configurations\n--------------------------------------------------------\n\n================   =================   ===========   =================   ================   =========================================================\nPBX Distribution    PBX Version        Driver Type   Registration Test   Ext to Ext Test    Notes \n================   =================   ===========   =================   ================   =========================================================\nFreePBX            Asterisk 13.22.0    chan_sip      Pass                Pass               see :ref:`enabling-the-path-header-for-asterisk-chan_sip`\nFreePBX            Asterisk 13.22.0    chan_pjsip    Pass                Not Tested         suppport_path needs to be enabled\nFusionPBX          FreeSWITCH 1.6      Sofia         Pass                Pass\n================   =================   ===========   =================   ================   =========================================================\n\n.. _enabling-the-path-header-for-asterisk-chan_sip:\n\nEnabling the Path Header for Asterisk chan_sip  \n----------------------------------------------\n\n1. Login into the FreePBX Admin GUI\n2. Click Settings -> Asterisk SIP Settings\n3. Click Chan SIP Settings\n4. Find the \"Other SIP Settings\" field\n5. Add the following field and click \"Add Field\"\n\n   supportpath = yes\n\n6. Click Submit\n7. Click the red \"Apply\" settings button at the very top of the page\n"
  },
  {
    "path": "docs/source/user/troubleshooting.rst",
    "content": "Troubleshooting\n===============\n\nHere you can troubleshoot logs for dSIPRouter, Kamailio and rtpengine:\n\nAll of our services are using syslog. For more information on `syslog <https://www.rsyslog.com/doc/master/index.html>`_ click here.\n\nDefault log facilities:\n\n============  ==========\nLog Facility  Service\n============  ==========\nlocal0        kamailio\nlocal1        rtpengine\nlocal2        dsiprouter\n============  ==========\n\nKamailio Logging\n----------------\n\n1. How to turn logging on\n\nEdit /etc/rsyslog.d/kamailio.conf and ensure the line beginning with local0 is not commented out:\n\n.. code-block:: bash\n\n    vi /etc/rsyslog.d/kamailio.conf\n\nThen restart syslog:\n\n.. code-block:: bash\n\n    systemctl restart rsyslog\n\n2. How to turn logging off\n\nEdit /etc/rsyslog.d/kamailio.conf and ensure the line beginning with local0 is commented out:\n\n.. code-block:: bash\n\n    vi /etc/rsyslog.d/kamailio.conf\n\nThen restart syslog:\n\n.. code-block:: bash\n\n    systemctl restart rsyslog\n\n3. Location of the log files\n\nThe default location is found here: /var/log/kamailio.log\n\n4. How to configure it\n\nEdit /etc/kamailio/kamailio.conf and change the variable ‘debug’ to the syslog logging verbosity of your choice.\n\n.. code-block:: bash\n\n    vi /etc/kamailio/kamailio.conf\n\n5. For more information see the documentation below:\n\nhttps://www.kamailio.org/wiki/tutorials/3.2.x/syslog\n\nRTPEngine Logging\n-----------------\n\n1. How to turn logging on\n\nEdit /etc/rsyslog.d/rtpengine.conf and ensure the line beginning with local1 is not commented out:\n\n.. code-block:: bash\n\n    vi /etc/rsyslog.d/rtpengine.conf\n\nThen restart syslog:\n\n.. code-block:: bash\n\n    systemctl restart rsyslog\n\n2. How to turn logging off\n\nEdit /etc/rsyslog.d/rtpengine.conf and ensure the line beginning with local1 is commented out:\n\n.. code-block:: bash\n\n    vi/etc/rsyslog.d/rtpengine.conf\n\nThen restart syslog:\n\n.. code-block:: bash\n\n    systemctl restart rsyslog\n\n3. Location of the log files\n\nThe default location is found here: /var/log/rtpengine.log\n\n4. How to configure it\n\nEdit /etc/rtpengine/rtpengine.conf and change the variable ‘debug’ to the syslog logging verbosity of your choice.\n\n.. code-block:: bash\n\n    vi /etc/rtpengine/rtpengine.conf\n\n**5. For more information see the documentation below:**\n\nhttps://github.com/sipwise/rtpengine\n\ndSIPRouter Logging\n------------------\n\n1. How to turn logging on\n\nEdit /etc/rsyslog.d/dsiprouter.conf and ensure the line beginning with local2 is not commented out:\n\n.. code-block:: bash\n\n    vi /etc/rsyslog.d/dsiprouter.conf\n\nThen restart syslog:\n\n.. code-block:: bash\n\n    systemctl restart rsyslog\n\n2. How to turn logging off\n\nEdit /etc/rsyslog.d/dsiprouter.conf and ensure the line beginning with local2 is commented out:\n\n.. code-block:: bash\n\n    vi /etc/rsyslog.d/dsiprouter.conf\n\nThen restart syslog:\n\n.. code-block:: bash\n\n    systemctl restart rsyslog\n\n3. Location of the log files\n\nThe default location is found here: /var/log/dsiprouter.log\n\n4. How to configure it\n\nEdit /etc/dsiprouter/gui/settings.py and change the variable ‘DSIP_LOG_LEVEL’ to the syslog logging verbosity of your choice.\n\n.. code-block:: bash\n\n    vi /etc/dsiprouter/gui/settings.py\n\n**5. For more infornation see the documentation below:**\n\nhttps://success.trendmicro.com/solution/TP000086250-What-are-Syslog-Facilities-and-Levels\n"
  },
  {
    "path": "docs/source/user/upgrade_0.50_to_0.51.rst",
    "content": "In this section we will show you how to upgrade from 0.50 to 0.51.\n\nBefore starting the upgrade process you will need to backup your kamailio database using the following command: \n\n.. code-block:: bash\n  \n  cd /opt/\n  mysqldump kamailio > kamailio-bk.sql\n\n\nAfter you've backed up your database you can now uninstall dsiprouter v0.50 by running the following commands: \n\n.. code-block:: bash\n\n  cd /opt/dsiprouter\n  ./dsiprouter.sh uninstall\n\n\nOnce the uninstall is complete you will need to either move or delete the /dsiprouter directory using the following command.\n\n.. code-block:: bash\n  \n  mv /dsiprouter /usr/local/src (moving directory)\n\nAlternatively:\n\n.. code-block:: bash\n\n  rm -r /dsiprouter (removing directory)\n\n\nInstalling dsiprouter v0.51\n\n.. code-block:: bash\n  \n  cd /opt/\n  apt-get update\n  apt-get install -y git curl\n  cd /opt\n  git clone -b v0.51 https://github.com/dOpensource/dsiprouter.git\n  cd dsiprouter\n  ./dsiprouter.sh install\n\n\n**Note: please take note of the credentials given after the script has completed.**\n\nAfter the install is completed you can now restore your kamailio database using the following command:\n\n.. code-block:: bash\n  \n  cd /opt/\n  mysql  kamailio < kamailio-bk.sql\n\n\nAfter the kamailio database is restored you need to restart dsiprouter using the following commands:\n\n.. code-block:: bash\n  \n  cd /opt/disprouter/\n  ./dsiprouter.sh restart \n\n\nAfter the install is complete and the dsiprouter service has been restarted, the login screen should now reflect v0.51 and you should be able to login with the dsiprouter credentials provided after the install completed.\n\n.. image:: images/dsip_v51.png\n        :align: center\n\n"
  },
  {
    "path": "docs/source/user/upgrade_0.522_to_0.523.rst",
    "content": "In this section we will show you how to upgrade from 0.522 to 0.523.\n\nBefore starting the upgrade process you will need to backup your kamailio database using the following command: \n\n.. code-block:: bash\n  \n  cd /opt/\n  mysqldump kamailio > kamailio-bk.sql\n\n\nAfter you've backed up your database you can now uninstall dsiprouter v0.50 by running the following commands: \n\n.. code-block:: bash\n\n  cd /opt/dsiprouter\n  ./dsiprouter.sh uninstall\n\n\nOnce the uninstall is complete you will need to either move or delete the /dsiprouter directory using the following command.\n\n.. code-block:: bash\n  \n  mv /dsiprouter /usr/local/src (moving directory)\n\nAlternatively:\n\n.. code-block:: bash\n\n  rm -r /dsiprouter (removing directory)\n\n\nInstalling dsiprouter v0.523\n\n.. code-block:: bash\n  \n  cd /opt/\n  apt-get update\n  apt-get install -y git curl\n  cd /opt\n  git clone -b v0.523 https://github.com/dOpensource/dsiprouter.git\n  cd dsiprouter\n  ./dsiprouter.sh install\n\n\n**Note: please take note of the credentials given after the script has completed.**\n\nAfter the install is completed you can now restore your kamailio database using the following command:\n\n.. code-block:: bash\n  \n  cd /opt/\n  mysql  kamailio < kamailio-bk.sql\n  mysql kamailio -e \"alter table dsip_multidomain_mapping add column domain_list_hash varchar(255) after domain_list;\"\n\n\nNow please restart dsiprouter using the following commands:\n\n.. code-block:: bash\n  \n  cd /opt/disprouter/\n  ./dsiprouter.sh restart \n\n\nAfter the install is complete and the dsiprouter service has been restarted, the login screen should now reflect v0.51 and you should be able to login with the dsiprouter credentials provided after the install completed.\n\n.. image:: images/dsip_v51.png\n        :align: center\n\n"
  },
  {
    "path": "docs/source/user/upgrade_0.621_to_0.63.rst",
    "content": "In this section we will show you how to upgrade from 0.621 to 0.63.  This is the first release\nto contain our new upgrade approach.  \n\nThe following steps will upgrade your Kamailio configuration from 0.621 to 0.63.  \n\n.. code-block:: bash\n  \n  cd /opt/dsiprouter\n  git stash\n  git checkout v0.63\n  dsiprouter upgrade -rel 0.63\n\nYou should now be able to login to dSIPRouter and see that the new release has been applied.  \n"
  },
  {
    "path": "docs/source/user/upgrading.rst",
    "content": ".. _upgrading:\n\nUpgrading dSIPRouter\n====================\n\nAuto Upgrade Feature\n--------------------\n\nThe dSIPRouter auto upgrade feature allows you to upgrade dSIPRouter from the User Interface (UI) and the command line (CLI).\nIf you are upgrading from 0.70, 0.72, or 0.721 you can boostrap to the latest release to get the auto-upgrade feature.\n\nUpgrading to 0.73 doesn't require a dSIPRouter Core Subscription license because the auto-upgrade framework was not yet feature complete.\nHowever, all subsequent releases of dSIPRouter require a Core Subscription License to use the auto-upgrade feature.\nA core license can be purchased from the `dSIPRouter Marketplace <https://dopensource.com/product/dsiprouter-core/>`_.\n\n.. image:: images/upgrade_up_to_date.png\n        :align: center\n\nNote that during upgrades, the hashed passwords have to be reset (such as dsiprouter admin password).\nYou can find the new passwords displayed; either on the CLI after completion, or by viewing the upgrade log in the UI:\n\n.. image:: images/upgrading-log-01.png\n        :align: center\n\n.. image:: images/upgrading-log-02.png\n        :align: center\n\nUpgrade to 0.78\n--------------------\n\nUpgrading from version 0.77 to 0.78 can be done using the auto upgrade feature via the UI or CLI.\n\nTo upgrade using the UI click the \"Upgrade Now\" button:\n\n.. image:: images/upgrade_available.png\n        :align: center\n\nOr by using the CLI:\n\n.. code-block:: bash\n\n   dsiprouter upgrade -rel 0.78\n\nUpgrade to 0.77\n--------------------\n\nUpgrading from version 0.76 to 0.77 can be done using the auto upgrade feature via the UI or CLI.\n\nTo upgrade using the UI click the \"Upgrade Now\" button:\n\n.. image:: images/upgrade_available.png\n        :align: center\n\nOr by using the CLI:\n\n.. code-block:: bash\n\n   dsiprouter upgrade -rel 0.77\n\nUpgrade to 0.76\n--------------------\n\nUpgrading from version 0.75 to 0.76 can be done using the auto upgrade feature via the UI or CLI.\n\nTo upgrade using the UI click the \"Upgrade Now\" button:\n\n.. image:: images/upgrade_available.png\n        :align: center\n\nOr by using the CLI:\n\n.. code-block:: bash\n\n   dsiprouter upgrade -rel 0.76\n\nUpgrade to 0.75\n---------------\n\nUpgrading to 0.75 can be done from a version greater than or equal to 0.72.\nUpgrading from version 0.73 or 0.74 can be done using the UI:\n\n.. image:: images/upgrade_available.png\n        :align: center\n\nOr by using the CLI:\n\n.. code-block:: bash\n\n   dsiprouter upgrade\n\nTo upgrade from 0.72 or 0.721 use the command below to bootstrap the upgrade:\n\n.. code-block:: bash\n\n   curl -s https://raw.githubusercontent.com/dOpensource/dsiprouter/v0.75/resources/upgrade/v0.75/scripts/bootstrap.sh | bash\n\nUpgrade 0.73 to 0.74\n--------------------\n\nUpgrading from version 0.73 to 0.74 can be done using the auto upgrade feature via the UI or CLI.\n\nUpgrade 0.72x to 0.73\n---------------------\n\nUpgrading to 0.73 can be done from 0.72 or 0.721 by doing the following\n\n1. SSH to your dSIPRouter Instance\n2. Run the following command\n\n.. code-block:: bash\n\n   curl -s https://raw.githubusercontent.com/dOpensource/dsiprouter/v0.73/resources/upgrade/v0.73/scripts/bootstrap.sh | bash\n\n3. Login to the dSIPRouter UI to validate that the upgrade was successful.\n\n**Note**, if you are upgrading from a debian 9 system you must first upgrade OS versions to a supported version.\nSee the `debian upgrade <https://wiki.debian.org/DebianUpgrade>`_ documentation for more information.\n\nNote, if the upgrade fails you can purchase a dSIPRouter Core Subscription from the `dSIPRouter Marketplace <https://dopensource.com/product/dsiprouter-core/>`_.\nThis will provide you with support hours so that we can help with the upgrade.\n\nUpgrade 0.70 to 0.721\n---------------------\n\nYou can upgrade from 0.70 by doing the following\n\n1. SSH to your dSIPRouter Instance\n2. Run the following command\n\n.. code-block:: bash\n\n   curl -s https://raw.githubusercontent.com/dOpensource/dsiprouter/v0.721/resources/upgrade/v0.721/scripts/bootstrap.sh | bash\n\n3. Login to the dSIPRouter UI to validate that the upgrade was successful.  \n\nNote, if the upgrade fails you can purchase a dSIPRouter Core Subscription which can be purchased from the `dSIPRouter Marketplace <https://dopensource.com/product/dsiprouter-core/>`_.\nThis will provide you with support hours so that we can help with the upgrade.\n\nUpgrade 0.70 to 0.72\n--------------------\n\nThis upgrade path is deprecated. Upgrade to the **0.721** release instead.\n\nUpgrade 0.644 to 0.70\n---------------------\n\nThere is no automated upgrade available from 0.644 to 0.70.\nSupport is available via a dSIPRouter Core Subscription which can be purchased from the `dSIPRouter Marketplace <https://dopensource.com/product/dsiprouter-core/>`_.This will provide you with support hours so that we can help with the upgrade.\n\nUpgrade 0.621 to 0.63\n---------------------\n\n.. include:: upgrade_0.621_to_0.63.rst\n\nUpgrade 0.522 to 0.523\n----------------------\n\n.. include:: upgrade_0.522_to_0.523.rst\n\nUpgrade 0.50 to 0.51\n--------------------\n\n.. include:: upgrade_0.50_to_0.51.rst\n"
  },
  {
    "path": "docs/source/user/use-cases.rst",
    "content": "Common Use Cases\n================\n\nThis section contains a list of the common use cases that are implemented using dSIPRouter\n\nSIP Trunking Using IP Authentication\n------------------------------------\n\ndSIPRouter enables an organization to start supporting SIP Trunking within minutes.\nHere are the steps to set it up using IP Authentication:\n\n1. Login to dSIPRouter\n2. Validate that your carrier is defined and specified in the Global Outbound Routes.  If not, please follow the steps in :ref:`carrier_groups`_ and/or :ref:`global_outbound_routes`_ documentation.\n\n3. Click on PBX's and Endpoints\n4. Click \"Add\" \n5. Select **IP Authentication** and fill in the fields specified below: \n\n- Friendly Name\n- IP Address of the PBX or Endpoint Device\n\n.. image:: images/sip_trunking_ip_auth.png\n    :align: center\n\n6. Click \"Add\"\n7. Click \"Reload\" to make the change active.\n\nSIP Trunking Using Username/Password Authentication \n---------------------------------------------------\n\nHere are the steps to set it up using Username/Password Authentication:\n\n1. Login to dSIPRouter\n2. Valiate that your carrier is defined and specified in the Global Outbound Routes.  If not, please follow the steps in `<carrier_groups.rst>`_ and/or `<global_outbound_routes>`_ documentation.\n3. Click on PBX's and Endpoints\n4. Click \"Add\"  \n5. Select  **Username/Password Authentication** and fill in the fields specified below: \n\n- Friendly Name\n- Click the \"Username/Password Auth\" radio button\n- Enter a username\n- Enter a domain. Note, you can make up the domain name.  If you don't specify one then the default domain will be used, which is sip.dsiprouter.org by default.\n- Enter a password\n\n.. image:: images/sip_trunking_credentials_auth.png\n    :align: center\n\n6. Click \"Add\"\n7. Click \"Reload\" to make the change active.\n\nUsing PJSIP Trunking  - FreePBX Example\n---------------------------------------\n\nThe following screenshot(s) shows how to configure a PJSIP trunk within FreePBX for Username/Password Authentication.  \n\nThe first screenshot shows the General tab of the  \"pjsip settings\" page:\n\n.. image:: images/sip_trunking_freepbx_pjsip_1.png\n        :align: center\n        \nThe following fields needs to be entered\n\n==================  ===============================================\nField               Value\n==================  ===============================================\nUsername            Username from dSIPRouter PBX Setup\nSecret\t\t        Password from dSIPRouter PBX Setup\nAuthentication      Outbound\nRegistration        Send\nSIP Server          Domain name defined in the dSIPRouter PBX Setup\nSIP Server          SIP port, which is 5060 in dSIPRouter\n==================  ===============================================\n\n.. image:: images/sip_trunking_freepbx_pjsip_2.png\n    :align: center\n\nThe following fields needs to be entered\n\n==================   =============================================================\nField                Value\n==================   =============================================================\nOutbound Proxy       IP address of dSIPRouter - must include the \"\\;lr\" at the end\nFrom Domain          The name of the domain defined in the dSIPRouter PBX Setup\n==================   =============================================================\n\nUsing chanSIP Trunking  - FreePBX Example\n-----------------------------------------\n\nThe following screenshot(s) shows how to configure a chanSIP trunk within FreePBX for Username/Password Authentication.\n\n1. Log into FreePBX server\n2. Click Connectivity→Trunks\n3. Select Add SIP (chan_sip) Trunk\n4. Under General tab enter \n  \nThe following fields needs to be entered\n\n==================   =====================================================================\nField                Value\n==================   =====================================================================\nTrunk Name           Labeled in dsiprouter\nOutbound Caller ID   Phone# that you want to appear during a outbound call (if applicable)\n==================   =====================================================================\n\n.. image:: images/sipchan_general.png\n    :align: center\n\n5. Next you will enter the configurations under the SIP Settings. Here you will enter the SIP settings for outgoing calls by selecting the **Outbound** tab. You will need the following information:\nThe following fields needs to be entered\n\n==================   =======================================\nField                Value\n==================   =======================================\nHost                 <host name or IP address of dsiprouter> \nUsername             <Specified in dsiprouter@domainname>\nSecret               <Specified in dsiprouter>\nType                 peer\nContext              from-trunk\n==================   =======================================\n\n**The domain name has to be included and correct.**\n\n.. image:: images/chansip_outgoing.png\n    :align: center\n\nNOTE:** Type <context=from-trunk> underneath the <type=peer> in the Peer Details box if it does not appear.\n\n6. Next you will enter the configurations for incoming by selecting the **Incoming** tab in the SIP Settings. Here you will enter the SIP settings for inbound calls. You will need:\n\nUser Context: This is most often the account name or number your provider expects. In this example we named it \"inbound\".\nThe following User Details needs to be entered:\n\n==================   =======================================\nField                Value\n==================   =======================================\nHost                 <host name or IP address of dsiprouter>\nInsecure             port,invite\nType                 peer\nContext              from-trunk\n==================   =======================================\n\n.. image:: images/chansip_incoming.png\n    :align: center\n\nIn the **Register String** enter: <username@domainname>:<password>@<ip address **or** hostname>. In this example it would be sipchantest@sip.dsiprouter.org:HFmx9u9N@demo.dsiprouter.org. **The domain name has to be included and correct.**\n\n.. image:: images/register_string.png\n    :align: center\n\n7. Click Submit\n\n8. Be sure to click the **Apply Config** button after submitting to confirm.\n\n.. image:: images/apply_config_button.png\n    :align: center\n\nYou will now be able to see the new chanSIP added in the truck.\t\n\n.. image:: images/add_trunk.png\n    :align: center\n\n9. Next you will need to setup an outbound route. Select Connectivity→ Outbound Routes. Click the “+” sign to add a outbound route. In this tab you will need to enter:\n\n=================================   ======================================\nField                               Value\n=================================   ======================================\nRoute Name                          Type desired name\nRoute CID                           Number you want to appear on caller ID\nTrunk Sequence for Matched Routes   Trunk name (select from drop down box)\n=================================   ======================================\n\n.. image:: images/outbound_routes_chansip.png\n    :align: center\n\n10. Click the Dial Patterns tab to set the dial patterns. \nIf you are familiar with dial patterns, you can enter the dial patterns manually or you can click the Dial Patterans Wizard to auto create dial patterns if you like. You can choose 7, 10 or 11 digit patterns. Click Generate Routes.\n\n.. image:: images/chansip_dial_wizard.png\n    :align: center\n\nDial pattern is set to your preference. Prefixes are optional, not required.\n\n.. image:: images/chansip_dial_pattern.png\n    :align: center\n\n11. Click Submit and Apply Config button.\n\nAssuming you already have an extention created in your FreePBX, you can validate incoming/outgoing calls by configuring a softphone or a hard phone. Below is an example of the information you would enter if you use a softphone: In this example we are using Zoiper. Once you’ve downloaded Zoiper application on your PC or smart device you would enter the following to configure the soft phone:\n\t\t\n==================  ==============================================\nField               Value\n==================  ==============================================\nUsername            <extension>@<siptrunkipaddress>\nsecret              <Password of that extension>\nHostname\t        <IP address of your FreePBX> (should autofill)\n==================  ==============================================\n\n**Note** Skip Authenication and Outbound Proxy\n\n.. image:: images/chansip_zoiper.png\n    :align: center\n\nYou should now be able to make a inbound and outbound call successfully!\n\nUsing SIP Trunking - FusionPBX IP Authenication\n-----------------------------------------------\n\nThe following screenshot(s) shows how to configure a SIP trunk within FusionPBX for IP Authenication.\n\n1. Log into your FusionPBX. \n2. Click Accounts --> Gateways-->Click the + sign to add a gateway/SIP Trunk. The only fields you will need to fill here are:\n    - Gateway= Name of the SIP Trunk\n    - Proxy= IP address of the SIP trunk\n    - Register= Change to False because you are using IP authenication\n\n.. image:: images/sip_trunking_fusionpbx.png\n    :align: center\n\n.. image:: images/sip_trunking_fusionpbx_2.png\n    :align: center\n\n3. Click Save\n4. Click DialPlan-->Outboung Routes-->Click the + sign to add a outbound route. Here you will enter in the following fields:\n    - Gateway= Name of the SIP Trunk\n    - Alternate gateways (if applicable)\n    - DialPlan Expression= 11d (standard setup in FusionPBX). To change the dialplan expression click on the dropdown box where it says \"Shortcut to create the outbound dialplan entries for this Gateway.\"\n    - Description= (if desired)\n5. Click Save\n\n.. image:: images/outbound-routes_fusionpbx.png\n    :align: center\n\n.. image:: images/outbound-routes_fusionpbx_2.png\n    :align: center\n\n**NOTE** To make these changes global for ALL domains for this SIP Trunk: reopen outbound routes and change the Domain to Global and the Context to ${domain_name} as shown below. \n\n.. image:: images/fusionpbx_global_dialplan.png\n    :align: center\n\nUsing SIP Trunking - FusionPBX Username/Password Authenication\n--------------------------------------------------------------\n\nThe following screenshot(s) shows how to configure a SIP trunk within FusionPBX for Username/Password Authenication with IP Authenication off.\n\n1. Log into your FusionPBX. \n2. Click Accounts --> Gateways-->Click the + sign to add a gateway/SIP Trunk. The following fields you will need to fill here are:\n    - Gateway= Name of the SIP Trunk\n    - Username= specified by dSIPRouter provider\n    - Password= specified by dSIPRouter provider\n    - From Domain= Specified or set by default\n    - Proxy= IP address of the SIP trunk\n    - Register= set to True because you are using Username/Password authenication.\n\n.. image:: images/sip_trunking_fusionpbx_3.png\n    :align: center\n\n.. image:: images/sip_trunking_fusionpbx_4.png\n    :align: center\n\n3. Click Save.\n4. Click DialPlan-->Outboung Routes-->Click the + sign to add a outbound route. Here you will enter in the following fields:\n    - Gateway= Name of the SIP Trunk\n    - Alternate gateways (if applicable)\n    - DialPlan Expression= 11d (standard setup in FusionPBX). To change the dialplan expression click on the dropdown box where it says \"Shortcut to create the outbound dialplan entries for this Gateway.\"\n    - Description= (if desired)\n\n.. image:: images/outbound-routes_fusionpbx.png\n    :align: center\n\n.. image:: images/outbound-routes_fusionpbx_2.png\n    :align: center\n\n5. Click Save\n\nFusionPBX Hosting\n-----------------\n\nHere we will demostrate how to setup dSIPRouter to enable hosting FusionPBX. We have built-in support for FusionPBX that allows domains to be dynamically pulled from FusionPBX.\n \n1. Login to dSIPRouter\n2. Click PBX(s) and EndPoints\n3. Click ADD; enter the following fields\n    - Friendly Name (opional)\n    - IP address\n    - IP Auth\n    - Click to enable FusionPBX Domain Support\n    - FusionPBX Database IP or Hostname\n4. Click ADD\n\n.. image:: images/fusionpbx_hosting.png\n    :align: center\n\n5. Click Reload Kamailio. (when changes are made reload button will change to orange)          \n\n.. image:: images/reload_button.png\n    :align: center\n\n6. Access your FusionPBX database via ssh.\n7. Run the command as illustrated in the \"Edit your PBX Detail\" window as root on the FusionPBX server. Replace <ip address> (not including the brackets) with the IP address of the dSIPRouter server you're adding. Command line will look simulair to the following picture.\n\n**NOTE** After you have entered the first two lines of commands you will not see a form of reply. If command is entered correctly it will return back to your root line. If the command line is incorrect you will receive a \"command not found\" error message. Recheck the command line and IP address.\n\n.. image:: images/fusionpbx_domain_support.png\n    :align: center\n\nAfter the command is run you should now be able to see the domains of that PBX in dSIPRouter.\n\n.. image:: images/list_of_domain.png\n    :align: center\n\nYou can test PBX Hosting is valid by configuring a softphone or a hard phone. Below is an example using a softphone: \n \nNow that domains have been synced in dSIPRouter you are able to register a softphone. In this example we are using Zoiper.\nOnce you've downloaded Zopier appliaction on your PC or smart device you would add:\n \n- username (extension@domainname)\n- password (password of that extension)\n- outbound proxy (IP address of the dSIPRouter)\n\n.. image:: images/zoiper_screenshot.png\n    :align: center\n\nProvisioning and Registering a Polycom VVX Phone\n------------------------------------------------\n\nNow that domains have been synced in dSIPRouter you are able to register a endpoint/hard-phone. In this example we are using a Polycom VVX410 desk phone.\n \n1. Log into your FusionPBX box\n    a) Update the \"outboundProxy.address\" of the template with the IP address or hostname of the dSIPRouter in the provisioning editor.\n\n.. image:: images/outbound_proxy.png\n    :align: center\n\n2. Assign the phone to a template.\n \n.. image:: images/assign_template.png\n    :align: center\n\n3. Configuring the Provisioning Server section of the phone. Enter the appropriate information into the fields.\n    a) Server Type (dSIPRouter uses HTTP/s by default)\n    b) Server Address (dSIPRouter Address*)\n    c) Server Username (device provisioning server name)\n    d) Server Password\n\n4. Click Save \n\n.. image:: images/provisioning_server.png\n    :align: center\n\n5. Reboot the phone\n\n* You will need to set http_domain_filter to false in FusionPBX.  You can change this value by navagating to \"Default Settings\", search for http_domain_filter, and set the value to false.  As shown below:\n\n.. image:: images/device_provisioning_http_domain_filter.png\n\n\nFreePBX Hosting - Pass Thru Authentication\n------------------------------------------\n\nHere we will demostrate how to setup dSIPRouter to enable hosting FreePBX using Pass Thru Authentication. FreePBX is designed to be a single tenant system or in other words, it was built to handle one SIP Domain.  So, we use dSIPRouter to define a SIP Domain and we pass thru Registration info to the FreePBX server so that you don't have to change how authentication is done.  However, this will only work for one FreePBX server.  If you have a cluster of FreePBX servers then use \"Local Subscriber Table\" authentication.  The value of having dSIPRouter in front of FreePBX is to provide you with flexibility.  After setting this up you will have the ability upgrade or migrate users from one FreePBX instance to another without having to take an outage.  The following video shows how to configure this.  The steps to implement this is below the video.\n\n.. raw:: html\n\n    <div style=\"position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; height: auto;\">\n        <iframe src=\"https://www.youtube.com/embed/OgTZLYYx1u8\" frameborder=\"0\" allowfullscreen style=\"position: absolute; top: 0; left: 0; width: 560px; height: 385px;\"></iframe>\n    </div>\n\nSteps to Implement\n++++++++++++++++++\n\n1. Click PBX and Endpoints\n2. Click Add\n\n.. image:: images/freepbx-pt-add-pbx.png\n    :align: center\n\n3. Reload Kamailio\n4. Click Domains\n5. Click Add\n\n.. image:: images/freepbx-pt-add-domain.png\n    :align: center\n\n6. Reload Kamailio\n7. Register a phone via dSIPRouter - notice that we used the hostname of dSIPRouter as the Outbound Proxy.  This forces the registration thru the proxy.\n\n.. image:: images/freepbx-pt-setup-softphone.png\n    :align: center\n\nMicrosoft Teams Direct Routing (SUBSCRIPTION REQUIRED)\n------------------------------------------------------\n\ndSIPRouter can act as an intermediary Session Border Controller between Microsoft Teams Direct Routing and your SIP provider or SIP servers.\n\nAn instance of dSIPRouter can either be a single tenant configuration (like sbc.example.com) or multi-tenant under a single wildcard subdomain (like *.sbc.example.com where * is the tenant's name).\n\n.. image:: images/direct-routing-sbcs.png\n    :align: center\n\nSteps to Implement\n++++++++++++++++++\n\n1. `Buy a license <https://dopensource.com/dsiprouter-annual-subscriptions/>`_  and follow the license installation instructions that are emailed to you.\n2. Add any carriers you need for inbound and outbound routing, define appropriate routes.\n3. Authorize your SBC's domain with Microsoft 365 by adding a TXT record starting with ms= per `Microsoft's documentation <https://docs.microsoft.com/en-us/microsoft-365/admin/setup/add-domain?view=o365-worldwide>`_.\n   Note: For multi-tenant use, authorizing the root subdomain or domain (if you use *.sbc.example.com, you would authorize sbc.example.com) should avoid the need to authorize each subdomain below this (like clientname.example.com)\n4. Create a global admin user with proper Teams licensing associated with the domain (or for multi-tenant both the root subdomain (eg: sbc.example.com) and client's domain (eg: client.sbc.example.com))\n5. Add the Teams session border controller in `Teams Admin Center <https://admin.teams.microsoft.com/direct-routing/v2>`_. Ensure the SIP port is correct (usually 5061) and the SBC is enabled!\n6. `Install PowerShell <https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux>`_ type pwsh then:\n\n.. code-block:: powershell\n\n\tInstall-Module -Name MicrosoftTeams\n\tImport-Module MicrosoftTeams\n\t$userCredential = Get-Credential\n\tConnect-MicrosoftTeams -Credential $userCredential\n\n*Login Note*:\n\nIf your using multi-factor authentication (MFA/2FA), log in by typing Connect-MicrosoftTeams\n\n*Debian 10 Note*:\n\nIf you run into `this OpenSSL issue <https://github.com/PowerShell/PowerShell/issues/12202>`_ , here is `a workaround <https://github.com/PowerShell/PowerShell/issues/12202#issuecomment-720402212>`_!\n**Replace sbc.example.com, user@example.com and +13137175555** with your SBC's FQDN, the user's email address and their phone number (with + then country code, use +1 if you are in the North American Numbering Plan)\n\n.. code-block:: powershell\n\n\tSet-CsOnlinePstnUsage -Identity Global -Usage @{Add=\"US and Canada\"}\n\tSet-CsOnlineVoiceRoute -Identity \"LocalRoute\" -NumberPattern \".*\" -OnlinePstnGatewayList sbc.example.com\n\tNew-CsOnlineVoiceRoutingPolicy \"US Only\" -OnlinePstnUsages \"US and Canada\"\n\n\t# This is suppose to stop MSTeams from using the Microsoft Dialing Plan and using the routing policies that was defined above\n\tSet-CsTenantHybridConfiguration -UseOnPremDialPlan $False\n\n\t# Apply and the US Only Voice Routing Policy to the user\n\tGrant-CsOnlineVoiceRoutingPolicy -Identity “user@example.com“ -PolicyName \"US Only\"\n\n\t# If it doesn’t return a value of US Only, then wait 15 minutes and try it again.  It sometime takes a while for the policy to be ready.\n\tGet-CsOnlineUser “user@example.com\" | select OnlineVoiceRoutingPolicy\n\n\t# Define a outgoing phone number (aka DID) and set Enterprise Voice and Voicemail\n\tSet-CsUser -Identity \"user@example.com\" -OnPremLineURI tel:+13137175555 -EnterpriseVoiceEnabled $true -HostedVoiceMail $true\n\n*Note*: Log out by typing ``Disconnect-MicrosoftTeams``\n\nCredits to Mack at dSIPRouter for the SkypeForBusiness script and `this blog post <https://seanmcavinue.net/2021/04/20/configure-teams-direct-routing-simple-deployment-via-powershell/>`_ for helping me update these commands for the new MicrosoftTeams PowerShell module.\n\nAdd a single Teams User\n+++++++++++++++++++++++\n\nIf you have an existing dSIPRouter SBC configured in Teams and have added a DID as an inbound route already, then run the commands below in PowerShell to add an additional user.\n\n**Replace user@example.com and +13137175555** with your SBC's FQDN, the user's email address and their phone number (with + then country code, use +1 if you are in the North American Numbering Plan)\n\n.. code-block:: powershell\n\n\t# Get Credentials, if using MFA/2FA just run Connect-MicrosoftTeams\n\t$userCredential = Get-Credential\n\tConnect-MicrosoftTeams -Credential $userCredential\n\n\t# Apply and the US Only Voice Routing Policy to the user\n\tGrant-CsOnlineVoiceRoutingPolicy -Identity “user@example.com“ -PolicyName \"US Only\"\n\n\t# Define a outgoing phone number (aka DID) and set Enterprise Voice and Voicemail\n\tSet-CsUser -Identity \"user@example.com\" -OnPremLineURI tel:+13137175555 -EnterpriseVoiceEnabled $true -HostedVoiceMail $true\n\n*Note*: Log out by typing ``Disconnect-MicrosoftTeams``\n\nConfigure STIR/SHAKEN (SUBSCRIPTION REQUIRED)\n---------------------------------------------\n\ndSIPRouter enables an organization to start signing calls by enabling the STIR/SHAKEN module.  This module will sign outbound calls and validate that inbound calls are signed.  It also have the ability to add a prefix to the callerid if calls have an attestion of an A, B or C.  You can also specify a callerid prefix if callers aren't validated.  Lastly, you have the option to block invalidated callers. \n\n1. Login to dSIPRouter\n2. Purchase a license from the `dSIPRouter Marketplace <https://dopensource.com/product-category/dsiprouter/>`_\n3. Click System Settings -> License Manager\n4. Add the license to the system\n5. If testing, connect to your dSIPRouter instance using ssh, run the command below and enter the requested information to create a self-signed certificate\n\n.. code-block:: bash\n\n\t/opt/dsiprouter/resources/stir_shaken/generate_self_signed_cert.sh\n\nIf not testing, obtain a valid STIR/SHAKEN certificate and place them in the /etc/dsiprouter/certs/stirshaken/ directory.  For the purpose of these instructions, please name the certificate sp-cert.pem and name the key sp-key.pem  \n\n6. Check that the certificate can be accessed via https.  Open a web browser and enter the following into the URL.  This will be used by other VoIP servers to validate the signature of the the call.  \n\n.. code-block:: bash\n\n\thttps://<replace with ip or hostname>:5000/stirshaken_certs/sp-cert.pem\n\n7. Click System Settings -> STIR/SHAKEN\n8. Slide the Disabled toggle to Enabled\n9. Enter the Certificate URL from Step 6\n10. Enter the Key Path, which by default will be \n\n.. code-block:: bash\n\n\t/etc/dsiprouter/certs/stirshaken/sp-key.pem\n\n11. Click Save\n\nThe STIR/SHAKEN page should look like this:\n\n.. image:: images/stir_shaken_settings.png\n    :align: center\n"
  },
  {
    "path": "dsiprouter/almalinux/8.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # Install dependencies for dSIPRouter\n    dnf install -y dnf-utils &&\n    dnf --setopt=group_package_types=mandatory,default,optional groupinstall -y \"Development Tools\" &&\n    dnf install -y firewalld sudo logrotate rsyslog perl \\\n        python3.11 python3.11-pip python3.11-libs python3.11-devel python3.11-PyMySQL \\\n        libev-devel util-linux postgresql-devel mariadb-devel openldap-devel\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # give dsiprouter permissions in SELINUX\n    semanage port -a -t http_port_t -p tcp ${DSIP_PORT} ||\n        semanage port -m -t http_port_t -p tcp ${DSIP_PORT}\n\n   # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n    systemctl enable nginx\n    systemctl restart nginx\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v2.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    # add hook to bash_completion in the standard debian location\n    echo '. /usr/share/bash-completion/bash_completion' > /etc/bash_completion\n}\n\n\nfunction uninstall {\n    dnf remove -y python36u\\*\n    dnf remove -y ius-release\n\n    # Remove the repos\n    rm -f /etc/dnf.repos.d/ius*\n    rm -f /etc/pki/rpm-gpg/IUS-COMMUNITY-GPG-KEY\n    dnf clean all\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter/almalinux/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    # Install dependencies for dSIPRouter\n    dnf install -y firewalld logrotate rsyslog perl curl python3 python3-devel libpq-devel \\\n        libev-devel openldap-devel &&\n    dnf install -y --enablerepo=crb mariadb-devel\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # give dsiprouter permissions in SELINUX\n    semanage port -a -t http_port_t -p tcp ${DSIP_PORT} ||\n    semanage port -m -t http_port_t -p tcp ${DSIP_PORT}\n\n   # Enable and start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v2.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    # add hook to bash_completion in the standard debian location\n    echo '. /usr/share/bash-completion/bash_completion' > /etc/bash_completion\n\n    return 0\n}\n\n\nfunction uninstall {\n    rm -rf ${PYTHON_VENV}\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter/amzn/2.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local NPROC=$(nproc)\n\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # Install dependencies for dSIPRouter\n    yum install -y yum-utils &&\n    yum groupinstall --setopt=group_package_types=mandatory,default -y 'Development Tools' &&\n    yum install -y firewalld logrotate rsyslog perl libev-devel util-linux postgresql-devel \\\n        bzip2-devel libffi-devel zlib-devel curl openldap-devel\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    ## compile and install openssl v1.1.1 (workaround for amazon linux repo conflicts)\n    ## we must overwrite system packages (openssl/openssl-devel) otherwise python's openssl package is not supported\n    if [[ \"$(openssl version 2>/dev/null | awk '{print $2}')\" != \"1.1.1w\" ]]; then\n        if [[ ! -d ${SRC_DIR}/openssl ]]; then\n            ( cd ${SRC_DIR} &&\n            curl -sL https://www.openssl.org/source/openssl-1.1.1w.tar.gz 2>/dev/null |\n            tar -xzf - --transform 's%openssl-1.1.1w%openssl%'; )\n        fi\n        (\n            cd ${SRC_DIR}/openssl &&\n            ./Configure --prefix=/usr linux-$(uname -m) &&\n            make -j $NPROC &&\n            make -j $NPROC install\n        ) || {\n            printerr 'Failed to compile openssl'\n            return 1\n        }\n    fi\n\n    # python 3.8 or higher is required\n    # if not installed already, install it now\n    if [[ \"$(python3 -V 2>/dev/null | cut -d ' ' -f 2)\" != \"3.9.18\" ]]; then\n        # installation / compilation never completed, start it now\n        if [[ ! -d \"${SRC_DIR}/Python-3.9.18\" ]]; then\n            (\n                cd ${SRC_DIR} &&\n                curl -s -o Python-3.9.18.tgz https://www.python.org/ftp/python/3.9.18/Python-3.9.18.tgz &&\n                tar -xf Python-3.9.18.tgz &&\n                rm -f Python-3.9.18.tgz\n            )\n        fi\n        (\n            cd ${SRC_DIR} &&\n            cd Python-3.9.18/ &&\n            ./configure --enable-optimizations CFLAGS=-I${SRC_DIR}/openssl/include LDFLAGS=-L${SRC_DIR}/openssl &&\n            make -j $NPROC &&\n            make -j $NPROC install\n        ) || {\n            printerr 'Failed to compile and install required python version'\n            return 1\n        }\n        python3 -m pip install -U pip setuptools || {\n            printerr 'Failed to update pip and setuptools'\n            return 1\n        }\n    fi\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # give dsiprouter permissions in SELINUX\n    semanage port -a -t http_port_t -p tcp ${DSIP_PORT} ||\n        semanage port -m -t http_port_t -p tcp ${DSIP_PORT}\n\n    # Start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    if (( $? != 0 )); then\n        # fix for bug: https://bugzilla.redhat.com/show_bug.cgi?id=1575845\n        systemctl restart dbus\n        systemctl restart firewalld\n        # fix for ensuing bug: https://bugzilla.redhat.com/show_bug.cgi?id=1372925\n        systemctl restart systemd-logind\n    fi\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v1.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    # add hook to bash_completion in the standard debian location\n    echo '. /usr/share/bash-completion/bash_completion' > /etc/bash_completion\n\n    return 0\n}\n\n\nfunction uninstall() {\n    rm -rf ${PYTHON_VENV}\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter/centos/7.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    local NPROC\n\n    # Install dependencies for dSIPRouter\n    yum install -y yum-utils &&\n    yum groupinstall -y \"Development Tools\" &&\n    yum install -y firewalld logrotate rsyslog perl libev-devel util-linux postgresql-devel \\\n        bzip2-devel libffi-devel zlib-devel curl openldap-devel\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    NPROC=$(nproc)\n\n    # python 3.8 or higher is required\n    # if not installed already, install it now\n    if [[ \"$(python3 -V 2>/dev/null | cut -d ' ' -f 2)\" != \"3.9.18\" ]]; then\n        # installation / compilation never completed, start it now\n        if [[ ! -d \"${SRC_DIR}/Python-3.9.18\" ]]; then\n            (\n                cd ${SRC_DIR} &&\n                curl -s -o Python-3.9.18.tgz https://www.python.org/ftp/python/3.9.18/Python-3.9.18.tgz &&\n                tar -xf Python-3.9.18.tgz &&\n                rm -f Python-3.9.18.tgz\n            )\n        fi\n        (\n            cd ${SRC_DIR} &&\n            cd Python-3.9.18/ &&\n            ./configure --enable-optimizations CFLAGS=-I${SRC_DIR}/openssl/include LDFLAGS=-L${SRC_DIR}/openssl &&\n            make -j $NPROC &&\n            make -j $NPROC install\n        ) || {\n            printerr 'Failed to compile and install required python version'\n            return 1\n        }\n        python3 -m pip install -U pip setuptools || {\n            printerr 'Failed to update pip and setuptools'\n            return 1\n        }\n    fi\n\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # give dsiprouter permissions in SELINUX\n    semanage port -a -t http_port_t -p tcp ${DSIP_PORT} ||\n        semanage port -m -t http_port_t -p tcp ${DSIP_PORT}\n\n   # Enable and start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v1.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    # add hook to bash_completion in the standard debian location\n    echo '. /usr/share/bash-completion/bash_completion' > /etc/bash_completion\n\n    return 0\n}\n\n\nfunction uninstall {\n    rm -rf ${PYTHON_VENV}\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter/centos/8.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    local NPROC\n\n    # Install dependencies for dSIPRouter\n    dnf install -y yum-utils &&\n    dnf groupinstall -y \"Development Tools\" &&\n    dnf install -y firewalld logrotate rsyslog perl libev-devel util-linux postgresql-devel \\\n        bzip2-devel libffi-devel zlib-devel curl openldap-devel\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    NPROC=$(nproc)\n\n    # python 3.8 or higher is required\n    # if not installed already, install it now\n    if [[ \"$(python3 -V 2>/dev/null | cut -d ' ' -f 2)\" != \"3.9.18\" ]]; then\n        # installation / compilation never completed, start it now\n        if [[ ! -d \"${SRC_DIR}/Python-3.9.18\" ]]; then\n            (\n                cd ${SRC_DIR} &&\n                curl -s -o Python-3.9.18.tgz https://www.python.org/ftp/python/3.9.18/Python-3.9.18.tgz &&\n                tar -xf Python-3.9.18.tgz &&\n                rm -f Python-3.9.18.tgz\n            )\n        fi\n        (\n            cd ${SRC_DIR} &&\n            cd Python-3.9.18/ &&\n            ./configure --enable-optimizations CFLAGS=-I${SRC_DIR}/openssl/include LDFLAGS=-L${SRC_DIR}/openssl &&\n            make -j $NPROC &&\n            make -j $NPROC install\n        ) || {\n            printerr 'Failed to compile and install required python version'\n            return 1\n        }\n        python3 -m pip install -U pip setuptools || {\n            printerr 'Failed to update pip and setuptools'\n            return 1\n        }\n    fi\n\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # give dsiprouter permissions in SELINUX\n    semanage port -a -t http_port_t -p tcp ${DSIP_PORT} ||\n        semanage port -m -t http_port_t -p tcp ${DSIP_PORT}\n\n   # Enable and start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v2.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    # add hook to bash_completion in the standard debian location\n    echo '. /usr/share/bash-completion/bash_completion' > /etc/bash_completion\n\n    return 0\n}\n\n\nfunction uninstall {\n    rm -rf ${PYTHON_VENV}\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter/centos/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    # Install dependencies for dSIPRouter\n    dnf install -y firewalld logrotate rsyslog perl curl python3 python3-devel libpq-devel \\\n        openldap-devel &&\n    dnf install -y --enablerepo=crb mariadb-devel libev-devel\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # give dsiprouter permissions in SELINUX\n    semanage port -a -t http_port_t -p tcp ${DSIP_PORT} ||\n    semanage port -m -t http_port_t -p tcp ${DSIP_PORT}\n\n   # Enable and start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v2.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    # add hook to bash_completion in the standard debian location\n    echo '. /usr/share/bash-completion/bash_completion' > /etc/bash_completion\n\n    return 0\n}\n\n\nfunction uninstall {\n    rm -rf ${PYTHON_VENV}\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter/debian/10.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local NPROC=$(nproc)\n\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # Install Dependencies and remove any conflicting packages\n    apt-get remove -y ufw &&\n    apt-get install -y build-essential curl pkg-config zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev \\\n        libsqlite3-dev libreadline-dev libffi-dev libbz2-dev libpq-dev logrotate rsyslog perl sngrep libev-dev \\\n        uuid-runtime tar &&\n    apt-get install -t bullseye -y firewalld python3 python3-venv python3-pip python3-dev libmariadb-dev &&\n    # Install libraries needed to install the python-ldap package\n    apt-get install -y libsasl2-dev libldap2-dev libssl-dev\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # TODO: figure out why compiling ultradict with the other deps hangs\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install UltraDict &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v2.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    return 0\n}\n\nfunction uninstall() {\n    rm -rf ${PYTHON_VENV}\n\n    # TODO: this is dangerous without dependency management (i.e. our own DEB)\n#    apt-get remove -y build-essential curl python3 python3-pip python-dev python3-openssl libpq-dev firewalld\n#    apt-get remove -y --allow-unauthenticated libmariadbclient-dev\n#    apt-get remove -y logrotate rsyslog perl sngrep libev-dev uuid-runtime\n    #apt-get remove -y build-essential curl python3 python3-pip python-dev libmariadbclient-dev libmariadb-client-lgpl-dev python-mysqldb libpq-dev firewalld\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter/debian/11.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # Install Dependencies and remove any conflicting packages\n    apt-get remove -y ufw &&\n    apt-get install -y build-essential curl python3 python3-pip python3-dev python3-venv \\\n        libpq-dev firewalld sudo logrotate rsyslog perl sngrep libev-dev uuid-runtime \\\n        libmariadb-dev pkg-config &&\n    # Install libraries needed to install the python-ldap package\n    apt-get install -y libsasl2-dev python-dev-is-python3 libldap2-dev libssl-dev\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # TODO: figure out why compiling ultradict with the other deps hangs\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install UltraDict &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v2.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    return 0\n}\n\nfunction uninstall() {\n    rm -rf ${PYTHON_VENV}\n\n    # TODO: this is dangerous without dependency management (i.e. our own DEB)\n#    apt-get remove -y build-essential curl python3 python3-pip python-dev python3-openssl libpq-dev firewalld\n#    apt-get remove -y --allow-unauthenticated libmariadbclient-dev\n#    apt-get remove -y logrotate rsyslog perl sngrep libev-dev uuid-runtime\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter/debian/12.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # Install Dependencies and remove any conflicting packages\n    apt-get remove -y ufw &&\n    apt-get install -y build-essential pkg-config python3-pip \\\n        python3-dev libpq-dev python3-venv libev-dev libffi-dev libmariadb-dev \\\n        curl python3 firewalld sudo logrotate rsyslog perl sngrep uuid-runtime &&\n    # Install libraries needed to install the python-ldap package\n    apt-get install -y libsasl2-dev python-dev-is-python3 libldap2-dev libssl-dev\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # TODO: figure out why compiling ultradict with the other deps hangs\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install UltraDict &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v2.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    return 0\n}\n\nfunction uninstall() {\n    rm -rf ${PYTHON_VENV}\n\n    # TODO: this is dangerous without dependency management (i.e. our own DEB)\n#    apt-get remove -y build-essential curl python3 python3-pip python-dev python3-openssl libpq-dev firewalld\n#    apt-get remove -y --allow-unauthenticated libmariadbclient-dev\n#    apt-get remove -y logrotate rsyslog perl sngrep libev-dev uuid-runtime\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter/debian/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # Install Dependencies and remove any conflicting packages\n    apt-get remove -y ufw &&\n    apt-get install -y build-essential curl python3 python3-pip python-dev python3-openssl \\\n        python3-venv libpq-dev firewalld sudo\n    apt-get install -y --allow-unauthenticated libmariadbclient-dev &&\n    apt-get install -y logrotate rsyslog perl sngrep libev-dev uuid-runtime libpq-dev\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # TODO: figure out why compiling ultradict with the other deps hangs\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install UltraDict &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v2.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    return 0\n}\n\nfunction uninstall() {\n    # Uninstall dependencies for dSIPRouter\n    cat ${DSIP_PROJECT_DIR}/gui/requirements.txt | xargs -n 1 $PYTHON_CMD -m pip uninstall --yes\n    if (( $? == 1 )); then\n        printerr \"dSIPRouter uninstall failed or the libraries are already uninstalled\"\n        exit 1\n    else\n        printdbg \"DSIPRouter uninstall was successful\"\n        exit 0\n    fi\n\n    apt-get remove -y build-essential curl python3 python3-pip python-dev python3-openssl libpq-dev firewalld\n    apt-get remove -y --allow-unauthenticated libmariadbclient-dev\n    apt-get remove -y logrotate rsyslog perl sngrep libev-dev uuid-runtime\n    #apt-get remove -y build-essential curl python3 python3-pip python-dev libmariadbclient-dev libmariadb-client-lgpl-dev python-mysqldb libpq-dev firewalld\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter/dsip-net-cfg.py",
    "content": "#!/usr/bin/env python3\n#\n# Handle any network configurations that need setup prior to the dSIPRouter services starting\n# Usage: ./dsip-net-cfg.py [cloud platform] [network mode]\n#\n\n# make sure the generated source files are imported instead of the template ones\nimport sys\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nimport requests, subprocess, json\nimport settings\n\n# get variables either from CLI args or settings file\nargs = sys.argv[1:]\ntry:\n    cloud_platform = args[0]\nexcept IndexError:\n    cloud_platform = settings.CLOUD_PLATFORM\ntry:\n    network_mode = args[1]\n    network_mode = int(network_mode)\nexcept IndexError:\n    network_mode = settings.NETWORK_MODE\n\n#===============================================================================================\n# Use Case 1\n#===============================================================================================\n# set floating IP as default route if available\n# TODO: support more cloud providers\n#===============================================================================================\nif cloud_platform == 'DO' and network_mode == 0:\n    # NOTE: we are using ipv4\n    def hasFloatingIp():\n        resp = requests.get(\n            'http://169.254.169.254/metadata/v1/floating_ip/ipv4/active'\n        ).text\n        if resp == 'true':\n            return True\n        return False\n\n    def getAnchorGateway():\n        return requests.get(\n            'http://169.254.169.254/metadata/v1/interfaces/public/0/anchor_ipv4/gateway'\n        ).text\n\n    def getAnchorIpAddr():\n        return requests.get(\n            'http://169.254.169.254/metadata/v1/interfaces/public/0/anchor_ipv4/address'\n        ).text\n\n    def ipAddrToIface(ip_addr):\n        result = subprocess.run(\n            ['ip', '-4', '-json', 'address', 'show'],\n            capture_output=True,\n            text=True\n        )\n        result_json = json.loads(result.stdout.strip())\n        for iface_info in result_json:\n            for addr_info in iface_info['addr_info']:\n                if addr_info['local'] == ip_addr:\n                    return iface_info['ifname']\n        raise Exception(f'No matching Iface for IP {ip_addr}')\n\n    def getDefRoutes():\n        result = subprocess.run(\n            ['ip', '-4', 'route', 'list', 'scope', 'global'],\n            capture_output=True,\n            text=True\n        )\n        routes = result.stdout.strip().split('\\n')\n        def_routes = []\n        for route in routes:\n            route_args = route.split(' ')\n            if route_args[0] in ('default', '0.0.0.0/0'):\n                def_routes.append(route_args)\n        return def_routes\n\n    def isIpDefRoute(check_ip):\n        result = subprocess.run(\n            ['ip', '-4', 'route', 'list', 'scope', 'global'],\n            capture_output=True,\n            text=True\n        )\n        route = result.stdout.strip().split('\\n')[0]\n        found = False\n        for route_arg in route.split(' '):\n            if found and route_arg == check_ip:\n                return True\n            if route_arg == 'via':\n                found = True\n        return False\n\n    if hasFloatingIp():\n        anchor_gw = getAnchorGateway()\n        anchor_ip = getAnchorIpAddr()\n        if not isIpDefRoute(anchor_gw):\n            anchor_iface = ipAddrToIface(anchor_ip)\n            def_routes = getDefRoutes()\n\n            for route_args in def_routes:\n                subprocess.run(\n                    ['ip', 'route', 'delete', *route_args],\n                    stdout=subprocess.DEVNULL\n                )\n            subprocess.run(\n                ['ip', 'route', 'add', 'default', 'via', anchor_gw, 'dev', anchor_iface],\n                stdout=subprocess.DEVNULL\n            )\n\n#===============================================================================================\n# Default Case\n#===============================================================================================\n# do nothing\n#===============================================================================================\nexit(0)\n"
  },
  {
    "path": "dsiprouter/dsip_completion.sh",
    "content": "#!/usr/bin/env bash\n\n#####################################\n# dsiprouter command completion\n#####################################\n\n_dsiprouter() {\n    COMPREPLY=( )\n    local cur=\"${COMP_WORDS[$COMP_CWORD]}\"\n    local prev=\"${COMP_WORDS[$((COMP_CWORD-1))]}\"\n    local cmd=\"${COMP_WORDS[1]}\"\n\n    # available commands for dsiprouter <cmd>\n    declare -a cmds=(\n        install\n        uninstall\n        clusterinstall\n        upgrade\n        start\n        stop\n        restart\n        chown\n        configurekam\n        configuredsip\n        configurertp\n        renewsslcert\n\t    configuresslcert\n        installmodules\n        resetpassword\n        setcredentials\n        licensemanager\n        backup\n        restore\n        help\n        -h\n        --help\n        version\n        -v\n        --version\n    )\n    # available long options (with value) for each cmd\n    declare -A llopts=(\n        [install]='--database= --dsip-clusterid= --database-admin= --dsip-clustersync= --dsip-privkey= --with_lcr= --with_dev= --dmz= --network-mode='\n        [uninstall]=''\n        [clusterinstall]=''\n        [upgrade]='--dsip-clusterid= --release= --repo-url='\n        [start]=''\n        [stop]=''\n        [restart]=''\n        [chown]=''\n        [configurekam]=''\n        [configuredsip]=''\n        [configurertp]=''\n        [renewsslcert]=''\n        [configuresslcert]=''\n        [installmodules]=''\n        [resetpassword]=''\n        [setcredentials]='--dsip-creds= --api-creds= --kam-creds= --mail-creds= --ipc-creds= --db-admin-creds= --session-creds='\n        [licensemanager]=''\n        [backup]=''\n        [restore]=''\n        [help]=''\n        [-h]=''\n        [--help]=''\n        [version]=''\n        [-v]=''\n        [--version]=''\n    )\n    # available long options (without value) for each cmd\n    declare -A lopts=(\n        [install]='--all --kamailio --dsiprouter --rtpengine --dnsmasq'\n        [uninstall]='--all --kamailio --dsiprouter --rtpengine'\n        [clusterinstall]='--'\n        [upgrade]=''\n        [start]='--all --kamailio --dsiprouter --rtpengine'\n        [stop]='--all --kamailio --dsiprouter --rtpengine'\n        [restart]='--all --kamailio --dsiprouter --rtpengine'\n        [chown]=''\n        [configurekam]=''\n        [configuredsip]=''\n        [configurertp]=''\n        [renewsslcert]=''\n        [configuresslcert]='--force'\n        [installmodules]=''\n        [resetpassword]='--all --dsip-creds --api-creds --kam-creds --ipc-creds --force-instance-id'\n        [setcredentials]=''\n        [licensemanager]=''\n        [backup]=''\n        [restore]=''\n        [help]=''\n        [-h]=''\n        [--help]=''\n        [version]=''\n        [-v]=''\n        [--version]=''\n    )\n    # available short options (with or without value) for each cmd\n    declare -A sopts=(\n        [install]='-debug -all -kam -dsip -rtp -dns -db -dsipcid -dbadmin -dsipcsync -dsipkey -with_lcr -with_dev -dmz -netm -homer'\n        [uninstall]='-debug -all -kam -dsip -rtp'\n        [clusterinstall]='-debug -i'\n        [upgrade]='-debug -dsipcid -rel -url'\n        [start]='-debug -all -kam -dsip -rtp'\n        [stop]='-debug -all -kam -dsip -rtp'\n        [restart]='-debug -all -kam -dsip -rtp'\n        [chown]='-debug -certs -dnsmasq -nginx -kamailio -dsiprouter -rtpengine'\n        [configurekam]='-debug'\n        [configuredsip]='-debug'\n        [configurertp]='-debug'\n        [renewsslcert]='-debug'\n        [configuresslcert]='-debug -f'\n        [installmodules]='-debug'\n        [resetpassword]='-debug -q -all -dc -ac -kc -ic -fid'\n        [setcredentials]='-debug --dc -ac -kc -mc -ic -dac -sc'\n        [licensemanager]='-debug -retrieve -list -activate -import -deactivate -clear -check'\n        [backup]='-debug -f'\n        [restore]='-debug -f'\n        [help]=''\n        [-h]=''\n        [--help]=''\n        [version]=''\n        [-v]=''\n        [--version]=''\n    )\n\n    # determine command being completed and generate possible values\n    if [[ $({ for x in ${cmds[*]}; do [[ \"$x\" == \"$cmd\" ]] && echo \"yes\"; done; }) == \"yes\" ]]; then\n        # special use cases\n        if [[ \"${cmd}\" == \"clusterinstall\" && \"${prev}\" == \"--\" ]]; then\n            cmd=\"install\"\n        fi\n\n        # normal opt matching\n        case \"$cur\" in\n            --*)\n                COMPREPLY=( $(compgen -W \"${lopts[$cmd]}\" -- ${cur}) $(compgen -o nospace -W \"${llopts[$cmd]}\" -- ${cur}) )\n                ;;\n            -*)\n                COMPREPLY=( $(compgen -W \"${sopts[$cmd]}\" -- ${cur}) )\n                ;;\n        esac\n    else\n        COMPREPLY=( $(compgen -W \"${cmds[*]}\" -- ${cur}) )\n    fi\n\n    return 0\n}\ncomplete -F _dsiprouter dsiprouter\n"
  },
  {
    "path": "dsiprouter/dsip_lib.sh",
    "content": "# NOTES:\n# contains utility functions and shared variables\n# should be sourced by an external script\n# exporting upon import removes need to import again in sub-processes\n\n######################\n# Imported Constants #\n######################\n\n# Ansi Colors\nexport ESC_SEQ=\"\\033[\"\nexport ANSI_NONE=\"${ESC_SEQ}39;49;00m\" # Reset colors\nexport ANSI_RED=\"${ESC_SEQ}1;31m\"\nexport ANSI_GREEN=\"${ESC_SEQ}1;32m\"\nexport ANSI_YELLOW=\"${ESC_SEQ}1;33m\"\nexport ANSI_CYAN=\"${ESC_SEQ}1;36m\"\nexport ANSI_WHITE=\"${ESC_SEQ}1;37m\"\n\n# public IP's us for testing / DNS lookups in scripts\nexport GOOGLE_DNS_IPV4=\"8.8.8.8\"\nexport GOOGLE_DNS_IPV6=\"2001:4860:4860::8888\"\n\n# Constants for imported functions\nexport DSIP_INIT_FILE=${DSIP_INIT_FILE:-\"/lib/systemd/system/dsip-init.service\"}\nexport DSIP_SYSTEM_CONFIG_DIR=${DSIP_SYSTEM_CONFIG_DIR:-\"/etc/dsiprouter\"}\nexport DSIP_PROJECT_DIR=${DSIP_PROJECT_DIR:-$(dirname $(dirname $(readlink -f \"$BASH_SOURCE\")))}\n\n# reuse credential settings from python files (exported for later usage)\nexport SALT_LEN=${SALT_LEN:-$(grep -m 1 -oP 'SALT_LEN[ \\t]+=[ \\t]+\\K[0-9]+' ${DSIP_PROJECT_DIR}/gui/util/security.py)}\nexport DK_LEN_DEFAULT=${DK_LEN_DEFAULT:-$(grep -m 1 -oP 'DK_LEN_DEFAULT[ \\t]+=[ \\t]+\\K[0-9]+' ${DSIP_PROJECT_DIR}/gui/util/security.py)}\nexport CREDS_MAX_LEN=${CREDS_MAX_LEN:-$(grep -m 1 -oP 'CREDS_MAX_LEN[ \\t]+=[ \\t]+\\K[0-9]+' ${DSIP_PROJECT_DIR}/gui/util/security.py)}\nexport HASH_ITERATIONS=${HASH_ITERATIONS:-$(grep -m 1 -oP 'HASH_ITERATIONS[ \\t]+=[ \\t]+\\K[0-9]+' ${DSIP_PROJECT_DIR}/gui/util/security.py)}\nexport HASHED_CREDS_ENCODED_MAX_LEN=${HASHED_CREDS_ENCODED_MAX_LEN:-$(grep -m 1 -oP 'HASHED_CREDS_ENCODED_MAX_LEN[ \\t]+=[ \\t]+\\K[0-9]+' ${DSIP_PROJECT_DIR}/gui/util/security.py)}\nexport AESCTR_CREDS_ENCODED_MAX_LEN=${AESCTR_CREDS_ENCODED_MAX_LEN:-$(grep -m 1 -oP 'AESCTR_CREDS_ENCODED_MAX_LEN[ \\t]+=[ \\t]+\\K[0-9]+' ${DSIP_PROJECT_DIR}/gui/util/security.py)}\nexport AES_CTR_NONCE_SIZE=${AES_CTR_NONCE_SIZE:-$(grep -m 1 -oP 'NONCE_SIZE[ \\t]+=[ \\t]+\\K[0-9]+' ${DSIP_PROJECT_DIR}/gui/util/security.py)}\nexport AES_CTR_KEY_SIZE=${AES_CTR_KEY_SIZE:-$(grep -m 1 -oP 'KEY_SIZE[ \\t]+=[ \\t]+\\K[0-9]+' ${DSIP_PROJECT_DIR}/gui/util/security.py)}\n\n# Flag denoting that these functions have been imported (verifiable in sub-processes)\nexport DSIP_LIB_IMPORTED=1\n\n##############################################\n# Printing functions and String Manipulation #\n##############################################\n\n# checks if stdin is null and sets STDIN_FIRST_BYTE to first character of stdin\nfunction isStdinNull() {\n    local c\n    read -r -d '' c\n}\n\nfunction printbold() {\n    if [[ \"$1\" == \"-n\" ]]; then\n        shift; printf \"%b%s%b\" \"${ANSI_WHITE}\" \"$*\" \"${ANSI_NONE}\"\n    else\n        printf \"%b%s%b\\n\" \"${ANSI_WHITE}\" \"$*\" \"${ANSI_NONE}\"\n    fi\n}\nexport -f printbold\n\nfunction printerr() {\n    if [[ \"$1\" == \"-n\" ]]; then\n        shift; printf \"%b%s%b\" \"${ANSI_RED}\" \"$*\" \"${ANSI_NONE}\"\n    else\n        printf \"%b%s%b\\n\" \"${ANSI_RED}\" \"$*\" \"${ANSI_NONE}\"\n    fi\n}\nexport -f printerr\n\nfunction printwarn() {\n    if [[ \"$1\" == \"-n\" ]]; then\n        shift; printf \"%b%s%b\" \"${ANSI_YELLOW}\" \"$*\" \"${ANSI_NONE}\"\n    else\n        printf \"%b%s%b\\n\" \"${ANSI_YELLOW}\" \"$*\" \"${ANSI_NONE}\"\n    fi\n}\nexport -f printwarn\n\nfunction printdbg() {\n    if [[ \"$1\" == \"-n\" ]]; then\n        shift; printf \"%b%s%b\" \"${ANSI_GREEN}\" \"$*\" \"${ANSI_NONE}\"\n    else\n        printf \"%b%s%b\\n\" \"${ANSI_GREEN}\" \"$*\" \"${ANSI_NONE}\"\n    fi\n}\nexport -f printdbg\n\nfunction pprint() {\n    if [[ \"$1\" == \"-n\" ]]; then\n        shift; printf \"%b%s%b\" \"${ANSI_CYAN}\" \"$*\" \"${ANSI_NONE}\"\n    else\n        printf \"%b%s%b\\n\" \"${ANSI_CYAN}\" \"$*\" \"${ANSI_NONE}\"\n    fi\n}\nexport -f pprint\n\nfunction tolower() {\n    [[ -p /dev/stdin ]] &&\n    (\n        read -r -d '' INPUT\n        [[ -z \"$INPUT\" ]] && exit 1\n        tr '[ABCDEFGHIJKLMNOPQRSTUVWXYZ]' '[abcdefghijklmnopqrstuvwxyz]' <<<\"$INPUT\"\n        exit 0\n    ) ||\n    {\n        printf '%s' \"$1\" | tr '[ABCDEFGHIJKLMNOPQRSTUVWXYZ]' '[abcdefghijklmnopqrstuvwxyz]'\n    }\n}\nexport -f tolower\n\nfunction toupper() {\n    [[ -p /dev/stdin ]] &&\n    (\n        read -r -d '' INPUT\n        [[ -z \"$INPUT\" ]] && exit 1\n        tr '[abcdefghijklmnopqrstuvwxyz]' '[ABCDEFGHIJKLMNOPQRSTUVWXYZ]' <<<\"$INPUT\"\n        exit 0\n    ) ||\n    {\n        printf '%s' \"$1\" | tr '[abcdefghijklmnopqrstuvwxyz]' '[ABCDEFGHIJKLMNOPQRSTUVWXYZ]'\n    }\n}\nexport -f toupper\n\nfunction hextoint() {\n    [[ -p /dev/stdin ]] &&\n    (\n        read -r -d '' INPUT\n        [[ -z \"$INPUT\" ]] && exit 1\n        printf '%d' \"0x$INPUT\" 2>/dev/null\n        exit 0\n    ) ||\n    {\n        printf '%d' \"0x$1\" 2>/dev/null\n    }\n}\nexport -f hextoint\n\n######################################\n# Traceback / Debug helper functions #\n######################################\n\nfunction backtrace() {\n    local DEPTN=${#FUNCNAME[@]}\n\n    for ((i=1; i < ${DEPTN}; i++)); do\n        local FUNC=\"${FUNCNAME[$i]}\"\n        local LINE=\"${BASH_LINENO[$((i-1))]}\"\n        local SRC=\"${BASH_SOURCE[$((i-1))]}\"\n        printf '%*s' $i '' # indent\n        printerr \"[ERROR]: ${FUNC}(), ${SRC}, line: ${LINE}\"\n    done\n}\nexport -f backtrace\n\nfunction setErrorTracing() {\n    set -o errtrace\n    trap 'backtrace' ERR\n}\nexport -f setErrorTracing\n\n#######################################\n# Reusable / Shared Utility functions #\n#######################################\n\n# TODO: we need to change the config getter/setter functions to use options parsing:\n# - when the value to set variable to is the empty string our functions error out\n# - ordering of filename and other options can be easily mistaken, which can set wrong values in config\n# - input validation would also be much easier if we switched added option parsing\n\n# $1 == attribute name\n# $2 == attribute value\n# $3 == python config file\n# $4 == -q (quote string) | -qb (quote byte string)\nfunction setConfigAttrib() {\n    local NAME=\"$1\"\n    local VALUE=\"$2\"\n    local CONFIG_FILE=\"$3\"\n\n    if (( $# >= 4 )); then\n        if [[ \"$4\" == \"-q\" ]]; then\n            VALUE=\"'${VALUE}'\"\n        elif [[ \"$4\" == \"-qb\" ]]; then\n            VALUE=\"b'${VALUE}'\"\n        fi\n    fi\n    sed -i -r -e \"s|^$NAME[ \\t]*=[ \\t]*.*|$NAME = $VALUE|g\" ${CONFIG_FILE}\n}\nexport -f setConfigAttrib\n\n# $1 == credentials to encrypt\n# usage:\n#   encryptCreds [options] <plaintext credentials>\n#   echo 'plaintext credentials' | encryptCreds [options]\n# options:\n#   -pk <private key string>\n#   -kf <private key file>\n# notes:\n#   one of \"-pk\" or \"-kf\" options must be given\n# returns:\n#   0 == successfully encrypted credentials\n#   1 == failed encrypting credentials\n# outputs:\n#   <nonce>:<ciphertext credentials>\nfunction encryptCreds() {\n    local TMP PT_CREDS NONCE CT_HEX PK_HEX\n\n    while (( $# > 0 )); do\n        case \"$1\" in\n            -pk)\n                shift\n                PK_HEX=$(xxd -p -l $AES_CTR_KEY_SIZE -c $AES_CTR_KEY_SIZE <<<\"$1\")\n                shift\n                ;;\n            -kf)\n                shift\n                PK_HEX=$(xxd -p -l $AES_CTR_KEY_SIZE -c $AES_CTR_KEY_SIZE <\"$1\")\n                shift\n                ;;\n            *)\n                PT_CREDS=\"$1\"\n                shift\n                ;;\n        esac\n    done\n\n    [[ -z \"$PK_HEX\" ]] && return 1\n\n    # if credentials were piped to stdin use that instead of positional args\n    if [[ -p /dev/stdin ]]; then\n        read -r -d '' TMP\n        [[ -n \"$TMP\" ]] && PT_CREDS=\"$TMP\"\n    fi\n\n    # openssl version - depends on openssl, xxd, and sed\n    NONCE=$(openssl rand -hex $AES_CTR_NONCE_SIZE) &&\n    CT_HEX=$(\n        set -o pipefail;\n        openssl enc -aes-256-ctr -e \\\n            -iv \"$NONCE\" \\\n            -K \"$PK_HEX\" \\\n            < <(echo -n \"$PT_CREDS\") \\\n            2>/dev/null |\n        xxd -p -c $AESCTR_CREDS_ENCODED_MAX_LEN\n    )\n\n    (( $? != 0 )) && return 1\n\n    echo -n \"${NONCE}${CT_HEX}\"\n    return 0\n}\nexport -f encryptCreds\n\n# $1 == attribute name\n# $2 == attribute value\n# $3 == python config file\nfunction encryptConfigAttrib() {\n    local NAME=\"$1\"\n    local VALUE=\"$2\"\n    local CONFIG_FILE=\"$3\"\n    local CT_CREDS\n    local PK_FILE=$(getConfigAttrib 'DSIP_PRIV_KEY' \"$CONFIG_FILE\")\n\n    # openssl version - depends on openssl, xxd, and sed\n    CT_CREDS=$(encryptCreds -kf \"$PK_FILE\" \"$VALUE\")\n    setConfigAttrib \"$NAME\" \"$CT_CREDS\" \"$CONFIG_FILE\" -qb\n    # python version - depends on dsiprouter's python3 venv and pycryptodome\n#    ${PYTHON_CMD} <<EOPY\n#import os, sys\n#os.chdir('${DSIP_PROJECT_DIR}/gui')\n#sys.path.insert(0, '${DSIP_SYSTEM_CONFIG_DIR}/gui')\n#import settings\n#from shared import updateConfig\n#from util.security import AES_CTR\n#updateConfig(settings, {'$NAME': AES_CTR.encrypt('$VALUE')})\n#EOPY\n}\nexport -f encryptConfigAttrib\n\n# $1 == attribute name\n# $2 == python config file\n# output: attribute value\nfunction getConfigAttrib() {\n    local NAME=\"$1\"\n    local CONFIG_FILE=\"$2\"\n\n    local VALUE=$(grep -oP '^(?!#)(?:'${NAME}')[ \\t]*=[ \\t]*\\K(?:\\w+\\(.*\\)[ \\t\\v]*$|[\\w\\d\\.]+[ \\t]*$|\\{.*\\}|\\[.*\\][ \\t]*$|\\(.*\\)[ \\t]*$|b?\"\"\".*\"\"\"[ \\t]*$|'\"b?'''.*'''\"'[ \\v]*$|b?\".*\"[ \\t]*$|'\"b?'.*'\"')' ${CONFIG_FILE})\n    printf '%s' \"${VALUE}\" | perl -0777 -pe 's~^b?[\"'\"'\"']+(.*?)[\"'\"'\"']+$|(.*)~\\1\\2~g'\n}\nexport -f getConfigAttrib\n\n# $1 == attribute name\n# $2 == python config file\n# output: attribute value decrypted\n# notes: if value is not encrypted the value is output instead\nfunction decryptConfigAttrib() {\n    local NAME=\"$1\"\n    local CONFIG_FILE=\"$2\"\n    # this is the file not the raw key\n    local PK_FILE=$(getConfigAttrib 'DSIP_PRIV_KEY' \"$CONFIG_FILE\")\n    local NONCE NONCE_OFFSET\n\n    local VALUE=$(grep -oP '^(?!#)(?:'${NAME}')[ \\t]*=[ \\t]*\\K(?:\\w+\\(.*\\)[ \\t\\v]*$|[\\w\\d\\.]+[ \\t]*$|\\{.*\\}|\\[.*\\][ \\t]*$|\\(.*\\)[ \\t]*$|b?\"\"\".*\"\"\"[ \\t]*$|'\"b?'''.*'''\"'[ \\v]*$|b?\".*\"[ \\t]*$|'\"b?'.*'\"')' ${CONFIG_FILE})\n    # if value is not a byte literal it isn't encrypted\n    if ! printf '%s' \"${VALUE}\" | grep -q -oP '(b\"\"\".*\"\"\"|'\"b'''.*'''\"'|b\".*\"|'\"b'.*')\"; then\n        printf '%s' \"${VALUE}\" | perl -0777 -pe 's~^b?[\"'\"'\"']+(.*?)[\"'\"'\"']+$|(.*)~\\1\\2~g'\n    else\n        VALUE=$(perl -pe 's%b\"\"\"(.*)\"\"\"|'\"b'''(.*)'''\"'|b\"(.*)\"|'\"b'(.*)'\"'%\\1\\2\\3\\4%' <<<\"$VALUE\")\n        # openssl version - depends on openssl and xxd\n        NONCE_OFFSET=$((AES_CTR_NONCE_SIZE * 2))\n        NONCE=${VALUE:0:$NONCE_OFFSET}\n        openssl enc -aes-256-ctr -d \\\n            -iv \"$NONCE\" \\\n            -K \"$(xxd -p -l $AES_CTR_KEY_SIZE -c $AES_CTR_KEY_SIZE <\"${PK_FILE}\")\" \\\n            < <(echo -n \"${VALUE:$NONCE_OFFSET}\" | xxd -r -p -c $AESCTR_CREDS_ENCODED_MAX_LEN) \\\n            2>/dev/null\n        # python version - depends on dsiprouter's python3 venv and pycryptodome\n#        ${PYTHON_CMD} <<EOPY\n#import os, sys\n#os.chdir('${DSIP_PROJECT_DIR}/gui')\n#sys.path.insert(0, '${DSIP_SYSTEM_CONFIG_DIR}/gui')\n#import settings\n#from util.security import AES_CTR\n#print(AES_CTR.decrypt(settings.${NAME}), end='')\n#EOPY\n    fi\n}\nexport -f decryptConfigAttrib\n\n# $1 == attribute name\n# $2 == kamailio config file\nfunction enableKamailioConfigAttrib() {\n    local NAME=\"$1\"\n    local CONFIG_FILE=\"$2\"\n\n    sed -i -r -e \"s~#+(!(define|trydef|redefine)[[:space:]]? $NAME)~#\\1~g\" ${CONFIG_FILE}\n}\nexport -f enableKamailioConfigAttrib\n\n# $1 == attribute name\n# $2 == kamailio config file\nfunction disableKamailioConfigAttrib() {\n    local NAME=\"$1\"\n    local CONFIG_FILE=\"$2\"\n\n    sed -i -r -e \"s~#+(!(define|trydef|redefine)[[:space:]]? $NAME)~##\\1~g\" ${CONFIG_FILE}\n}\nexport -f disableKamailioConfigAttrib\n\n# $1 == name of defined url to change\n# $2 == value to change url to\n# $3 == kamailio config file\n# notes: will skip any cluster url attributes\nfunction setKamailioConfigDburl() {\n    local NAME=\"$1\"\n    local VALUE=\"$2\"\n    local CONFIG_FILE=\"$3\"\n\n    perl -e \"\\$dburl='${VALUE}';\" \\\n        -0777 -i -pe 's~(#!(define|trydef|redefine)\\s+?'\"${NAME}\"'\\s+)['\"'\"'\"](?!cluster\\:).*['\"'\"'\"]~\\1\"${dburl}\"~g' ${CONFIG_FILE}\n}\nexport -f setKamailioConfigDburl\n\n# $1 == name of subst/substdef/substdefs to change\n# $2 == value to change subst/substdef/substdefs to\n# $3 == kamailio config file\nfunction setKamailioConfigSubst() {\n    local NAME=\"$1\"\n    local VALUE=\"$2\"\n    local CONFIG_FILE=\"$3\"\n\n    perl -e \"\\$name='$NAME'; \\$value='$VALUE';\" \\\n        -i -pe 's~(#!subst(?:def|defs)?.*!${name}!).*(!.*)~\\1${value}\\2~g' ${CONFIG_FILE}\n}\nexport -f setKamailioConfigSubst\n\n# $1 == name of global variable to change\n# $2 == value to change variable to\n# $3 == kamailio config file\nfunction setKamailioConfigGlobal() {\n    local NAME=\"$1\"\n    local VALUE=\"$2\"\n    local CONFIG_FILE=\"$3\"\n    local REPLACE_TOKEN='__ABCDEFGHIJKLMNOPQRSTUVWXYZ__'\n\n    perl -pi -e \"s~^(${NAME}\\s?=\\s?)(?:(\\\"|')(.*?)(\\\"|')|\\d+)(\\sdesc\\s(?:\\\"|').*?(?:\\\"|'))?~\\1\\2${REPLACE_TOKEN}\\4\\5~g\" ${CONFIG_FILE}\n    sed -i -e \"s%${REPLACE_TOKEN}%${VALUE}%g\" ${CONFIG_FILE}\n}\nexport -f setKamailioConfigGlobal\n\n# $1 == attribute name\n# $2 == rtpengine config file\nfunction enableRtpengineConfigAttrib() {\n    local NAME=\"$1\"\n    local CONFIG_FILE=\"$2\"\n\n    sed -i -r -e \"s~^#+(${NAME}[ \\t]*=[ \\t]*.*)~\\1~g\" ${CONFIG_FILE}\n}\nexport -f enableRtpengineConfigAttrib\n\n# $1 == attribute name\n# $2 == rtpengine config file\nfunction disableRtpengineConfigAttrib() {\n    local NAME=\"$1\"\n    local CONFIG_FILE=\"$2\"\n\n    sed -i -r -e \"s~^#*(${NAME}[ \\t]*=[ \\t]*.*)~#\\1~g\" ${CONFIG_FILE}\n}\nexport -f disableRtpengineConfigAttrib\n\n# $1 == attribute name\n# $2 == rtpengine config file\nfunction getRtpengineConfigAttrib() {\n    local NAME=\"$1\"\n    local CONFIG_FILE=\"$2\"\n\n    grep -oP '^(?!#)('${NAME}'[ \\t]*=[ \\t]*\\K.*)' ${CONFIG_FILE}\n}\nexport -f getRtpengineConfigAttrib\n\n# $1 == attribute name\n# $2 == value of attribute\n# $3 == rtpengine config file\nfunction setRtpengineConfigAttrib() {\n    local NAME=\"$1\"\n    local VALUE=\"$2\"\n    local CONFIG_FILE=\"$3\"\n\n    perl -e \"\\$name='$NAME'; \\$value='$VALUE';\" \\\n        -i -pe 's%^(?!#)(${name}[ \\t]*=[ \\t]*.*)%${name} = ${value}%g' ${CONFIG_FILE}\n}\nexport -f setRtpengineConfigAttrib\n\n# output: Linux Distro name\nfunction getDistroName() {\n    grep '^ID=' /etc/os-release 2>/dev/null | cut -d '=' -f 2 | cut -d '\"' -f 2\n}\nexport -f getDistroName\n\n# output: Linux Distro version\nfunction getDistroVer() {\n    grep '^VERSION_ID=' /etc/os-release 2>/dev/null | cut -d '=' -f 2 | cut -d '\"' -f 2\n}\nexport -f getDistroVer\n\n# $1 == command to test\n# returns: 0 == true, 1 == false\nfunction cmdExists() {\n    if command -v \"$1\" > /dev/null 2>&1; then\n        return 0\n    else\n        return 1\n    fi\n}\nexport -f cmdExists\n\n# $1 == directory to check for in PATH\n# returns: 0 == found, 1 == not found\nfunction pathCheck() {\n    case \":${PATH-}:\" in\n        *:\"$1\":*)\n            return 0\n            ;;\n        *)\n            return 1\n            ;;\n    esac\n}\nexport -f pathCheck\n\n# returns: 0 == success, otherwise failure\n# notes: try to access the AWS metadata URL to determine if this is an AMI instance\nfunction isInstanceAMI() {\n    local RET TOKEN\n\n    # handle IMDS version selection automatically\n    # ref: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html\n    RET=$(curl -s -o /dev/null --connect-timeout 2 -w '%{http_code}' http://169.254.169.254/latest/meta-data/ami-id)\n    if (( $RET == 200 )); then\n        return 0\n    elif (( $RET == 401 )); then\n        TOKEN=$(curl -s -X PUT --connect-timeout 2 -H 'X-aws-ec2-metadata-token-ttl-seconds: 60' http://169.254.169.254/latest/api/token)\n        curl -s -f --connect-timeout 2 -H \"X-aws-ec2-metadata-token: $TOKEN\" http://169.254.169.254/latest/meta-data/ami-id\n        return $?\n    else\n        return 1\n    fi\n}\nexport -f isInstanceAMI\n\n# returns: 0 == success, otherwise failure\n# notes: try to access the DO metadata URL to determine if this is an Digital Ocean instance\nfunction isInstanceDO() {\n    curl -s -f --connect-timeout 2 http://169.254.169.254/metadata/v1/id &>/dev/null\n    return $?\n}\nexport -f isInstanceDO\n\n# returns: 0 == success, otherwise failure\n# notes: try to access the GCE metadata URL to determine if this is an Google instance\nfunction isInstanceGCE() {\n    curl -s -f --connect-timeout 2 -H \"Metadata-Flavor: Google\" http://metadata.google.internal/computeMetadata/v1/id &>/dev/null\n    return $?\n}\nexport -f isInstanceGCE\n\n# returns: 0 == success, otherwise failure\n# notes: try to access the MS Azure metadata URL to determine if this is an Azure instance\nfunction isInstanceAZURE() {\n    curl -s -f --connect-timeout 2 -H \"Metadata: true\" \"http://169.254.169.254/metadata/instance?api-version=2018-10-01\" &>/dev/null\n    return $?\n}\nexport -f isInstanceAZURE\n\n# returns: 0 == success, otherwise failure\n# notes: try to access the DO metadata URL to determine if this is an VULTR instance\nfunction isInstanceVULTR() {\n    curl -s -f --connect-timeout 2 http://169.254.169.254/v1/instanceid &>/dev/null\n    return $?\n}\nexport -f isInstanceDO\n\n# output: instance ID || blank string\n# notes: we try checking for exported instance variable to avoid querying again\nfunction getInstanceID() {\n    local RET TOKEN\n\n    if (( ${AWS_ENABLED:-0} == 1)); then\n        RET=$(curl -s -o /dev/null -w '%{http_code}' http://169.254.169.254/latest/meta-data/ami-id 2>/dev/null)\n        if (( $RET == 200 )); then\n            curl -s -f http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null\n        elif (( $RET == 401 )); then\n            TOKEN=$(curl -s -X PUT --connect-timeout 2 -H 'X-aws-ec2-metadata-token-ttl-seconds: 60' http://169.254.169.254/latest/api/token 2>/dev/null)\n            curl -s -f --connect-timeout 2 -H \"X-aws-ec2-metadata-token: $TOKEN\" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null\n        else\n            return 1\n        fi\n    elif (( ${DO_ENABLED:-0} == 1 )); then\n        curl -s -f http://169.254.169.254/metadata/v1/id 2>/dev/null\n    elif (( ${GCE_ENABLED:-0} == 1 )); then\n        curl -s -f -H \"Metadata-Flavor: Google\" http://metadata.google.internal/computeMetadata/v1/instance/id 2>/dev/null\n    elif (( ${AZURE_ENABLED:-0} == 1 )); then\n        curl -s -f -H \"Metadata: true\" \"http://169.254.169.254/metadata/instance/compute/vmId?api-version=2018-10-01\" 2>/dev/null\n    elif (( ${VULTR_ENABLED:-0} == 1 )); then\n        curl -s -f http://169.254.169.254/v1/instanceid 2>/dev/null\n    else\n        if isInstanceAMI; then\n            RET=$(curl -s -o /dev/null -w '%{http_code}' http://169.254.169.254/latest/meta-data/ami-id 2>/dev/null)\n            if (( $RET == 200 )); then\n                curl -s -f http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null\n            elif (( $RET == 401 )); then\n                TOKEN=$(curl -s -X PUT --connect-timeout 2 -H 'X-aws-ec2-metadata-token-ttl-seconds: 60' http://169.254.169.254/latest/api/token 2>/dev/null)\n                curl -s -f --connect-timeout 2 -H \"X-aws-ec2-metadata-token: $TOKEN\" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null\n            else\n                return 1\n            fi\n        elif isInstanceDO; then\n            curl -s -f http://169.254.169.254/metadata/v1/id 2>/dev/null\n        elif isInstanceGCE; then\n            curl -s -f -H \"Metadata-Flavor: Google\" http://metadata.google.internal/computeMetadata/v1/instance/id 2>/dev/null\n        elif isInstanceAZURE; then\n            curl -s -f -H \"Metadata: true\" \"http://169.254.169.254/metadata/instance/compute/vmId?api-version=2018-10-01\" 2>/dev/null\n        elif isInstanceVULTR; then\n            curl -s -f http://169.254.169.254/v1/instanceid 2>/dev/null\n        fi\n    fi\n}\nexport -f getInstanceID\n\n# summary:  append a crontab entry\n# usage:    cronAppend [options] <crontab entry>\n# options:  -u <crontab user>\nfunction cronAppend() {\n    local ENTRY USER_ARGS=()\n\n    case \"$1\" in\n        -u)\n            shift\n            USER_ARGS=(-u \"$1\")\n            shift\n            ENTRY=\"$1\"\n            ;;\n        *)\n            ENTRY=\"$1\"\n            ;;\n    esac\n\n    crontab ${USER_ARGS[@]} -l 2>/dev/null | { cat; echo \"$ENTRY\"; } | crontab ${USER_ARGS[@]} -\n}\nexport -f cronAppend\n\n# summary:  remove crontab entries matching search\n# usage:    cronRemove [options] <search string>\n# options:  -u <crontab user>\nfunction cronRemove() {\n    local SEARCH USER_ARGS=()\n\n    case \"$1\" in\n        -u)\n            shift\n            USER_ARGS=(-u \"$1\")\n            shift\n            SEARCH=\"$1\"\n            ;;\n        *)\n            SEARCH=\"$1\"\n            ;;\n    esac\n\n    crontab ${USER_ARGS[@]} -l 2>/dev/null | grep -v -F -w \"$SEARCH\" | crontab ${USER_ARGS[@]} -\n}\nexport -f cronRemove\n\n# $1 == ip to test\n# returns: 0 == success, 1 == failure\n# notes: regex credit to <https://helloacm.com>\nfunction ipv4Test() {\n    local IP=\"$1\"\n\n    if [[ $IP =~ ^([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$ ]]; then\n        return 0\n    fi\n    return 1\n}\nexport -f ipv4Test\n\n# $1 == ip to test\n# returns: 0 == success, 1 == failure\n# notes: regex credit to <https://helloacm.com>\nfunction ipv6Test() {\n    local IP=\"$1\"\n\n    if [[ $IP =~ ^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$ ]]; then\n        return 0\n    fi\n    return 1\n}\nexport -f ipv6Test\n\n# $1 == ip to test\n# returns: 0 == success, 1 == failure\nfunction ipv4TestRFC1918() {\n    local IP=\"$1\"\n    if [[ $IP =~ ^(10\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|172\\.(1[6-9]|2[0-9]|3[01])\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|192\\.168\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5]))$ ]]; then\n        return 0\n    fi\n    return 1\n}\nexport -f ipv4TestRFC1918\n\n# output: all physical network interfaces on this machine\nfunction getPhysicalIfaces() {\n    ( cd /sys/class/net && dirname */device; )\n}\nexport -f getPhysicalIfaces\n\n# $1 == [-4|-6] to force specific IP version\n# output: the internal IP for this system\n# notes: prints internal ip, or empty string if not available\n# notes: tries ipv4 first then ipv6\n# returns: 0 on success, 1 if the interface was not found\n# TODO: currently we only check for the internal IP associated with the default interface/default route\n#       this will fail if the internal IP is not assigned to the default interface/default route\n#       not sure what networking scenarios that would be useful for, the community should provide us feedback on this\nfunction getInternalIP() {\n    local INTERNAL_IP=\"\"\n\n    case \"$1\" in\n        -4)\n            local IPV4_ENABLED=1\n            local IPV6_ENABLED=0\n            ;;\n        -6)\n            local IPV4_ENABLED=0\n            local IPV6_ENABLED=1\n            ;;\n        *)\n            local IPV4_ENABLED=1\n            local IPV6_ENABLED=${IPV6_ENABLED:-0}\n            ;;\n    esac\n\n    if (( ${IPV6_ENABLED} == 1 )); then\n\t\tINTERFACE=$(ip -br -6 a| grep UP | head -1 | awk {'print $1'})\n    else\n\t\tINTERFACE=$(ip -4 route show default | head -1 | awk '{print $5}')\n    fi\n\n    # Get the ip address without depending on DNS\n    if (( ${IPV4_ENABLED} == 1 )); then\n        # Marked for removal because it depends on DNS\n\t\t#INTERNAL_IP=$(ip -4 route get $GOOGLE_DNS_IPV4 2>/dev/null | head -1 | grep -oP 'src \\K([^\\s]+)')\n\t\tINTERNAL_IP=$(ip addr show $INTERFACE | grep 'inet ' | awk '{print $2}' | cut -f1 -d'/' | head -1)\n    fi\n\n    if (( ${IPV6_ENABLED} == 1 )) && [[ -z \"$INTERNAL_IP\" ]]; then\n        # Marked for removal because it depends on DNS\n        #INTERNAL_IP=$(ip -6 route get $GOOGLE_DNS_IPV6 2>/dev/null | head -1 | grep -oP 'src \\K([^\\s]+)')\n\t\tINTERNAL_IP=$(ip addr show $INTERFACE | grep 'inet6 ' | awk '{print $2}' | cut -f1 -d'/' | head -1)\n    fi\n\n    printf '%s' \"$INTERNAL_IP\"\n}\nexport -f getInternalIP\n\n# $1 == [-4|-6] to force specific IP version\n# $2 == network interface \n# output: the IP for the given interface\n# notes: prints ip, or empty string if not available\n# notes: tries ipv4 first then ipv6\nfunction getIP() {\n    local IP=\"\"\n\n    case \"$1\" in\n        -4)\n            local IPV4_ENABLED=1\n            local IPV6_ENABLED=0\n            ;;\n        -6)\n            local IPV4_ENABLED=0\n            local IPV6_ENABLED=1\n            ;;\n        *)\n            local IPV4_ENABLED=1\n            local IPV6_ENABLED=${IPV6_ENABLED:-0}\n            ;;\n    esac\n\n    # Use the provided interface or get the first interface - other then lo\n    if ! [ -z $2 ]; then\n\t    INTERFACE=$2\n    else\n\t    if (( ${IPV6_ENABLED} == 1 )); then\n\t\t\tINTERFACE=$(ip -br -6 a| grep UP | head -1 | awk {'print $1'})\n\t    else\n\t    \tINTERFACE=$(ip -4 route show default | head -1 | awk '{print $5}')\n\t    fi\n    fi\n\n    # Get the ip address without depending on DNS\n    if (( ${IPV4_ENABLED} == 1 )); then\n        # Marked for removal because it depends on DNS\n\t\t#INTERNAL_IP=$(ip -4 route get $GOOGLE_DNS_IPV4 2>/dev/null | head -1 | grep -oP 'src \\K([^\\s]+)')\n\t\tIP=$(ip addr show $INTERFACE | grep 'inet ' | awk '{print $2}' | cut -f1 -d'/' | head -1)\n    fi\n\n    if (( ${IPV6_ENABLED} == 1 )) && [[ -z \"$INTERNAL_IP\" ]]; then\n        # Marked for removal because it depends on DNS\n        #INTERNAL_IP=$(ip -6 route get $GOOGLE_DNS_IPV6 2>/dev/null | head -1 | grep -oP 'src \\K([^\\s]+)')\n\t\tIP=$(ip addr show $INTERFACE | grep 'inet6 ' | awk '{print $2}' | cut -f1 -d'/' | head -1)\n    fi\n\n    printf '%s' \"$IP\"\n}\nexport -f getIP\n\n# TODO: run requests in parallel and grab first good one (start ipv4 first)\n#       this is what we are already doing in gui/util/networking.py\n#       this would be much faster bcuz DNS exceptions take a while to handle\n#       GNU parallel should not be used bcuz package support is not very good\n#       this should instead use a pure bash version of GNU parallel, refs:\n#       https://stackoverflow.com/questions/10909685/run-parallel-multiple-commands-at-once-in-the-same-terminal\n#       https://www.cyberciti.biz/faq/how-to-run-command-or-code-in-parallel-in-bash-shell-under-linux-or-unix/\n#       https://unix.stackexchange.com/questions/305039/pausing-a-bash-script-until-previous-commands-are-finished\n#       https://unix.stackexchange.com/questions/497614/bash-execute-background-process-whilst-reading-output\n#       https://stackoverflow.com/questions/3338030/multiple-bash-traps-for-the-same-signal\n# $1 == [-4|-6] to force specific IP version\n# output: the external IP for this system\n# notes: prints external ip, or empty string if not available\n# notes: below we have measurements for average time of each service\n#        over 10 non-cached requests, in seconds, round trip\n#\n# |          External Service         | Mean RTT | IP Protocol |\n# |:---------------------------------:|:--------:|:-----------:|\n# | https://icanhazip.com             | 0.38080  | IPV4        |\n# | https://ipecho.net/plain          | 0.39810  | IPV4        |\n# | https://myexternalip.com/raw      | 0.51850  | IPV4        |\n# | https://api.ipify.org             | 0.64860  | IPV4        |\n# | https://bot.whatismyipaddress.com | 0.69640  | IPV4        |\n# | https://icanhazip.com             | 0.40190  | IPV6        |\n# | https://bot.whatismyipaddress.com | 0.72490  | IPV6        |\n# | https://ifconfig.co               | 0.80290  | IPV6        |\n# | https://ident.me                  | 0.97620  | IPV6        |\n# | https://api6.ipify.org            | 1.08510  | IPV6        |\n#\nfunction getExternalIP() {\n    local EXTERNAL_IP=\"\" TIMEOUT=5\n    local IPV4_URLS=(\n        \"https://icanhazip.com\"\n        \"https://ipecho.net/plain\"\n        \"https://myexternalip.com/raw\"\n        \"https://api.ipify.org\"\n        \"https://bot.whatismyipaddress.com\"\n    )\n    local IPV6_URLS=(\n        \"https://icanhazip.com\"\n        \"https://bot.whatismyipaddress.com\"\n        \"https://ifconfig.co\"\n        \"https://ident.me\"\n        \"https://api6.ipify.org\"\n    )\n\n    case \"$1\" in\n        -4)\n            local IPV4_ENABLED=1\n            local IPV6_ENABLED=0\n            ;;\n        -6)\n            local IPV4_ENABLED=0\n            local IPV6_ENABLED=1\n            ;;\n        *)\n            local IPV4_ENABLED=1\n            local IPV6_ENABLED=${IPV6_ENABLED:-0}\n            ;;\n    esac\n\n    if (( ${IPV4_ENABLED} == 1 )); then\n        for URL in ${IPV4_URLS[@]}; do\n            EXTERNAL_IP=$(curl -4 -s --connect-timeout $TIMEOUT $URL 2>/dev/null)\n            ipv4Test \"$EXTERNAL_IP\" && { printf '%s' \"$EXTERNAL_IP\"; return 0; }\n        done\n    fi\n\n    if (( ${IPV6_ENABLED} == 1 )) && [[ -z \"$EXTERNAL_IP\" ]]; then\n        for URL in ${IPV6_URLS[@]}; do\n            EXTERNAL_IP=$(curl -6 -s --connect-timeout $TIMEOUT $URL 2>/dev/null)\n            ipv6Test \"$EXTERNAL_IP\" && { printf '%s' \"$EXTERNAL_IP\"; return 0; }\n        done\n    fi\n\n    return 1\n}\nexport -f getExternalIP\n\n# output: the internal FQDN for this system\n# notes: prints internal FQDN, or empty string if not available\nfunction getInternalFQDN() {\n    printf '%s' \"$(hostname -f 2>/dev/null || hostname 2>/dev/null)\"\n}\nexport -f getInternalFQDN\n\n# output: the external FQDN for this system\n# notes: prints external FQDN, or empty string if not available\n# notes: will use EXTERNAL_IP if available or look it up dynamically\n# notes: tries ipv4 first then ipv6\nfunction getExternalFQDN() {\n    local EXTERNAL_FQDN=$(dig @${GOOGLE_DNS_IPV4} +short -x ${EXTERNAL_IP:-$(getExternalIP -4)} 2>/dev/null | head -1 | sed 's/\\.$//')\n    if (( ${IPV6_ENABLED:-0} == 1 )) && [[ -z \"$EXTERNAL_FQDN\" ]]; then\n          EXTERNAL_FQDN=$(dig @${GOOGLE_DNS_IPV6} +short -x ${EXTERNAL_IP6:-$(getExternalIP -6)} 2>/dev/null | head -1 | sed 's/\\.$//')\n    fi\n    printf '%s' \"$EXTERNAL_FQDN\"\n}\nexport -f getExternalFQDN\n\n# $1 == [-4|-6] to force specific IP version\n# $2 == interface\n# output: the internal IP CIDR for this system\n# notes: prints internal CIDR address, or empty string if not available\n# notes: tries ipv4 first then ipv6\nfunction getInternalCIDR() {\n    local PREFIX_LEN=\"\" DEF_IFACE=\"\" INTERNAL_IP=\"\"\n    #local IP=$(ip -4 route get $GOOGLE_DNS_IPV4 2>/dev/null | head -1 | grep -oP 'src \\K([^\\s]+)')\n\n    case \"$1\" in\n        -4)\n            local IPV4_ENABLED=1\n            local IPV6_ENABLED=0\n            ;;\n        -6)\n            local IPV4_ENABLED=0\n            local IPV6_ENABLED=1\n            ;;\n        *)\n            local IPV4_ENABLED=1\n            local IPV6_ENABLED=${IPV6_ENABLED:-0}\n            ;;\n    esac\n    \n    if ! [ -z $2 ]; then\n\t    INTERFACE=$2\n    fi\n\n    if (( ${IPV4_ENABLED} == 1 )); then\n        INTERNAL_IP=$(getIP -4 \"$INTERFACE\")\n        if [[ -n \"$INTERNAL_IP\" ]]; then\n\t\t\tif [[ -n \"$INTERFACE\" ]]; then\n\t\t\t\tDEF_IFACE=$INTERFACE\n\t\t\telse\n\t\t\t\tDEF_IFACE=$(ip -4 route list scope global 2>/dev/null | perl -e 'while (<>) { if (s%^(?:0\\.0\\.0\\.0|default).*dev (\\w+).*$%\\1%) { print; exit; } }')\n\t\t\tfi\n\t\t\tPREFIX_LEN=$(ip -4 route list | grep \"$INTERNAL_IP\" | perl -e 'while (<>) { if (s%^(?!0\\.0\\.0\\.0|default).*/(\\d+) .*src [\\w/.]*.*$%\\1%) { print; exit; } }')\n        fi\n    fi\n\n    if (( ${IPV6_ENABLED} == 1 )) && [[ -z \"$INTERNAL_IP\" ]]; then\n        INTERNAL_IP=$(getInternalIP -6)\n        if [[ -n \"$INTERNAL_IP\" ]]; then\n            DEF_IFACE=$(ip -6 route list scope global 2>/dev/null | perl -e 'while (<>) { if (s%^(?:::/0|default).*dev (\\w+).*$%\\1%) { print; exit; } }')\n            PREFIX_LEN=$(ip -6 route list 2>/dev/null | grep \"dev $DEF_IFACE\" | perl -e 'while (<>) { if (s%^(?!::/0|default).*/(\\d+) .*via [\\w:/.]*.*$%\\1%) { print; exit; } }')\n        fi\n    fi\n\n    # make sure output is empty if error occurred\n    if [[ -n \"$INTERNAL_IP\" && -n \"$PREFIX_LEN\" ]]; then\n        printf '%s/%s' \"$INTERNAL_IP\" \"$PREFIX_LEN\"\n    fi\n}\nexport -f getInternalCIDR\n\n# $1 == host to check\n# returns: 0 == host is local, 1 == host is remote\nfunction isHostLocal() {\n    local LOCAL_MATCH=$(\n        joinwith '' '|' '' \\\n            localhost \\\n            $(hostname 2>/dev/null) \\\n            $(hostname -f 2>/dev/null) \\\n            $(ip -json address show | jq -r '.[].addr_info[].local')\n    )\n\n    if [[ \"$1\" =~ $LOCAL_MATCH ]]; then\n        return 0\n    fi\n    return 1\n}\nexport -f isHostLocal\n\n# $1 == cmd as executed in systemd (by ExecStart=)\n# notes: take precaution when adding long running functions as they will block startup in boot order\n# notes: adding init commands on an AMI instance must not be long running processes, otherwise they will fail\nfunction addInitCmd() {\n    local CMD=$(printf '%s' \"$1\" | sed -e 's|[\\/&]|\\\\&|g') # escape string\n    local TMP_FILE=\"${DSIP_INIT_FILE}.tmp\"\n\n    # sanity check, does the entry already exist?\n    grep -q -oP \"^ExecStart\\=.*${CMD}.*\" 2>/dev/null ${DSIP_INIT_FILE} && return 0\n\n    tac ${DSIP_INIT_FILE} | sed -r \"0,\\|^ExecStart\\=.*|{s|^ExecStart\\=.*|ExecStart=${CMD}\\n&|}\" | tac > ${TMP_FILE}\n    mv -f ${TMP_FILE} ${DSIP_INIT_FILE}\n\n    systemctl daemon-reload\n}\nexport -f addInitCmd\n\n# $1 == string to match for removal (after ExecStart=)\nfunction removeInitCmd() {\n    local STR=$(printf '%s' \"$1\" | sed -e 's|[\\/&]|\\\\&|g') # escape string\n\n    sed -i -r \"\\|^ExecStart\\=.*${STR}.*|d\" ${DSIP_INIT_FILE}\n    systemctl daemon-reload\n}\nexport -f removeInitCmd\n\n# $1 == service name (full name with target) to add dependency on dsip-init service\n# notes: only adds startup ordering dependency (service continues if init fails)\n# notes: the Before= section of init will link to an After= dependency on daemon-reload\nfunction addDependsOnInit() {\n    local SERVICE=\"$1\"\n\n    # sanity check, does the entry already exist?\n    grep -q -oP \"^(Before\\=|Wants\\=).*${SERVICE}.*\" 2>/dev/null ${DSIP_INIT_FILE} && return 0\n\n    perl -i -e \"\\$service='$SERVICE';\" -pe 's%^(Before\\=|Wants\\=)(.*)%length($2)==0 ? \"${1}${service}\" : \"${1}${2} ${service}\"%ge;' ${DSIP_INIT_FILE}\n    systemctl daemon-reload\n}\nexport -f addDependsOnInit\n\n# $1 == service name (full name with target) to remove dependency on dsip-init service\nfunction removeDependsOnInit() {\n    local SERVICE=\"$1\"\n\n    perl -i -e \"\\$service='$SERVICE';\" -pe 's%^((?:Before\\=|Wants\\=).*?)( ${service}|${service} |${service})(.*)%\\1\\3%g;' ${DSIP_INIT_FILE}\n    systemctl daemon-reload\n}\nexport -f removeDependsOnInit\n\n# $1 == ip or hostname\n# $2 == port (optional)\n# returns: 0 == connection good, 1 == connection bad\n# NOTE: if port is not given a ping test will be used instead\nfunction checkConn() {\n    local TIMEOUT=3 IP_ADDR=\"\" PING_V6_SELECTOR=\"\"\n\n    if (( $# == 2 )); then\n        timeout $TIMEOUT bash -c \"< /dev/tcp/$1/$2\" &>/dev/null; return $?\n    else\n        # NOTE: older versions of ping don't automatically detect IP address version\n        IP_ADDR=$(getent hosts \"$1\" 2>/dev/null | awk '{ print $1 ; exit }')\n        if ipv6Test \"$IP_ADDR\"; then\n            PING_V6_SELECTOR=\"-6\"\n        fi\n        ping $PING_V6_SELECTOR -q -W $TIMEOUT -c 3 \"$1\" &>/dev/null; return $?\n    fi\n}\nexport -f checkConn\n\n# $@ == ssh command to test\n# returns: 0 == ssh connected, 1 == ssh could not connect\nfunction checkSSH() {\n    $@ -o ConnectTimeout=5 'exit 0' &>/dev/null\n    return $?\n}\nexport -f checkSSH\n\n# bake in the connection details for kamailio user/database\n# standardizes our usage and avoids various pitfalls with the client APIs\n# usage:    withKamDB <mysql cmd> [mysql options/args]\nfunction withKamDB() {\n    local CONN_OPTS=()\n    local CMD=\"$1\"\n    shift\n\n    [[ -n \"$KAM_DB_HOST\" ]] && CONN_OPTS+=( \"--host=${KAM_DB_HOST}\" )\n    [[ -n \"$KAM_DB_PORT\" ]] && CONN_OPTS+=( \"--port=${KAM_DB_PORT}\" )\n    [[ -n \"$KAM_DB_USER\" ]] && CONN_OPTS+=( \"--user=${KAM_DB_USER}\" )\n    [[ -n \"$KAM_DB_PASS\" ]] && CONN_OPTS+=( \"--password=${KAM_DB_PASS}\" )\n    if [[ \"$CMD\" == \"mysql\" ]]; then\n        [[ -n \"$KAM_DB_NAME\" ]] && CONN_OPTS+=( \"--database=${KAM_DB_NAME}\" )\n    fi\n\n    if [[ -p /dev/stdin ]]; then\n        ${CMD} \"${CONN_OPTS[@]}\" \"$@\" </dev/stdin\n    else\n        ${CMD} \"${CONN_OPTS[@]}\" \"$@\"\n    fi\n    return $?\n}\nexport -f withKamDB\n\n# bake in the connection details for root user/database\n# standardizes our usage and avoids various pitfalls with the client APIs\n# usage:    withRootDBConn [options] <mysql cmd> [mysql options/args]\n# options:  --db=<mysql db name>\nfunction withRootDBConn() {\n    local TMP CMD\n    local CONN_OPTS=()\n\n    case \"$1\" in\n        --db=*)\n            TMP=$(cut -d '=' -f 2- <<<\"$1\")\n            [[ -n \"$TMP\" ]] && CONN_OPTS+=( \"--database=${TMP}\" )\n            shift\n            CMD=\"$1\"\n            shift\n            ;;\n        *)\n            CMD=\"$1\"\n            shift\n            if [[ \"$CMD\" == \"mysql\" ]]; then\n                [[ -n \"$ROOT_DB_NAME\" ]] && CONN_OPTS+=( \"--database=${ROOT_DB_NAME}\" )\n            fi\n            ;;\n    esac\n\n    [[ -n \"$ROOT_DB_HOST\" ]] && CONN_OPTS+=( \"--host=${ROOT_DB_HOST}\" )\n    [[ -n \"$ROOT_DB_PORT\" ]] && CONN_OPTS+=( \"--port=${ROOT_DB_PORT}\" )\n    [[ -n \"$ROOT_DB_USER\" ]] && CONN_OPTS+=( \"--user=${ROOT_DB_USER}\" )\n    [[ -n \"$ROOT_DB_PASS\" ]] && CONN_OPTS+=( \"--password=${ROOT_DB_PASS}\" )\n\n    if [[ -p /dev/stdin ]]; then\n        ${CMD} \"${CONN_OPTS[@]}\" \"$@\" </dev/stdin\n    else\n        ${CMD} \"${CONN_OPTS[@]}\" \"$@\"\n    fi\n    return $?\n}\nexport -f withRootDBConn\n\n# allow passing in connection details to bake into the command\n# standardizes our usage and avoids various pitfalls with the client APIs\n# usage:    withGivenDB [options] <mysql cmd> [mysql options/args]\n# options:  --user=<mysql user>\n#           --pass=<mysql password>\n#           --host=<mysql host>\n#           --port=<mysql port>\n#           --db=<mysql db name>\nfunction withGivenDB() {\n    local TMP\n    local ARGS=()\n    local CONN_OPTS=()\n\n    while (( $# > 0 )); do\n        case \"$1\" in\n            --user=*)\n                TMP=$(cut -d '=' -f 2- <<<\"$1\")\n                [[ -n \"$TMP\" ]] && CONN_OPTS+=( \"--user=${TMP}\" )\n                shift\n                ;;\n            --pass=*)\n                TMP=$(cut -d '=' -f 2- <<<\"$1\")\n                [[ -n \"$TMP\" ]] && CONN_OPTS+=( \"--password=${TMP}\" )\n                shift\n                ;;\n            --host=*)\n                TMP=$(cut -d '=' -f 2- <<<\"$1\")\n                [[ -n \"$TMP\" ]] && CONN_OPTS+=( \"--host=${TMP}\" )\n                shift\n                ;;\n            --port=*)\n                TMP=$(cut -d '=' -f 2- <<<\"$1\")\n                [[ -n \"$TMP\" ]] && CONN_OPTS+=( \"--port=${TMP}\" )\n                shift\n                ;;\n            --db=*)\n                TMP=$(cut -d '=' -f 2- <<<\"$1\")\n                [[ -n \"$TMP\" ]] && CONN_OPTS+=( \"--database=${TMP}\" )\n                shift\n                ;;\n            *)\n                ARGS+=( \"$1\" )\n                shift\n                ;;\n        esac\n    done\n\n    if [[ -p /dev/stdin ]]; then\n        ${ARGS[0]} \"${CONN_OPTS[@]}\" \"${ARGS[@]:1}\" </dev/stdin\n    else\n        ${ARGS[0]} \"${CONN_OPTS[@]}\" \"${ARGS[@]:1}\"\n    fi\n    return $?\n}\nexport -f withGivenDB\n\n# usage: checkDB <database>\n# returns:  0 if DB exists, 1 otherwise\nfunction checkDB() {\n    local CHECK=$(\n        withRootDBConn mysql -sN \\\n            -e \"SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$1';\" \\\n            2>/dev/null\n    )\n    if [[ -n \"$CHECK\" ]]; then\n        return 0\n    fi\n    return 1\n}\nexport -f checkDB\n\n# usage: checkDBUserExists <user>@<host>\n# returns: 0 if user exists, 1 on DB connection failure, 2 if user does not exist\nfunction checkDBUserExists() {\n    local USER=$(cut -d '@' -f 1 <<<\"$1\")\n    local HOST=$(cut -d '@' -f 2 <<<\"$1\")\n\n    return $(\n        withRootDBConn mysql -sN -A \\\n            -e \"SELECT IF(EXISTS(SELECT 1 FROM mysql.user WHERE User='${USER}' AND Host='${HOST}'),0,2);\" \\\n            2>/dev/null\n    )\n}\nexport -f checkDBUserExists\n\n# usage: dumpDB <databases>\n# output: dumped database as sql (redirect as needed)\n# returns: 0 on success, non zero otherwise\nfunction dumpDB() {\n    withRootDBConn mysqldump --single-transaction --opt --routines --triggers --hex-blob --databases \"$@\" \\\n        2>/dev/null |\n        sed -r \\\n            -e 's|DEFINER=[`\"'\"'\"'][a-zA-Z0-9_%]*[`\"'\"'\"']@[`\"'\"'\"'][a-zA-Z0-9_%]*[`\"'\"'\"']||g' \\\n            -e 's|ENGINE=MyISAM|ENGINE=InnoDB|g' \\\n            2>/dev/null\n    return ${PIPESTATUS[0]}\n}\nexport -f dumpDB\n\n# usage: dumpDBUser <user>@<host>|<user>\n# output: dumped database user as sql (redirect as needed)\n# returns: 0 on success, non zero otherwise\nfunction dumpDBUser() {\n    USER=$(printf '%s' \"$1\" | cut -s -d '@' -f 1)\n    if [[ -n \"$USER\" ]]; then\n        shift\n        USER=\"$1\"\n    fi\n\n    (withRootDBConn mysql -sN -A \\\n        -e \"SELECT DISTINCT CONCAT('SHOW GRANTS FOR ''',user,'''@''',host,''';') FROM mysql.user WHERE user='${USER}'\" 2>/dev/null \\\n        | withRootDBConn mysql -sN -A 2>/dev/null \\\n        | sed 's/$/;/g' \\\n        | awk '!x[$0]++' &&\n        printf '%s\\n' 'FLUSH PRIVILEGES;';\n        exit ${PIPESTATUS[0]}; ) 2>/dev/null\n    return $?\n}\nexport -f dumpDBUser\n\n# usage:\n#   sqlAsTransaction [options] <database> <sql statements>\n#   echo 'sql statements' | sqlAsTransaction [options] <database>\n# options:\n#   --user=<mysql user>\n#   --pass=<mysql password>\n#   --host=<mysql host>\n#   --port=<mysql port>\n#   --db=<mysql database name>\n# returns:\n#   0 == statements executed successfully\n#   1 == error occurred running statements\nfunction sqlAsTransaction() {\n    local TMP\n    local DB_SET=0\n    local MYSQL_USER='' MYSQL_PASS='' MYSQL_HOST='' MYSQL_PORT=''\n    local OPTS=() SQL_STATEMENTS=()\n\n    while (( $# > 0 )); do\n        case \"$1\" in\n            --user=*|--pass=*|--host=*|--port=*)\n                OPTS+=( \"$1\" )\n                shift\n                ;;\n            --db=*)\n                OPTS+=( \"$1\" )\n                shift\n                DB_SET=1\n                ;;\n            *)  # all positional args are part of SQL query\n                SQL_STATEMENTS+=( \"$1\" )\n                shift\n                ;;\n        esac\n    done\n\n    # creating the procedure will fail if we don't select a database\n    # TODO: do we need this now that we switched to prepared statements?\n    (( $DB_SET == 0 )) && OPTS+=( \"--db=mysql\" )\n\n    # if query was piped to stdin use that instead of positional args\n    if [[ -p /dev/stdin ]]; then\n        read -r -d '' TMP\n        [[ -n \"$TMP\" ]] && SQL_STATEMENTS=( \"$TMP\" )\n    fi\n\n    cat <<EOF | withGivenDB \"${OPTS[@]}\" mysql -s\nSTART TRANSACTION;\n${SQL_STATEMENTS[@]}\nSET @end_transaction = (SELECT IF(@@error_count > 0, \"ROLLBACK;\", \"COMMIT;\"));\nPREPARE stmt FROM @end_transaction;\nEXECUTE stmt;\nEOF\n    return $?\n}\nexport -f sqlAsTransaction\n\n# usage: parseDBConnURI <field> <connection uri>\n# field:    -user\n#           -pass\n#           -host\n#           -port\n#           -name\n# output: the selected field of the connection uri\n# returns:\n#   0   == field set\n#   1   == field not set\n#   255 == error parsing\nfunction parseDBConnURI() {\n    local GROUP\n\n    case \"$1\" in\n        -user)\n            shift\n            GROUP=0\n            ;;\n        -pass)\n            shift\n            GROUP=1\n            ;;\n        -host)\n            shift\n            GROUP=2\n            ;;\n        -port)\n            shift\n            GROUP=3\n            ;;\n        -name)\n            shift\n            GROUP=4\n            ;;\n        *)  # not valid field\n            return 1\n            ;;\n    esac\n\n    perl -e '\n@matches = ($ARGV[0] =~ m\"^[\\t\\r\\n\\v\\f]*(?:([^:]+)?(?::([^@]+)?)?@)?([^:]+)(?::([^/]+)?)?(?:/([^\\t\\r\\n\\v\\f]+))?[\\t\\r\\n\\v\\f]*$\");\nif ($matches[$ARGV[1]] ne \"\") {\n    print $matches[$ARGV[1]];\n    exit 0;\n}\nelse {\n    exit 1;\n}\n' \"$1\" \"$GROUP\"\n\n    return $?\n}\nexport -f parseDBConnURI\n\n# usage:    urandomChars [options] [args]\n# options:  -f <filter> == characters to allow\n# args:     $1 == number of characters to get\n# output:   string of random printable characters\nfunction urandomChars() {\n\tlocal LEN=32 FILTER=\"a-zA-Z0-9\"\n\n    while (( $# > 0 )); do\n    \t# last arg is length\n        if (( $# == 1 )); then\n            LEN=\"$1\"\n            shift\n            break\n        fi\n\n        case \"$1\" in\n        \t# user defined filter\n            -f)\n                shift\n                FILTER=\"$1\"\n                shift\n                ;;\n\t\t\t# not valid option skip\n            *)\n                shift\n                ;;\n        esac\n    done\n\n    tr -dc \"$FILTER\" </dev/urandom | dd if=/dev/stdin of=/dev/stdout bs=1 count=\"$LEN\" 2>/dev/null\n}\nexport -f urandomChars\n\n# $1 == prefix for each arg\n# $2 == delimiter between args\n# $3 == suffix for each arg\n# $@ == args to join\nfunction joinwith() {\n    local START=\"$1\" IFS=\"$2\" END=\"$3\" ARR=()\n    shift;shift;shift\n\n    for VAR in \"$@\"; do\n        ARR+=(\"${START}${VAR}${END}\")\n    done\n\n    echo \"${ARR[*]}\"\n}\nexport -f joinwith\n\n# $1 == rpc command\n# $@ == rpc args\n# output: output returned from kamailio\n# returns: curl return code (ref: man 1 curl)\n# note: curl will timeout after 3 seconds\nfunction sendKamCmd() {\n    local CMD=\"$1\" PARAMS=\"\" KAM_API_URL='http://127.0.0.1:5060/api/kamailio'\n    shift\n    local ARGS=(\"$@\")\n\n    if [[ \"$CMD\" == \"cfg.seti\" ]]; then\n        local LAST_ARG=\"${ARGS[$#-1]}\"\n        unset \"ARGS[$#-1]\"\n        PARAMS='['$(joinwith '\"' ',' '\"' \"${ARGS[@]}\")\",${LAST_ARG}\"']'\n    else\n        PARAMS='['$(joinwith '\"' ',' '\"' \"$@\")']'\n    fi\n\n    curl -s -m 3 -X GET -d '{\"method\": \"'\"${CMD}\"'\", \"jsonrpc\": \"2.0\", \"id\": 1, \"params\": '\"${PARAMS}\"'}' ${KAM_API_URL}\n}\nexport -f sendKamCmd\n\n# TODO: improve performance of openssl native version and swap it out\nfunction hashCreds() {\n\tlocal CREDS SALT DK_LEN\n\t# we use system python3 if dsiprouter python venv does not yet exist\n\tif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]]; then\n\t    local PYTHON_CMD=\"$PYTHON_CMD\"\n\telse\n\t    local PYTHON_CMD=\"python3\"\n\tfi\n\n\t# grab credentials from stdin if provided\n\tif [[ -p /dev/stdin ]]; then\n\t\tCREDS=$(</dev/stdin)\n\tfi\n\n    while (( $# > 0 )); do\n    \t# last arg is credentials\n        if (( $# == 1 )) && [[ -z \"$CREDS\" ]]; then\n\t\t\tCREDS=\"$1\"\n\t\t\tshift\n            break\n        fi\n\n        case \"$1\" in\n        \t# user defined salt\n            -s)\n                shift\n                SALT=\"$1\"\n                shift\n                ;;\n        \t# user defined derived key length\n            -l)\n                shift\n                DK_LEN=\"$1\"\n                shift\n                ;;\n\t\t\t# not valid option skip\n            *)\n                shift\n                ;;\n        esac\n    done\n\n    # defaults if not set by args\n    SALT=${SALT:-$(urandomChars -f 'a-fA-F0-9' $SALT_LEN)}\n    DK_LEN=${DK_LEN:-$DK_LEN_DEFAULT}\n\n\t# python native version\n\t# no external dependencies other than vanilla python3\n\t${PYTHON_CMD} <<EOPYTHON\nimport hashlib,binascii\ncreds='$CREDS'.encode('utf-8')\nsalt='$SALT'.encode('utf-8')\nhash=hashlib.pbkdf2_hmac('sha512', creds, salt, iterations=$HASH_ITERATIONS, dklen=$DK_LEN) + salt\nprint(binascii.hexlify(hash).decode('utf-8'))\nEOPYTHON\n\t# bash native version\n\t# currently too slow for production usage\n\t#${DSIP_PROJECT_DIR}/dsiprouter/pbkdf2.sh 'sha512' \"$CREDS\" \"$SALT\" \"$HASH_ITERATIONS\" 4\n}\nexport -f hashCreds\n\n# args:\n#   $1  ==  version 1 to compare from\n#   $2  ==  compare operation\n#   $3  ==  version 2 to compare against\n# returns:\n#   0   ==  version comparison is true\n#   1   ==  version comparison is false\n#   255 ==  comparison failed\nfunction versionCompare() { (\n    set -e\n    trap 'exit 255' ERR\n\n    if [[ ! \"$1$2\" =~ [\\.0-9]+ ]]; then\n        echo \"${FUNCNAME}(): invalid version\"\n        exit 255\n    fi\n\n    local IFS=.\n    local IDX LEN\n    local VER1=( $1 ) VER2=( $3 )\n    if (( ${#VER1[@]} >= ${#VER2[@]} )); then\n        LEN=${#VER1[@]}\n    else\n        LEN=${#VER2[@]}\n    fi\n    for (( IDX=0; IDX<$LEN; IDX++)); do\n        VER1[$IDX]=${VER1[$IDX]:-0}\n        VER2[$IDX]=${VER2[$IDX]:-0}\n    done\n\n    case \"$2\" in\n        lt)\n            [[ \"${VER1[@]}\" == \"${VER2[@]}\" ]] && exit 1\n            for (( IDX=0; IDX<$LEN; IDX++)); do\n                (( ${VER1[$IDX]} > ${VER2[$IDX]} )) && exit 1\n            done\n            exit 0\n            ;;\n        lteq)\n            for (( IDX=0; IDX<$LEN; IDX++)); do\n                (( ${VER1[$IDX]} <= ${VER2[$IDX]} )) || exit 1\n            done\n            exit 0\n            ;;\n        eq)\n            for (( IDX=0; IDX<$LEN; IDX++)); do\n                (( ${VER1[$IDX]} == ${VER2[$IDX]} )) || exit 1\n            done\n            exit 0\n            ;;\n        gt)\n            [[ \"${VER1[@]}\" == \"${VER2[@]}\" ]] && exit 1\n            for (( IDX=0; IDX<$LEN; IDX++)); do\n                (( ${VER1[$IDX]} < ${VER2[$IDX]} )) && exit 1\n            done\n            exit 0\n            ;;\n        gteq)\n            for (( IDX=0; IDX<$LEN; IDX++)); do\n                (( ${VER1[$IDX]} >= ${VER2[$IDX]} )) || exit 1\n            done\n            exit 0\n            ;;\n        *)\n            echo \"${FUNCNAME}(): invalid comparator\"\n            exit 255\n            ;;\n    esac\n) }\nexport -f versionCompare\n\n# $1 == repo path\nfunction getGitTagFromShallowRepo() { (\n    cd \"$1\" 2>/dev/null &&\n    git config --get remote.origin.fetch | cut -d ':' -f 2- | rev | cut -d '/' -f 1 | rev\n) }\nexport -f getGitTagFromShallowRepo\n"
  },
  {
    "path": "dsiprouter/pbkdf2.sh",
    "content": "#!/usr/bin/env bash\n\n# Inspired By: https://gist.github.com/grondilu/abe50de34f6c838dbc9388fe797ea4e4\n# Credit to: https://github.com/stayradiated/pbkdf2-sha512\n# Copyright (c) 2014, JP Richardson Copyright (c) 2010-2011 Intalio Pte, All Rights Reserved\n# Original Author: Lucien Grondin, 2022\n\ndeclare hash_name=\"$1\" key_str=\"$2\" salt_str=\"$3\"\ndeclare -ai key salt u t block1 dk\ndeclare -i hLen=\"$(openssl dgst \"-$hash_name\" -binary <<<\"foo\" |wc -c)\"\ndeclare -i iterations=$4 dkLen=${5:-hLen}\ndeclare -i i j k l=$(( (dkLen+hLen-1)/hLen ))\n\nfor ((i=0; i<${#key_str}; i++)); do\n    printf -v \"key[$i]\" \"%d\" \"'${key_str:i:1}\"\ndone\n\nfor ((i=0; i<${#salt_str}; i++)); do\n    printf -v \"salt[$i]\" \"%d\" \"'${salt_str:i:1}\"\ndone\n\nblock1=(${salt[@]} 0 0 0 0)\n\nstep() {\n    printf '%02x' \"$@\" |\n    xxd -p -r |\n    openssl dgst -\"$hash_name\" -hmac \"$key_str\" -binary |\n    xxd -p -c 1 |\n    sed 's/^/0x/'\n}\n\nfor ((i=1;i<=l;i++)); do\n    for k in {0..3}; do \n        block1[${#salt[@]}+$k]=$((i >> (8*(3-k)) & 0xff))\n    done\n    \n    u=($(step \"${block1[@]}\"))\n    t=(${u[@]})\n    for ((j=1; j<iterations; j++)); do\n        u=($(step \"${u[@]}\"))\n        for ((k=0; k<hLen; k++)); do\n            t[k]=$((t[k]^u[k]))\n        done\n    done\n    \n    dk+=(${t[@]})\ndone\n\nprintf \"%02x\" \"${dk[@]:0:dkLen}\"; echo '';\n\n"
  },
  {
    "path": "dsiprouter/rhel/8.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # Install dependencies for dSIPRouter\n    yum remove -y rs-epel-release* &&\n    yum install -y yum-utils &&\n    yum --setopt=group_package_types=mandatory,default,optional groupinstall -y \"Development Tools\" &&\n    yum install -y firewalld python36 python36-libs python36-devel python36-pip MySQL-python sudo \\\n        python36-virtualenv logrotate rsyslog perl libev-devel util-linux postgresql-devel mariadb-devel\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # give dsiprouter permissions in SELINUX\n    semanage port -a -t http_port_t -p tcp ${DSIP_PORT} ||\n        semanage port -m -t http_port_t -p tcp ${DSIP_PORT}\n\n   # Enable and start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n    systemctl enable nginx\n    systemctl restart nginx\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v2.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    # add hook to bash_completion in the standard debian location\n    echo '. /usr/share/bash-completion/bash_completion' > /etc/bash_completion\n\n    return 0\n}\n\n\nfunction uninstall {\n    # Uninstall dependencies for dSIPRouter\n    yum remove -y python36u\\*\n    yum remove -y ius-release\n\n    # Remove the repos\n    rm -f /etc/yum.repos.d/ius*\n    rm -f /etc/pki/rpm-gpg/IUS-COMMUNITY-GPG-KEY\n    yum clean all\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter/rhel/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    # Install dependencies for dSIPRouter\n    {\n        dnf config-manager -y --set-enabled codeready-builder-for-rhel-9-$(uname -m)-rpms ||\n        dnf config-manager -y --set-enabled codeready-builder-for-rhel-9-rhui-rpms\n    } &&\n    dnf install -y firewalld logrotate rsyslog perl curl python3 python3-devel libpq-devel \\\n        libev-devel openldap-devel mariadb-devel\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # give dsiprouter permissions in SELINUX\n    semanage port -a -t http_port_t -p tcp ${DSIP_PORT} ||\n    semanage port -m -t http_port_t -p tcp ${DSIP_PORT}\n\n   # Enable and start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v2.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    # add hook to bash_completion in the standard debian location\n    echo '. /usr/share/bash-completion/bash_completion' > /etc/bash_completion\n\n    return 0\n}\n\n\nfunction uninstall {\n    rm -rf ${PYTHON_VENV}\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter/rocky/8.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # Install dependencies for dSIPRouter\n    dnf remove -y rs-epel-release* &&\n    dnf install -y dnf-utils &&\n    dnf --setopt=group_package_types=mandatory,default,optional groupinstall -y \"Development Tools\" &&\n    dnf install -y firewalld sudo logrotate rsyslog perl \\\n        python3.11 python3.11-pip python3.11-libs python3.11-devel python3.11-PyMySQL \\\n        libev-devel util-linux postgresql-devel mariadb-devel openldap-devel\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # give dsiprouter permissions in SELINUX\n    semanage port -a -t http_port_t -p tcp ${DSIP_PORT} ||\n        semanage port -m -t http_port_t -p tcp ${DSIP_PORT}\n\n   # Enable and start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n    systemctl enable nginx\n    systemctl restart nginx\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v2.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    # add hook to bash_completion in the standard debian location\n    echo '. /usr/share/bash-completion/bash_completion' > /etc/bash_completion\n\n    return 0\n}\n\n\nfunction uninstall {\n    dnf remove -y python36u\\*\n    dnf remove -y ius-release\n\n    # Remove the repos\n    rm -f /etc/dnf.repos.d/ius*\n    rm -f /etc/pki/rpm-gpg/IUS-COMMUNITY-GPG-KEY\n    dnf clean all\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter/rocky/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    # Install dependencies for dSIPRouter\n    dnf install -y firewalld logrotate rsyslog perl curl python3 python3-devel libpq-devel \\\n        libev-devel openldap-devel &&\n    dnf install -y --enablerepo=crb mariadb-devel\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # give dsiprouter permissions in SELINUX\n    semanage port -a -t http_port_t -p tcp ${DSIP_PORT} ||\n    semanage port -m -t http_port_t -p tcp ${DSIP_PORT}\n\n   # Enable and start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v2.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    # add hook to bash_completion in the standard debian location\n    echo '. /usr/share/bash-completion/bash_completion' > /etc/bash_completion\n\n    return 0\n}\n\n\nfunction uninstall {\n    rm -rf ${PYTHON_VENV}\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter/sudoers.d/99-dsiprouter",
    "content": "Cmnd_Alias DSIP_UPGRADE_CMDS = /usr/bin/dsiprouter upgrade, /usr/bin/dsiprouter upgrade *\nCmnd_Alias DSIP_RESTART_CMDS = /usr/bin/dsiprouter restart, /usr/bin/dsiprouter restart *\nCmnd_Alias DSIP_BACKUP_CMDS = /usr/bin/dsiprouter backup, /usr/bin/dsiprouter backup *\nCmnd_Alias DSIP_RESTORE_CMDS = /usr/bin/dsiprouter restore, /usr/bin/dsiprouter restore *\ndsiprouter ALL=(ALL) NOPASSWD:SETENV: DSIP_UPGRADE_CMDS\ndsiprouter ALL=(ALL) NOPASSWD: DSIP_RESTART_CMDS\ndsiprouter ALL=(ALL) NOPASSWD: DSIP_BACKUP_CMDS\ndsiprouter ALL=(ALL) NOPASSWD: DSIP_RESTORE_CMDS"
  },
  {
    "path": "dsiprouter/systemd/dsiprouter-v1.service",
    "content": "# Utilzing Type=simple while main process is still single threaded\n# Main process is managed via signals from systemd (stop/reload data/etc..)\n# When stopping the service a SIGTERM is sent immediately, to the main process\n# If, after 3s dsiprouter is still running, sends SIGKILL to all process\n#\n# The following are updated dynamically on install and should not be changed here:\n#   Environment=DSIP_PROJECT_DIR\n#   Environment=DSIP_RUN_DIR\n#   Environment=DSIP_SYSTEM_CONFIG_DIR\n[Unit]\nDescription=dSIPRouter Service\nDefaultDependencies=no\nRequires=basic.target network.target\nAfter=network.target network-online.target systemd-journald.socket basic.target\nAfter=rsyslog.service mariadb.service nginx.service kamailio.service\nWants=nginx.service mariadb.service kamailio.service\nStartLimitInterval=30\nStartLimitBurst=3\n\n[Service]\nType=simple\nPermissionsStartOnly=true\nUser=dsiprouter\nGroup=dsiprouter\nEnvironment='DSIP_PROJECT_DIR=/opt/dsiprouter'\nEnvironment='DSIP_RUN_DIR=/run/dsiprouter'\nEnvironment='DSIP_SYSTEM_CONFIG_DIR=/etc/dsiprouter'\n# PIDFile requires an absolute path\nPIDFile=/run/dsiprouter/dsiprouter.pid\n# ExecStart* requires an absolute path for the program\nExecStartPre=/usr/bin/dsiprouter chown -dsiprouter\nExecStart=/usr/bin/dsiprouter exec\nTimeoutStopSec=3\nKillMode=mixed\nRestart=on-failure\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "dsiprouter/systemd/dsiprouter-v2.service",
    "content": "# Utilzing Type=simple while main process is still single threaded\n# Main process is managed via signals from systemd (stop/reload data/etc..)\n# When stopping the service a SIGTERM is sent immediately, to the main process\n# If, after 3s dsiprouter is still running, sends SIGKILL to all process\n#\n# The following are updated dynamically on install and should not be changed here:\n#   Environment=DSIP_PROJECT_DIR\n#   Environment=DSIP_RUN_DIR\n#   Environment=DSIP_SYSTEM_CONFIG_DIR\n[Unit]\nDescription=dSIPRouter Service\nDefaultDependencies=no\nRequires=basic.target network.target\nAfter=network.target network-online.target systemd-journald.socket basic.target\nAfter=rsyslog.service mariadb.service nginx.service kamailio.service\nWants=nginx.service mariadb.service kamailio.service\nStartLimitIntervalSec=30\nStartLimitBurst=3\n\n[Service]\nType=simple\nUser=dsiprouter\nGroup=dsiprouter\nEnvironment='DSIP_PROJECT_DIR=/opt/dsiprouter'\nEnvironment='DSIP_RUN_DIR=/run/dsiprouter'\nEnvironment='DSIP_SYSTEM_CONFIG_DIR=/etc/dsiprouter'\nEnvironmentFile=-/etc/defaults/dsiprouter.conf\nEnvironmentFile=-/etc/defaults/dsiprouter.d/*.conf\n# PIDFile requires an absolute path\nPIDFile=/run/dsiprouter/dsiprouter.pid\n# ExecStart* requires an absolute path for the program\nExecStartPre=!-/usr/bin/dsiprouter chown -dsiprouter\nExecStart=/usr/bin/dsiprouter exec\nTimeoutStopSec=3\nKillMode=mixed\nRestart=on-failure\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "dsiprouter/ubuntu/20.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # Install dependencies for dSIPRouter\n    apt-get install -y build-essential curl python3 python3-pip python-dev python3-openssl python3-mysqldb \\\n        python3-venv libpq-dev firewalld sudo &&\n    apt-get install -y --allow-unauthenticated libmariadbclient-dev &&\n    apt-get install -y logrotate rsyslog perl sngrep libev-dev uuid-runtime libpq-dev\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # TODO: figure out why compiling ultradict with the other deps hangs\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install UltraDict &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v2.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    return 0\n}\n\nfunction uninstall() {\n    # Uninstall dependencies for dSIPRouter\n    cat ${DSIP_PROJECT_DIR}/gui/requirements.txt | xargs -n 1 $PYTHON_CMD -m pip uninstall --yes\n    if (( $? == 1 )); then\n        printerr \"dSIPRouter uninstall failed or the libraries are already uninstalled\"\n        exit 1\n    else\n        printdbg \"DSIPRouter uninstall was successful\"\n        exit 0\n    fi\n\n    apt-get remove -y build-essential curl python3 python3-pip python-dev python3-openssl libpq-dev firewalld\n    apt-get remove -y --allow-unauthenticated libmariadbclient-dev\n    apt-get remove -y logrotate rsyslog perl sngrep libev-dev uuid-runtime\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter/ubuntu/22.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # Install dependencies for dSIPRouter\n    apt-get install -y build-essential curl python3 python3-pip python-dev python3-openssl python3-mysqldb \\\n        python3-venv libpq-dev firewalld sudo &&\n    apt-get install -y --allow-unauthenticated libmariadbclient-dev &&\n    apt-get install -y logrotate rsyslog perl sngrep libev-dev uuid-runtime libpq-dev\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # TODO: figure out why compiling ultradict with the other deps hangs\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install UltraDict &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v2.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    return 0\n}\n\nfunction uninstall() {\n    apt-get remove -y curl python3 python3-pip python-dev python3-openssl libpq-dev firewalld\n    apt-get remove -y --allow-unauthenticated libmariadbclient-dev\n    apt-get remove -y logrotate rsyslog perl sngrep libev-dev uuid-runtime\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter/ubuntu/24.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create dsiprouter user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dsiprouter &>/dev/null; groupdel dsiprouter &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n\n    # Install Dependencies and remove any conflicting packages\n    apt-get remove -y ufw &&\n    apt-get install -y build-essential pkg-config python3-pip \\\n        python3-dev libpq-dev python3-venv libev-dev libffi-dev default-libmysqlclient-dev \\\n        curl python3 firewalld sudo logrotate rsyslog perl sngrep uuid-runtime &&\n    # Install libraries needed to install the python-ldap package\n    apt-get install -y libsasl2-dev python-dev-is-python3 libldap2-dev libssl-dev\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter\n    mkdir -p ${DSIP_RUN_DIR}\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall for DSIP_PORT\n    firewall-cmd --zone=public --add-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # TODO: figure out why compiling ultradict with the other deps hangs\n    python3 -m venv --upgrade-deps ${PYTHON_VENV} &&\n    ${PYTHON_CMD} -m pip install UltraDict &&\n    ${PYTHON_CMD} -m pip install -r ${DSIP_PROJECT_DIR}/gui/requirements.txt\n    if (( $? == 1 )); then\n        printerr \"Failed installing required python libraries\"\n        return 1\n    fi\n\n    # setup dsiprouter nginx configs\n    perl -e \"\\$dsip_port='${DSIP_PORT}'; \\$dsip_unix_sock='${DSIP_UNIX_SOCK}'; \\$dsip_ssl_cert='${DSIP_SSL_CERT}'; \\$dsip_ssl_key='${DSIP_SSL_KEY}';\" \\\n        -pe 's%DSIP_UNIX_SOCK%${dsip_unix_sock}%g; s%DSIP_PORT%${dsip_port}%g; s%DSIP_SSL_CERT%${dsip_ssl_cert}%g; s%DSIP_SSL_KEY%${dsip_ssl_key}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/dsiprouter.conf >/etc/nginx/sites-available/dsiprouter.conf\n    ln -sf /etc/nginx/sites-available/dsiprouter.conf /etc/nginx/sites-enabled/dsiprouter.conf\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup dSIPRouter Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/dsiprouter.conf /etc/rsyslog.d/dsiprouter.conf\n    touch /var/log/dsiprouter.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/dsiprouter /etc/logrotate.d/dsiprouter\n\n    # Install dSIPRouter as a service\n    perl -p \\\n        -e \"s|'DSIP_RUN_DIR\\=.*'|'DSIP_RUN_DIR=$DSIP_RUN_DIR'|;\" \\\n        -e \"s|'DSIP_PROJECT_DIR\\=.*'|'DSIP_PROJECT_DIR=$DSIP_PROJECT_DIR'|;\" \\\n        -e \"s|'DSIP_SYSTEM_CONFIG_DIR\\=.*'|'DSIP_SYSTEM_CONFIG_DIR=$DSIP_SYSTEM_CONFIG_DIR'|;\" \\\n        ${DSIP_PROJECT_DIR}/dsiprouter/systemd/dsiprouter-v2.service > /lib/systemd/system/dsiprouter.service\n    chmod 644 /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n    systemctl enable dsiprouter\n\n    return 0\n}\n\nfunction uninstall() {\n    apt-get remove -y curl python3 python3-pip python-dev python3-openssl libpq-dev firewalld\n    apt-get remove -y --allow-unauthenticated libmariadbclient-dev\n    apt-get remove -y logrotate rsyslog perl sngrep libev-dev uuid-runtime\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl stop dsiprouter.service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "dsiprouter.sh",
    "content": "#!/usr/bin/env bash\n#\n#=============== dSIPRouter Management Script ==============#\n#\n# install, configure, and manage dsiprouter\n#\n#========================== NOTES ==========================#\n#\n# Supported OS:\n# - Debian 12 (bookworm)    - STABLE\n# - Debian 11 (bullseye)    - STABLE\n# - Debian 10 (buster)      - STABLE\n# - Debian 9 (stretch)      - DEPRECATED\n# - CentOS 9 (stream)       - STABLE\n# - CentOS 8 (stream)       - STABLE\n# - CentOS 7                - DEPRECATED\n# - RedHat Linux 9          - STABLE\n# - RedHat Linux 8          - ALPHA\n# - Alma Linux 9            - STABLE\n# - Alma Linux 8            - BETA\n# - Rocky Linux 9           - STABLE\n# - Rocky Linux 8           - BETA\n# - Amazon Linux 2          - STABLE\n# - Ubuntu 24.04 (noble)    - STABLE\n# - Ubuntu 22.04 (jammy)    - ALPHA\n# - Ubuntu 20.04 (focal)    - DEPRECATED\n#\n# Conventions:\n# - In general exported variables & functions are used in externally called scripts / programs\n#\n# TODO:\n# - allow user to move carriers freely between carrier groups\n# - allow a carrier to be in more than one carrier group\n# - add ncurses selection menu for enabling / disabling modules\n# - naming convention for system vs dsip config files is very confusing (make more explicit)\n# - cleanup dependency installs/checks, many of these could be condensed\n# - allow overwriting caller id per gwgroup / gw (setup in gui & kamcfg)\n# - update tests with new mysql command wrapper functions\n# - update HA scripts with new mysql command wrapper functions\n# - add documentation generation to supported CLI commands\n# - move python install into it's own script to allow fine grain control of version/compilation if needed\n#\n#============== Detailed Debugging Information =============#\n# - splits stdout, stderr, and trace streams into 3 files\n# - output files are timestamped throughout process (cpu intensive)\n# - useful for tracking down bugs, especially when a lot of output is produced\n# - the gawk version seems to be more efficient but mawk is supported as well\n#\n#mkdir -p /tmp/debug && rm -f /tmp/debug/*.log 2>/dev/null\n#\n# - gawk version (alias awk='gawk')\n#exec   > >(awk '{ print strftime(\"[%Y-%m-%d_%H:%M:%S] \"), $0; fflush(); }' | tee -ia /tmp/debug/stdout.log)\n#exec  2> >(awk '{ print strftime(\"[%Y-%m-%d_%H:%M:%S] \"), $0; fflush(); }' | tee -ia /tmp/debug/stderr.log 1>&2)\n#exec 19> >(awk '{ print strftime(\"[%Y-%m-%d_%H:%M:%S] \"), $0; fflush(); }' > /tmp/debug/trace.log)\n# - mawk version (alias awk='mawk')\n#exec   > >(awk -v time=$(date +\"[%Y-%m-%d_%H:%M:%S] \") '{ print time, $0; fflush(); }' | tee -ia /tmp/debug/stdout.log)\n#exec  2> >(awk -v time=$(date +\"[%Y-%m-%d_%H:%M:%S] \") '{ print time, $0; fflush(); }' | tee -ia /tmp/debug/stderr.log 1>&2)\n#exec 19> >(awk -v time=$(date +\"[%Y-%m-%d_%H:%M:%S] \") '{ print time, $0; fflush(); }' > /tmp/debug/trace.log)\n#\n#BASH_XTRACEFD=\"19\"\n#set -x\n#===========================================================#\n\n\n# set project dir (where src files are located)\nexport DSIP_PROJECT_DIR=${DSIP_PROJECT_DIR:-$(dirname $(readlink -f \"$0\"))}\n# Import dsip_lib utility / shared functions\n. ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\n\n\n# settings used by script that are user configurable\nfunction setStaticScriptSettings() {\n    # to be clear, we define constants or variables with defaults here\n    # generally these configuration settings effect how this script or the platform operate\n    # do not change these settings without knowing exactly how it effects normal operation\n    FLT_CARRIER=8\n    FLT_PBX=9\n    FLT_MSTEAMS=17\n    FLT_OUTBOUND=8000\n    FLT_INBOUND=9000\n    FLT_LCR_MIN=10000\n    FLT_FWD_MIN=20000\n    WITH_LCR=1\n    export DEBUG=${DEBUG:-0}\n    export TEAMS_ENABLED=1\n    DSIP_MIN_PYTHON_VER='3.8'\n    export PYTHON_VENV=\"${DSIP_PROJECT_DIR}/venv\"\n    export PYTHON_CMD=\"${PYTHON_VENV}/bin/python\"\n    export PROJECT_KAMAILIO_CONFIG_DIR=\"${DSIP_PROJECT_DIR}/kamailio/configs\"\n    export PROJECT_DSIP_DEFAULTS_DIR=\"${DSIP_PROJECT_DIR}/kamailio/defaults\"\n    export DSIP_SYSTEM_CONFIG_DIR=\"/etc/dsiprouter\"\n    DSIP_PRIV_KEY=\"${DSIP_SYSTEM_CONFIG_DIR}/privkey\"\n    export DSIP_KAMAILIO_CONFIG_FILE=\"${DSIP_SYSTEM_CONFIG_DIR}/kamailio/kamailio.cfg\"\n    export DSIP_KAMAILIO_TLS_CONFIG_FILE=\"${DSIP_SYSTEM_CONFIG_DIR}/kamailio/tls.cfg\"\n    export DSIP_CONFIG_FILE=\"${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py\"\n    export DSIP_RUN_DIR=\"/run/dsiprouter\"\n    export DSIP_LIB_DIR=\"/var/lib/dsiprouter\"\n    export DSIP_CERTS_DIR=\"${DSIP_SYSTEM_CONFIG_DIR}/certs\"\n    DSIP_DOCS_DIR=\"${DSIP_PROJECT_DIR}/docs/build/html\"\n    export SYSTEM_KAMAILIO_CONFIG_DIR=\"/etc/kamailio\"\n    export SYSTEM_KAMAILIO_CONFIG_FILE=\"${SYSTEM_KAMAILIO_CONFIG_DIR}/kamailio.cfg\" # will be symlinked\n    export SYSTEM_KAMAILIO_TLS_CONFIG_FILE=\"${SYSTEM_KAMAILIO_CONFIG_DIR}/tls.cfg\" # will be symlinked\n    export SYSTEM_RTPENGINE_CONFIG_DIR=\"/etc/rtpengine\"\n    export SYSTEM_RTPENGINE_CONFIG_FILE=\"${SYSTEM_RTPENGINE_CONFIG_DIR}/rtpengine.conf\"\n    export PATH_UPDATE_FILE=\"/etc/profile.d/dsip_paths.sh\" # updates paths required\n    GIT_UPDATE_FILE=\"/etc/profile.d/dsip_git.sh\" # extends git command\n    DSIP_SUDOERS_FILE=\"/etc/sudoers.d/99-dsiprouter\"\n    export SRC_DIR=\"/usr/local/src\"\n    export BACKUPS_DIR=\"/var/backups/dsiprouter\"\n    IMAGE_BUILD=${IMAGE_BUILD:-0}\n    # TODO: marked for review in v0.78\n    #       we should move these settings to the OS specific install scripts\n    #       OS vendors may change these locations in the future\n    if [[ \"$DISTRO\" == \"ubuntu\" ]] && (( ${DISTRO_MAJOR_VER} >= 24 )); then\n        APT_OFFICIAL_SOURCES=\"/etc/apt/sources.d/ubuntu.sources\"\n        APT_OFFICIAL_SOURCES_BAK=\"${BACKUPS_DIR}/original-sources.sources\"\n    else\n        APT_OFFICIAL_SOURCES=\"/etc/apt/sources.list\"\n        APT_OFFICIAL_SOURCES_BAK=\"${BACKUPS_DIR}/original-sources.list\"\n    fi\n    APT_OFFICIAL_PREFS=\"/etc/apt/preferences\"\n    APT_OFFICIAL_PREFS_BAK=\"${BACKUPS_DIR}/original-sources.pref\"\n    APT_DSIP_CONFIG=\"/etc/apt/apt.conf.d/99dsiprouter\"\n    YUM_OFFICIAL_REPOS=\"/etc/yum.repos.d/official-releases.repo\"\n\n    # Force the installation of an Kamailio version by uncommenting\n    # can also be set as an environment variable\n    #KAM_VERSION=5.8.3\n\n    # Force the installation of an RTPEngine version by uncommenting\n    # can also be set as an environment variable\n    #RTPENGINE_VER=\"mr11.5.1.11\"\n\n    # Network configuration values\n    export DSIP_UNIX_SOCK='/run/dsiprouter/dsiprouter.sock'\n    export DSIP_PORT=5000\n    export RTP_PORT_MIN=10000\n    export RTP_PORT_MAX=20000\n    export KAM_SIP_PORT=5060\n    export KAM_SIPS_PORT=5061\n    export KAM_DMQ_PORT=5090\n    export KAM_WSS_PORT=4443\n\n    export DSIP_PROTO='https'\n    export DSIP_API_PROTO='https'\n    export DSIP_SSL_KEY=\"${DSIP_CERTS_DIR}/dsiprouter-key.pem\"\n    export DSIP_SSL_CERT=\"${DSIP_CERTS_DIR}/dsiprouter-cert.pem\"\n    export DSIP_SSL_CA=\"${DSIP_CERTS_DIR}/ca-list.pem\"\n\n    # make sure we run package installs unattended\n    if cmdExists 'apt-get'; then\n        export DEBIAN_FRONTEND=\"noninteractive\"\n        export DEBIAN_PRIORITY=\"critical\"\n    fi\n    # make perl CPAN installs non interactive\n    export PERL_MM_USE_DEFAULT=1\n}\n\n# NOTE: will set NETWORK_MODE in current scope\nfunction preProcessNetworkMode() {\n    local ARG=\"$1\" OPT=\"\"\n\n    # TEMP: parse these options ahead of time until we can move arg parsing ahead of this logic\n    # [note to self] this will require preempting undefined functions and/or some porting to bash-native versions of parsing logic\n    if [[ \"$ARG\" == \"install\" ]]; then\n        shift\n        for OPT in \"$@\"; do\n            case $OPT in\n                -dmz|--dmz=*)\n                    NETWORK_MODE=2\n                    if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                        TMP=$(echo \"$1\" | cut -d '=' -f 2)\n                        shift\n                    else\n                        shift\n                        TMP=\"$1\"\n                        shift\n                    fi\n                    PUBLIC_IFACE=$(echo \"$TMP\" | cut -d ',' -f 1)\n                    PRIVATE_IFACE=$(echo \"$TMP\" | cut -d ',' -f 2)\n                    ;;\n                -netm|--network-mode=*)\n                    if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                        NETWORK_MODE=$(echo \"$1\" | cut -d '=' -f 2)\n                        shift\n                    else\n                        shift\n                        NETWORK_MODE=\"$1\"\n                        shift\n                    fi\n                    ;;\n            esac\n        done\n    fi\n\n    # network settings determined by mode\n    NETWORK_MODE=${NETWORK_MODE:-$(getConfigAttrib 'NETWORK_MODE' ${DSIP_CONFIG_FILE})}\n    NETWORK_MODE=${NETWORK_MODE:-0}\n\n    if [[ \"$ARG\" == \"install\" && ! -f \"$DSIP_INIT_FILE\" ]]; then\n        # special use case\n        # on install before dsip-init is installed the network changes must be manually applied\n        # this must happen prior to setting the script variables used throughout the install portion\n        # WARNING: this may change the default route\n        setCloudPlatform\n        ${DSIP_PROJECT_DIR}/dsiprouter/dsip-net-cfg.py ${CLOUD_PLATFORM} ${NETWORK_MODE}\n    fi\n}\n\n# settings used by script that are generated by the script\n# NOTE: variable NETWORK_MODE should be set before running this\nfunction setDynamicScriptSettings() {\n    # TODO: ipv6 intentionally disabled here\n    export IPV6_ENABLED=0\n\n    # grab the network settings dynamically\n    if (( $NETWORK_MODE == 0 )); then\n        export INTERNAL_IP_ADDR=$(getInternalIP -4)\n        export INTERNAL_IP_NET=$(getInternalCIDR -4)\n        export INTERNAL_IP6_ADDR=$(getInternalIP -6)\n        export INTERNAL_IP_NET6=$(getInternalCIDR -6)\n\n        # if external ip address is not found then this box is on an internal subnet\n        EXTERNAL_IP_ADDR=$(getExternalIP -4)\n        export EXTERNAL_IP_ADDR=${EXTERNAL_IP_ADDR:-$INTERNAL_IP_ADDR}\n        EXTERNAL_IP6_ADDR=$(getExternalIP -6)\n        export EXTERNAL_IP6_ADDR=${EXTERNAL_IP6_ADDR:-$INTERNAL_IP6_ADDR}\n\n        # determine whether ipv6 is enabled\n        # /proc/net/if_inet6 tells us if the kernel has ipv6 enabled\n#\t\tif [[ -f /proc/net/if_inet6 ]] && [[ -n \"$INTERNAL_IP6_ADDR\" ]]; then\n#\t\t\t# sanity check, is the ipv6 address routable?\n#\t\t\t# if not we can not use this address (interface is not configured properly)\n#\t\t\tif ! checkConn \"$INTERNAL_IP6_ADDR\"; then\n#\t\t\t\tprinterr \"IPV6 enabled but address [$INTERNAL_IP6_ADDR] is not routable\"\n#\t\t\t\texit 1\n#\t\t\tfi\n#\t\t\texport IPV6_ENABLED=1\n#\t\telse\n#\t\t\texport IPV6_ENABLED=0\n#\t\tfi\n\n        # the address we put in the contact when registering to carriers via uac module\n        # by default it is set to the external IP of this server\n        export UAC_REG_ADDR=\"$EXTERNAL_IP_ADDR\"\n\n        export INTERNAL_FQDN=$(getInternalFQDN)\n        export EXTERNAL_FQDN=$(getExternalFQDN)\n        if [[ -z \"$EXTERNAL_FQDN\" ]] || ! checkConn \"$EXTERNAL_FQDN\"; then\n            # if external fqdn is not routable set it to the internal fqdn instead\n            export EXTERNAL_FQDN=\"$INTERNAL_FQDN\"\n        fi\n\n        # set the external fqdn to the internal fqdn if the hostname contain vultrusercontent\n        # Kamailio doesn't like hostname names with dots and LetsEncrypt can't create certs for that domain\n        grep vultrusercontent <<< \"$EXTERNAL_FQDN\" >/dev/null\n        if (( $? == 0 ));then\n            export EXTERNAL_FQDN=\"$INTERNAL_FQDN\"\n        fi\n\n    # network settings pulled from env variables or from config file\n    elif (( $NETWORK_MODE == 1 )); then\n        export INTERNAL_IP_ADDR=${INTERNAL_IP_ADDR:-$(getConfigAttrib 'INTERNAL_IP_ADDR' ${DSIP_CONFIG_FILE})}\n        export INTERNAL_IP_NET=${INTERNAL_IP_NET:-$(getConfigAttrib 'INTERNAL_IP_NET' ${DSIP_CONFIG_FILE})}\n        export INTERNAL_IP6_ADDR=${INTERNAL_IP6_ADDR:-$(getConfigAttrib 'INTERNAL_IP6_ADDR' ${DSIP_CONFIG_FILE})}\n        export INTERNAL_IP_NET6=${INTERNAL_IP_NET6:-$(getConfigAttrib 'INTERNAL_IP_NET6' ${DSIP_CONFIG_FILE})}\n\n        export EXTERNAL_IP_ADDR=${EXTERNAL_IP_ADDR:-$(getConfigAttrib 'EXTERNAL_IP_ADDR' ${DSIP_CONFIG_FILE})}\n        export EXTERNAL_IP6_ADDR=${EXTERNAL_IP6_ADDR:-$(getConfigAttrib 'EXTERNAL_IP6_ADDR' ${DSIP_CONFIG_FILE})}\n\n#\t\tif [[ -n \"$IPV6_ENABLED\" ]]; then\n#\t\t\texport IPV6_ENABLED\n#\t\telse\n#\t\t\t[[ \"$(getConfigAttrib 'IPV6_ENABLED' ${DSIP_CONFIG_FILE})\" == \"True\" ]] &&\n#\t\t\t\texport IPV6_ENABLED=1 ||\n#\t\t\t\texport IPV6_ENABLED=0\n#\t\tfi\n\n        export INTERNAL_FQDN=${INTERNAL_FQDN:-$(getConfigAttrib 'INTERNAL_FQDN' ${DSIP_CONFIG_FILE})}\n        export EXTERNAL_FQDN=${EXTERNAL_FQDN:-$(getConfigAttrib 'EXTERNAL_FQDN' ${DSIP_CONFIG_FILE})}\n        export UAC_REG_ADDR=${UAC_REG_ADDR:-$(getConfigAttrib 'UAC_REG_ADDR' ${DSIP_CONFIG_FILE})}\n    # network settings resolved dynamically except IP/subnets (they are resolved by interfaces from CLI args or from the config)\n    elif (( $NETWORK_MODE == 2 )); then\n        PUBLIC_IFACE=${PUBLIC_IFACE:-$(getConfigAttrib 'PUBLIC_IFACE' ${DSIP_CONFIG_FILE})}\n        PRIVATE_IFACE=${PRIVATE_IFACE:-$(getConfigAttrib 'PRIVATE_IFACE' ${DSIP_CONFIG_FILE})}\n\n        export INTERNAL_IP_ADDR=$(getIP -4 \"$PRIVATE_IFACE\")\n        export INTERNAL_IP_NET=$(getInternalCIDR -4 \"$PRIVATE_IFACE\")\n        export INTERNAL_IP6_ADDR=$(getIP -6 \"$PRIVATE_IFACE\")\n        export INTERNAL_IP_NET6=$(getInternalCIDR -6 \"$PRIVATE_IFACE\")\n\n        EXTERNAL_IP_ADDR=$(getIP -4 \"$PUBLIC_IFACE\")\n        export EXTERNAL_IP_ADDR=${EXTERNAL_IP_ADDR:-$INTERNAL_IP_ADDR}\n        EXTERNAL_IP6_ADDR=$(getIP -6 \"$PUBLIC_IFACE\")\n        export EXTERNAL_IP6_ADDR=${EXTERNAL_IP6_ADDR:-$INTERNAL_IP6_ADDR}\n\n#\t\tif [[ -f /proc/net/if_inet6 ]] && [[ -n \"$INTERNAL_IP6_ADDR\" ]]; then\n#\t\t\t# sanity check, is the ipv6 address routable?\n#\t\t\t# if not we can not use this address (interface is not configured properly)\n#\t\t\tif ! checkConn \"$INTERNAL_IP6_ADDR\"; then\n#\t\t\t\tprinterr \"IPV6 enabled but address [$INTERNAL_IP6_ADDR] is not routable\"\n#\t\t\t\texit 1\n#\t\t\tfi\n#\t\t\texport IPV6_ENABLED=1\n#\t\telse\n#\t\t\texport IPV6_ENABLED=0\n#\t\tfi\n\n        # the address we put in the contact when registering to carriers via uac module\n        # by default it is set to the external IP of this server\n        export UAC_REG_ADDR=\"$EXTERNAL_IP_ADDR\"\n\n        export INTERNAL_FQDN=$(getInternalFQDN)\n        export EXTERNAL_FQDN=$(getExternalFQDN)\n        if [[ -z \"$EXTERNAL_FQDN\" ]] || ! checkConn \"$EXTERNAL_FQDN\"; then\n            # if external fqdn is not routable set it to the internal fqdn instead\n            export EXTERNAL_FQDN=\"$INTERNAL_FQDN\"\n        fi\n    else\n        printerr 'Network Mode is invalid, can not proceed any further'\n        exit 1\n    fi\n\n    # if the public ip address is not the same as the internal address then enable serverside NAT\n    if [[ \"$EXTERNAL_IP_ADDR\" != \"$INTERNAL_IP_ADDR\" ]]; then\n        export SIGNAL_SERVERNAT=1\n    else\n        export SIGNAL_SERVERNAT=0\n    fi\n    # same as above but for ipv6, note that NAT is rarely used on ipv6 networks\n    if (( ${IPV6_ENABLED} == 1 )) && [[ \"$EXTERNAL_IP6_ADDR\" != \"$INTERNAL_IP6_ADDR\" ]]; then\n        export SIGNAL_SERVERNAT6=1\n    else\n        export SIGNAL_SERVERNAT6=0\n    fi\n\n    # grab root db settings from env or settings file\n    export ROOT_DB_USER=${ROOT_DB_USER:-$(getConfigAttrib 'ROOT_DB_USER' ${DSIP_CONFIG_FILE})}\n    export ROOT_DB_PASS=${ROOT_DB_PASS:-$(decryptConfigAttrib 'ROOT_DB_PASS' ${DSIP_CONFIG_FILE})}\n    export ROOT_DB_HOST=${ROOT_DB_HOST:-$(getConfigAttrib 'ROOT_DB_HOST' ${DSIP_CONFIG_FILE})}\n    export ROOT_DB_PORT=${ROOT_DB_PORT:-$(getConfigAttrib 'ROOT_DB_PORT' ${DSIP_CONFIG_FILE})}\n    export ROOT_DB_NAME=${ROOT_DB_NAME:-$(getConfigAttrib 'ROOT_DB_NAME' ${DSIP_CONFIG_FILE})}\n\n    # grab kam db settings from env or settings file\n    export KAM_DB_HOST=${KAM_DB_HOST:-$(getConfigAttrib 'KAM_DB_HOST' ${DSIP_CONFIG_FILE})}\n    export KAM_DB_TYPE=${KAM_DB_TYPE:-$(getConfigAttrib 'KAM_DB_TYPE' ${DSIP_CONFIG_FILE})}\n    export KAM_DB_PORT=${KAM_DB_PORT:-$(getConfigAttrib 'KAM_DB_PORT' ${DSIP_CONFIG_FILE})}\n    export KAM_DB_NAME=${KAM_DB_NAME:-$(getConfigAttrib 'KAM_DB_NAME' ${DSIP_CONFIG_FILE})}\n    export KAM_DB_USER=${KAM_DB_USER:-$(getConfigAttrib 'KAM_DB_USER' ${DSIP_CONFIG_FILE})}\n    export KAM_DB_PASS=${KAM_DB_PASS:-$(decryptConfigAttrib 'KAM_DB_PASS' ${DSIP_CONFIG_FILE} 2>/dev/null)}\n\n    # set the email used to obtain LetsEncrypt Certificates\n    export DSIP_SSL_EMAIL=\"admin@${EXTERNAL_FQDN}\"\n\n    export DSIP_ID=$(getConfigAttrib 'DSIP_ID' ${DSIP_CONFIG_FILE})\n    if [[ \"$DSIP_ID\" == \"None\" || -z \"$DSIP_ID\" ]]; then\n        export DSIP_ID=$(cat /etc/machine-id | hashCreds)\n    fi\n\n    export HOMER_ID=$(getConfigAttrib 'HOMER_ID' ${DSIP_CONFIG_FILE})\n    if [[ \"$HOMER_ID\" == \"None\" ]] || [[ -z \"$HOMER_ID\" ]]; then\n        export HOMER_ID=$(cat /etc/machine-id | hashCreds -l 4 | dd if=/dev/stdin of=/dev/stdout bs=1 count=8 2>/dev/null | hextoint)\n    fi\n\n    # find the repo where we are getting upgrades from\n    # note that remote is assumed to be \"origin\"\n    # note that the VCS is assumed to be git\n    GIT_REPO_URL=$(getConfigAttrib 'GIT_REPO_URL' ${DSIP_CONFIG_FILE})\n    GIT_RELEASE_URL=$(getConfigAttrib 'GIT_RELEASE_URL' ${DSIP_CONFIG_FILE})\n\n    export CURR_BACKUP_DIR=${CURR_BACKUP_DIR:-\"${BACKUPS_DIR}/$(date '+%s')\"}\n}\n\n# Check if we are on a VPS Cloud Instance\nfunction setCloudPlatform() {\n    # if we already set the cloud platform return immediately\n    if [[ ! -z \"${CLOUD_PLATFORM+x}\" ]]; then\n        return\n    fi\n\n    # 0 == not enabled, 1 == enabled\n    export AWS_ENABLED=0\n    export DO_ENABLED=0\n    export GCE_ENABLED=0\n    export AZURE_ENABLED=0\n    export VULTR_ENABLED=0\n\n    # -- amazon web service check --\n    if isInstanceAMI; then\n        export AWS_ENABLED=1\n        CLOUD_PLATFORM='AWS'\n    # -- digital ocean check --\n    elif isInstanceDO; then\n        export DO_ENABLED=1\n        CLOUD_PLATFORM='DO'\n    # -- google compute engine check --\n    elif isInstanceGCE; then\n        export GCE_ENABLED=1\n        CLOUD_PLATFORM='GCE'\n    # -- microsoft azure check --\n    elif isInstanceAZURE; then\n        export AZURE_ENABLED=1\n        CLOUD_PLATFORM='AZURE'\n    # -- vultr cloud check --\n    elif isInstanceVULTR; then\n        export VULTR_ENABLED=1\n        CLOUD_PLATFORM='VULTR'\n    # -- bare metal or unsupported cloud platform --\n    else\n        CLOUD_PLATFORM=''\n    fi\n}\n\nfunction displayLogo() {\necho \"CiAgICAgXyAgX19fX18gX19fX18gX19fX18gIF9fX19fICAgICAgICAgICAgIF8gCiAgICB8IHwv\nIF9fX198XyAgIF98ICBfXyBcfCAgX18gXCAgICAgICAgICAgfCB8ICAgICAgICAgICAKICBfX3wg\nfCAoX19fICAgfCB8IHwgfF9fKSB8IHxfXykgfF9fXyAgXyAgIF98IHxfIF9fXyBfIF9fIAogLyBf\nYCB8XF9fXyBcICB8IHwgfCAgX19fL3wgIF8gIC8vIF8gXHwgfCB8IHwgX18vIF8gXCAnX198Cnwg\nKF98IHxfX19fKSB8X3wgfF98IHwgICAgfCB8IFwgXCAoXykgfCB8X3wgfCB8fCAgX18vIHwgICAK\nIFxfXyxffF9fX19fL3xfX19fX3xffCAgICB8X3wgIFxfXF9fXy8gXF9fLF98XF9fXF9fX3xffCAg\nIAoKQnVpbHQgaW4gRGV0cm9pdCwgVVNBIC0gUG93ZXJlZCBieSBLYW1haWxpbyAgICAgICAgICAg\nICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgClN1cHBv\ncnQgY2FuIGJlIHB1cmNoYXNlZCBmcm9tIGh0dHBzOi8vZHNpcHJvdXRlci5vcmcvIAoKVGhhbmtz\nIHRvIG91ciBzcG9uc29yOiBkT3BlblNvdXJjZSAoaHR0cHM6Ly9kb3BlbnNvdXJjZS5jb20pCg==\" \\\n| base64 -d \\\n| { echo -e \"\\e[1;49;36m\"; cat; echo -e \"\\e[39;49;00m\"; }\n}\n\n# check if running as root\nfunction validateRootPriv() {\n    if (( $(id -u 2>/dev/null) != 0 )); then\n        printerr \"$0 must be run as root user\"\n        exit 1\n    fi\n}\n\n# Validate OS and export OS specific config variables\nfunction validateOSInfo() {\n    export DISTRO=$(getDistroName)\n    export DISTRO_VER=$(getDistroVer)\n    export DISTRO_MAJOR_VER=$(cut -d '.' -f 1 <<<\"$DISTRO_VER\")\n    export DISTRO_MINOR_VER=$(cut -s -d '.' -f 2 <<<\"$DISTRO_VER\")\n\n    case \"$DISTRO\" in\n    debian)\n        case \"$DISTRO_VER\" in\n        12|11|10)\n            KAM_VERSION=${KAM_VERSION:-\"5.8.3\"}\n            RTPENGINE_VER=${RTPENGINE_VER:-\"mr11.5.1.11\"}\n            ;;\n        9)\n            printerr \"Your Operating System Version is DEPRECATED. To ask for support open an issue https://github.com/dOpensource/dsiprouter/\"\n            KAM_VERSION=${KAM_VERSION:-\"5.5.7\"}\n            RTPENGINE_VER=${RTPENGINE_VER:-\"mr9.5.5.1\"}\n            ;;\n        *)\n            printerr \"Your Operating System Version is not supported yet. Please open an issue at https://github.com/dOpensource/dsiprouter/\"\n            exit 1\n            ;;\n        esac\n        ;;\n    centos)\n        case \"$DISTRO_VER\" in\n        9)\n            KAM_VERSION=${KAM_VERSION:-\"6.0.2\"}\n            RTPENGINE_VER=${RTPENGINE_VER:-\"mr11.5.1.11\"}\n            ;;\n        8)\n            KAM_VERSION=${KAM_VERSION:-\"5.8.3\"}\n            RTPENGINE_VER=${RTPENGINE_VER:-\"mr11.5.1.11\"}\n            ;;\n        7)\n            printwarn \"Your Operating System Version is DEPRECATED. To ask for support open an issue https://github.com/dOpensource/dsiprouter/\"\n            KAM_VERSION=${KAM_VERSION:-\"5.7.6\"}\n            RTPENGINE_VER=${RTPENGINE_VER:-\"mr11.5.1.11\"}\n            ;;\n        *)\n            printerr \"Your Operating System Version is not supported yet. Please open an issue at https://github.com/dOpensource/dsiprouter/\"\n            exit 1\n            ;;\n        esac\n        ;;\n    amzn)\n        case \"$DISTRO_VER\" in\n        2)\n            KAM_VERSION=${KAM_VERSION:-\"5.7.6\"}\n            RTPENGINE_VER=${RTPENGINE_VER:-\"mr11.5.1.11\"}\n            ;;\n        *)\n            printerr \"Your Operating System Version is not supported yet. Please open an issue at https://github.com/dOpensource/dsiprouter/\"\n            exit 1\n            ;;\n        esac\n        ;;\n    ubuntu)\n        case \"$DISTRO_VER\" in\n        24.04)\n            KAM_VERSION=${KAM_VERSION:-\"5.8.4\"}\n            RTPENGINE_VER=${RTPENGINE_VER:-\"mr11.5.1.11\"}\n            ;;\n        22.04)\n            printwarn \"Your operating System Version is in ALPHA support. Some features may not work yet. Use at your own risk.\"\n            KAM_VERSION=${KAM_VERSION:-\"5.8.3\"}\n            RTPENGINE_VER=${RTPENGINE_VER:-\"mr11.5.1.11\"}\n            ;;\n        20.04)\n            printwarn \"Your Operating System Version is DEPRECATED. To ask for support open an issue https://github.com/dOpensource/dsiprouter/\"\n            KAM_VERSION=${KAM_VERSION:-\"5.8.3\"}\n            RTPENGINE_VER=${RTPENGINE_VER:-\"mr9.5.5.1\"}\n            ;;\n        *)\n            printerr \"Your Operating System Version is not supported yet. Please open an issue at https://github.com/dOpensource/dsiprouter/\"\n            exit 1\n            ;;\n        esac\n        ;;\n    rhel)\n        case \"$DISTRO_MAJOR_VER\" in\n        9)\n            KAM_VERSION=${KAM_VERSION:-\"5.8.3\"}\n            RTPENGINE_VER=${RTPENGINE_VER:-\"mr11.5.1.11\"}\n            ;;\n        8)\n            printwarn \"Your operating System Version is in ALPHA support. Some features may not work yet. Use at your own risk.\"\n            KAM_VERSION=${KAM_VERSION:-\"5.8.3\"}\n            RTPENGINE_VER=${RTPENGINE_VER:-\"mr9.5.5.1\"}\n            ;;\n        *)\n            printerr \"Your Operating System Version is not supported yet. Please open an issue at https://github.com/dOpensource/dsiprouter/\"\n            exit 1\n            ;;\n        esac\n        ;;\n    almalinux)\n        case \"$DISTRO_MAJOR_VER\" in\n        9)\n            KAM_VERSION=${KAM_VERSION:-\"5.8.3\"}\n            RTPENGINE_VER=${RTPENGINE_VER:-\"mr11.5.1.11\"}\n            ;;\n        8)\n            printwarn \"Your operating System Version is in BETA support. Some features may have bugs. Use at your own risk.\"\n            KAM_VERSION=${KAM_VERSION:-\"5.8.3\"}\n            RTPENGINE_VER=${RTPENGINE_VER:-\"mr11.5.1.11\"}\n            ;;\n        *)\n            printerr \"Your Operating System Version is not supported yet. Please open an issue at https://github.com/dOpensource/dsiprouter/\"\n            exit 1\n            ;;\n        esac\n        ;;\n    rocky)\n        case \"$DISTRO_MAJOR_VER\" in\n        9)\n            KAM_VERSION=${KAM_VERSION:-\"5.8.3\"}\n            RTPENGINE_VER=${RTPENGINE_VER:-\"mr11.5.1.11\"}\n            ;;\n        8)\n            printwarn \"Your operating System Version is in BETA support. Some features may have bugs. Use at your own risk.\"\n            KAM_VERSION=${KAM_VERSION:-\"5.8.3\"}\n            RTPENGINE_VER=${RTPENGINE_VER:-\"mr11.5.1.11\"}\n            ;;\n        *)\n            printerr \"Your Operating System Version is not supported yet. Please open an issue at https://github.com/dOpensource/dsiprouter/\"\n            exit 1\n            ;;\n        esac\n        ;;\n    *)\n        printerr \"Your Operating System is not supported yet. Please open an issue at https://github.com/dOpensource/dsiprouter/\"\n        exit 1\n        ;;\n    esac\n\n    # export it for external scripts\n    export KAM_VERSION\n    export RTPENGINE_VER\n}\n\n# run prior to any cmd being processed\nfunction initialChecks() {\n    validateRootPriv\n    validateOSInfo\n    setStaticScriptSettings\n    setupScriptRequiredFiles\n    configureSystemRepos\n    installScriptRequirements\n    preProcessNetworkMode \"$@\"\n    setDynamicScriptSettings\n}\n\n# exported because its used throughout child scripts as well\nfunction reconfigureMysqlSystemdService() {\n    local KAM_DB_HOST=\"${SET_KAM_DB_HOST:-$KAM_DB_HOST}\"\n    KAM_DB_HOST=${KAM_DB_HOST:-$(getConfigAttrib 'KAM_DB_HOST' ${DSIP_CONFIG_FILE})}\n\n    if isHostLocal \"$KAM_DB_HOST\"; then\n        # in this case mysql DBMS is running locally on this server\n        rm -f /etc/systemd/system/mariadb.service 2>/dev/null\n    else\n        # in this case mysql DBMS is running on a remote server\n        cp -f ${DSIP_PROJECT_DIR}/mysql/systemd/dummy.service /etc/systemd/system/mariadb.service\n        chmod 644 /etc/systemd/system/mariadb.service\n    fi\n\n    systemctl daemon-reload\n    systemctl enable mariadb\n}\nexport -f reconfigureMysqlSystemdService\n\n# note: exports variable MEDIA_SERVERNAT\nfunction reconfigureRtpengineSystemdService() {\n    local RTPENGINE_URI=${RTPENGINE_URI:-$(getConfigAttrib 'RTPENGINE_URI' ${DSIP_CONFIG_FILE})}\n    local RTPENGINE_HOST=$(cut -s -d ':' -f 2 <<<\"$RTPENGINE_URI\")\n\n    if isHostLocal \"$RTPENGINE_HOST\"; then\n        # in this case rtpengine is running locally on this server\n        export MEDIA_SERVERNAT=1\n        rm -f /etc/systemd/system/rtpengine.service 2>/dev/null\n    else\n        # in this case rtpengine is running on a remote server\n        export MEDIA_SERVERNAT=0\n        cp -f ${DSIP_PROJECT_DIR}/rtpengine/systemd/dummy.service /etc/systemd/system/rtpengine.service\n        chmod 644 /etc/systemd/system/rtpengine.service\n    fi\n\n    systemctl daemon-reload\n    systemctl enable rtpengine\n}\n\nfunction generateDsiprouterConfig() {\n    mkdir -p ${BACKUPS_DIR}/gui/\n    cp -f ${DSIP_SYSTEM_CONFIG_DIR}/gui/*.py ${BACKUPS_DIR}/gui/ 2>/dev/null\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/gui/*.py 2>/dev/null\n    cp -f ${DSIP_PROJECT_DIR}/gui/settings.py ${DSIP_CONFIG_FILE}\n}\n\nfunction updateDsiprouterConfig() {\n    local NETWORK_MODE=${NETWORK_MODE:-$(getConfigAttrib 'NETWORK_MODE' ${DSIP_CONFIG_FILE})}\n    local LOAD_SETTINGS_FROM=${LOAD_SETTINGS_FROM:-$(getConfigAttrib 'LOAD_SETTINGS_FROM' ${DSIP_CONFIG_FILE})}\n\n    # the following variables are always updated\n    setConfigAttrib 'KAM_KAMCMD_PATH' \"$(type -p kamcmd)\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'KAM_CFG_PATH' \"$SYSTEM_KAMAILIO_CONFIG_FILE\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'KAM_TLSCFG_PATH' \"$SYSTEM_KAMAILIO_TLS_CONFIG_FILE\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'RTP_CFG_PATH' \"$SYSTEM_RTPENGINE_CONFIG_FILE\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'FLT_CARRIER' \"$FLT_CARRIER\" ${DSIP_CONFIG_FILE}\n    setConfigAttrib 'FLT_PBX' \"$FLT_PBX\" ${DSIP_CONFIG_FILE}\n    setConfigAttrib 'FLT_MSTEAMS' \"$FLT_MSTEAMS\" ${DSIP_CONFIG_FILE}\n    setConfigAttrib 'FLT_OUTBOUND' \"$FLT_OUTBOUND\" ${DSIP_CONFIG_FILE}\n    setConfigAttrib 'FLT_INBOUND' \"$FLT_INBOUND\" ${DSIP_CONFIG_FILE}\n    setConfigAttrib 'FLT_LCR_MIN' \"$FLT_LCR_MIN\" ${DSIP_CONFIG_FILE}\n    setConfigAttrib 'FLT_FWD_MIN' \"$FLT_FWD_MIN\" ${DSIP_CONFIG_FILE}\n    setConfigAttrib 'DSIP_PROJECT_DIR' \"$DSIP_PROJECT_DIR\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'DSIP_DOCS_DIR' \"$DSIP_DOCS_DIR\" ${DSIP_CONFIG_FILE} -q\n\n    # the following variables are only updated when set\n    [[ -n \"$DSIP_ID\" ]] && setConfigAttrib 'DSIP_ID' \"$DSIP_ID\" ${DSIP_CONFIG_FILE} -qb\n    [[ -n \"$DSIP_CLUSTER_ID\" ]] && setConfigAttrib 'DSIP_CLUSTER_ID' \"$DSIP_CLUSTER_ID\" ${DSIP_CONFIG_FILE}\n    if [[ -n \"$DSIP_CLUSTER_SYNC\" ]]; then\n        if (( $DSIP_CLUSTER_SYNC == 1 )); then\n            setConfigAttrib 'DSIP_CLUSTER_SYNC' 'True' ${DSIP_CONFIG_FILE}\n        else\n            setConfigAttrib 'DSIP_CLUSTER_SYNC' 'False' ${DSIP_CONFIG_FILE}\n        fi\n    fi\n    [[ -n \"$DSIP_PROTO\" ]] && setConfigAttrib 'DSIP_PROTO' \"$DSIP_PROTO\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$DSIP_PORT\" ]] && setConfigAttrib 'DSIP_PORT' \"$DSIP_PORT\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$DSIP_API_PROTO\" ]] && setConfigAttrib 'DSIP_API_PROTO' \"$DSIP_API_PROTO\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$DSIP_API_PORT\" ]] && setConfigAttrib 'DSIP_API_PORT' \"$DSIP_API_PORT\" ${DSIP_CONFIG_FILE}\n    [[ -n \"$DSIP_PRIV_KEY\" ]] && setConfigAttrib 'DSIP_PRIV_KEY' \"$DSIP_PRIV_KEY\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$DSIP_PID_FILE\" ]] && setConfigAttrib 'DSIP_PID_FILE' \"$DSIP_PID_FILE\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$DSIP_UNIX_SOCK\" ]] && setConfigAttrib 'DSIP_UNIX_SOCK' \"$DSIP_UNIX_SOCK\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$DSIP_IPC_SOCK\" ]] && setConfigAttrib 'DSIP_IPC_SOCK' \"$DSIP_IPC_SOCK\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$DSIP_LOG_LEVEL\" ]] && setConfigAttrib 'DSIP_LOG_LEVEL' \"$DSIP_LOG_LEVEL\" ${DSIP_CONFIG_FILE}\n    [[ -n \"$DSIP_LOG_FACILITY\" ]] && setConfigAttrib 'DSIP_LOG_FACILITY' \"$DSIP_LOG_FACILITY\" ${DSIP_CONFIG_FILE}\n    [[ -n \"$DSIP_SSL_KEY\" ]] && setConfigAttrib 'DSIP_SSL_KEY' \"$DSIP_SSL_KEY\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$DSIP_SSL_CERT\" ]] && setConfigAttrib 'DSIP_SSL_CERT' \"$DSIP_SSL_CERT\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$DSIP_SSL_CA\" ]] && setConfigAttrib 'DSIP_SSL_CA' \"$DSIP_SSL_CA\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$DSIP_SSL_EMAIL\" ]] && setConfigAttrib 'DSIP_SSL_EMAIL' \"$DSIP_SSL_EMAIL\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$DSIP_CERTS_DIR\" ]] && setConfigAttrib 'DSIP_CERTS_DIR' \"$DSIP_CERTS_DIR\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$VERSION\" ]] && setConfigAttrib 'VERSION' \"$VERSION\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$ROLE\" ]] && setConfigAttrib 'ROLE' \"$ROLE\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$GUI_INACTIVE_TIMEOUT\" ]] && setConfigAttrib 'GUI_INACTIVE_TIMEOUT' \"$GUI_INACTIVE_TIMEOUT\" ${DSIP_CONFIG_FILE}\n    [[ -n \"$KAM_DB_DRIVER\" ]] && setConfigAttrib 'KAM_DB_DRIVER' \"$KAM_DB_DRIVER\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$KAM_DB_TYPE\" ]] && setConfigAttrib 'KAM_DB_TYPE' \"$KAM_DB_TYPE\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$DEFAULT_AUTH_DOMAIN\" ]] && setConfigAttrib 'DEFAULT_AUTH_DOMAIN' \"$DEFAULT_AUTH_DOMAIN\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$TELEBLOCK_GW_ENABLED\" ]] && setConfigAttrib 'TELEBLOCK_GW_ENABLED' \"$TELEBLOCK_GW_ENABLED\" ${DSIP_CONFIG_FILE}\n    [[ -n \"$TELEBLOCK_GW_IP\" ]] && setConfigAttrib 'TELEBLOCK_GW_IP' \"$TELEBLOCK_GW_IP\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$TELEBLOCK_GW_PORT\" ]] && setConfigAttrib 'TELEBLOCK_GW_PORT' \"$TELEBLOCK_GW_PORT\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$TELEBLOCK_MEDIA_IP\" ]] && setConfigAttrib 'TELEBLOCK_MEDIA_IP' \"$TELEBLOCK_MEDIA_IP\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$TELEBLOCK_MEDIA_PORT\" ]] && setConfigAttrib 'TELEBLOCK_MEDIA_PORT' \"$TELEBLOCK_MEDIA_PORT\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$FLOWROUTE_ACCESS_KEY\" ]] && setConfigAttrib 'FLOWROUTE_ACCESS_KEY' \"$FLOWROUTE_ACCESS_KEY\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$FLOWROUTE_SECRET_KEY\" ]] && setConfigAttrib 'FLOWROUTE_SECRET_KEY' \"$FLOWROUTE_SECRET_KEY\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$FLOWROUTE_API_ROOT_URL\" ]] && setConfigAttrib 'FLOWROUTE_API_ROOT_URL' \"$FLOWROUTE_API_ROOT_URL\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$HOMER_ID\" ]] && setConfigAttrib 'HOMER_ID' \"$HOMER_ID\" ${DSIP_CONFIG_FILE}\n    [[ -n \"$HOMER_HEP_HOST\" ]] && setConfigAttrib 'HOMER_HEP_HOST' \"$HOMER_HEP_HOST\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$HOMER_HEP_PORT\" ]] && setConfigAttrib 'HOMER_HEP_PORT' \"$HOMER_HEP_PORT\" ${DSIP_CONFIG_FILE}\n    [[ -n \"$UPLOAD_FOLDER\" ]] && setConfigAttrib 'UPLOAD_FOLDER' \"$UPLOAD_FOLDER\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$MAIL_SERVER\" ]] && setConfigAttrib 'MAIL_SERVER' \"$MAIL_SERVER\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$MAIL_PORT\" ]] && setConfigAttrib 'MAIL_PORT' \"$MAIL_PORT\" ${DSIP_CONFIG_FILE}\n    if [[ -n \"$MAIL_USE_TLS\" ]]; then\n        if (( $MAIL_USE_TLS == 0 )); then\n            setConfigAttrib 'MAIL_USE_TLS' \"False\" ${DSIP_CONFIG_FILE}\n        else\n            setConfigAttrib 'MAIL_USE_TLS' \"True\" ${DSIP_CONFIG_FILE}\n        fi\n\n    fi\n    if [[ -n \"$MAIL_ASCII_ATTACHMENTS\" ]]; then\n        if (( $MAIL_ASCII_ATTACHMENTS == 1 )); then\n            setConfigAttrib 'MAIL_ASCII_ATTACHMENTS' \"True\" ${DSIP_CONFIG_FILE}\n        else\n            setConfigAttrib 'MAIL_ASCII_ATTACHMENTS' \"False\" ${DSIP_CONFIG_FILE}\n        fi\n    fi\n    [[ -n \"$MAIL_USERNAME\" ]] && setConfigAttrib 'MAIL_DEFAULT_SENDER' \"dSIPRouter $EXTERNAL_FQDN <$MAIL_USERNAME>\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$MAIL_DEFAULT_SUBJECT\" ]] && setConfigAttrib 'MAIL_DEFAULT_SUBJECT' \"$MAIL_DEFAULT_SUBJECT\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$RTPENGINE_URI\" ]] && setConfigAttrib 'RTPENGINE_URI' \"$RTPENGINE_URI\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$CLOUD_PLATFORM\" ]] && setConfigAttrib 'CLOUD_PLATFORM' \"$CLOUD_PLATFORM\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$BACKUPS_DIR\" ]] && setConfigAttrib 'BACKUP_FOLDER' \"$BACKUPS_DIR\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$DID_PREFIX_ALLOWED_CHARS\" ]] && setConfigAttrib 'DID_PREFIX_ALLOWED_CHARS' \"$DID_PREFIX_ALLOWED_CHARS\" ${DSIP_CONFIG_FILE}\n    [[ -n \"$LOAD_SETTINGS_FROM\" ]] && setConfigAttrib 'LOAD_SETTINGS_FROM' \"$LOAD_SETTINGS_FROM\" ${DSIP_CONFIG_FILE} -q\n    [[ -n \"$DSIP_LICENSE_STORE\" ]] && setConfigAttrib 'DSIP_LICENSE_STORE' \"$DSIP_LICENSE_STORE\" ${DSIP_CONFIG_FILE}\n\n    # update settings based on values set by setDynamicScriptSettings()\n    setConfigAttrib 'NETWORK_MODE' \"$NETWORK_MODE\" ${DSIP_CONFIG_FILE}\n    if (( $IPV6_ENABLED == 1 )); then\n        setConfigAttrib 'IPV6_ENABLED' \"True\" ${DSIP_CONFIG_FILE}\n    else\n        setConfigAttrib 'IPV6_ENABLED' \"False\" ${DSIP_CONFIG_FILE}\n    fi\n    setConfigAttrib 'INTERNAL_IP_ADDR' \"$INTERNAL_IP_ADDR\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'INTERNAL_IP_NET' \"$INTERNAL_IP_NET\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'INTERNAL_IP6_ADDR' \"$INTERNAL_IP6_ADDR\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'INTERNAL_IP6_NET' \"$INTERNAL_IP6_NET\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'INTERNAL_FQDN' \"$INTERNAL_FQDN\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'EXTERNAL_IP_ADDR' \"$EXTERNAL_IP_ADDR\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'EXTERNAL_IP6_ADDR' \"$EXTERNAL_IP6_ADDR\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'EXTERNAL_FQDN' \"$EXTERNAL_FQDN\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'PUBLIC_IFACE' \"$PUBLIC_IFACE\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'PRIVATE_IFACE' \"$PRIVATE_IFACE\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'UAC_REG_ADDR' \"$UAC_REG_ADDR\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'GIT_REPO_URL' \"$GIT_REPO_URL\" ${DSIP_CONFIG_FILE} -q\n    setConfigAttrib 'GIT_RELEASE_URL' \"$GIT_RELEASE_URL\" ${DSIP_CONFIG_FILE} -q\n\n    # TODO: the following are updated in setCredentials() and the config file should only be updated here\n    #\t\ti.e. settings the variables elsewhere is fine but any changes to the config file or DB should be centralised here\n    # DSIP_GUI_USER\n    # DSIP_GUI_PASS\n    # DSIP_API_TOKEN\n    # DSIP_MAIL_USER\n    # DSIP_MAIL_PASS\n    # DSIP_IPC_PASS\n    # KAM_DB_USER\n    # KAM_DB_PASS\n    # KAM_DB_HOST\n    # KAM_DB_PORT\n    # KAM_DB_NAME\n    # ROOT_DB_HOST\n    # ROOT_DB_PORT\n    # ROOT_DB_USER\n    # ROOT_DB_PASS\n    # ROOT_DB_NAME\n    # DSIP_SESSION_KEY\n\n    # TODO: the following settings are only updatable via the GUI\n    # TRANSNEXUS_AUTHSERVICE_ENABLED\n    # TRANSNEXUS_AUTHSERVICE_HOST\n    # TRANSNEXUS_LICENSE_KEY\n    # TRANSNEXUS_VERIFYSERVICE_ENABLED\n    # TRANSNEXUS_VERIFYSERVICE_HOST\n    # STIR_SHAKEN_ENABLED\n    # STIR_SHAKEN_PREFIX_A\n    # STIR_SHAKEN_PREFIX_B\n    # STIR_SHAKEN_PREFIX_C\n    # STIR_SHAKEN_PREFIX_INVALID\n    # STIR_SHAKEN_BLOCK_INVALID\n    # STIR_SHAKEN_CERT_URL\n    # STIR_SHAKEN_KEY_PATH\n    # MSTEAMS_DNS_ENDPOINTS\n    # MSTEAMS_IP_ENDPOINTS\n\n    # TODO: workaround to update DB settings until next major release (v0.80)\n    if [[ \"$LOAD_SETTINGS_FROM\" == \"db\" ]]; then\n        setConfigAttrib 'LOAD_SETTINGS_FROM' 'file' ${DSIP_CONFIG_FILE} -q\n        ${PYTHON_CMD} -c \"import os,sys; os.chdir('${DSIP_PROJECT_DIR}/gui'); sys.path.insert(0, '${DSIP_SYSTEM_CONFIG_DIR}/gui'); from dsiprouter import syncSettings; syncSettings();\"\n        setConfigAttrib 'LOAD_SETTINGS_FROM' 'db' ${DSIP_CONFIG_FILE} -q\n    fi\n\n    return 0\n}\n\n# TODO: these variables should be ephemeral, set as environment variables when running the service, no need to store them\nfunction updateDsiprouterConfigRuntimeSettings() {\n    if (( ${DEBUG} == 1 )); then\n        setConfigAttrib 'DEBUG' 'True' ${DSIP_CONFIG_FILE}\n    else\n        setConfigAttrib 'DEBUG' 'False' ${DSIP_CONFIG_FILE}\n    fi\n}\n\nfunction updateDsiprouterStartup {\n    local KAM_UPDATE_OPTS=\"\"\n\n    # update dsiprouter configs on reboot\n    removeInitCmd \"/usr/bin/dsiprouter updatedsipconfig\"\n    addInitCmd \"/usr/bin/dsiprouter updatedsipconfig $KAM_UPDATE_OPTS\"\n\n    # make sure dsip-init service runs prior to dsiprouter service\n    removeDependsOnInit \"dsiprouter.service\"\n    addDependsOnInit \"dsiprouter.service\"\n}\n\n# supported methods for renewing certificates:\n# 1. using Let's Encrypt / certbot\n# 2. issuing a new self-signed cert\nfunction renewSSLCert() {\n    local DEFAULT_CERT_UPLOADED CERT_ISSUER RENEW_START_TS LAST_CHANGE_TS\n\n    # Do not renew if the admin uploaded a default cert\n    DEFAULT_CERT_UPLOADED=$(\n        withKamDB mysql -sN -e \"select count(*) from dsip_certificates where domain='default'\" 2>/dev/null\n    )\n    if (( ${DEFAULT_CERT_UPLOADED:-0} == 1 )); then\n        printwarn \"Current X509 certificate for dSIPRouter can not be automatically renewed\"\n        return 1\n    fi\n\n    CERT_ISSUER=$(\n        openssl x509 -in ${DSIP_SSL_CERT} -noout -nameopt compat -issuer 2>/dev/null |\n        perl -pe 's%^.*?/O=([^/]*).*?$%\\1%'\n    )\n    case \"$CERT_ISSUER\" in\n        \"Let's Encrypt\")\n            if certbot -n certificates | grep -q 'No certs found' &>/dev/null; then\n                printwarn \"No LetsEncrypt certificates managed by Certbot found\"\n                return 1\n            fi\n            RENEW_START_TS=$(date '+%s')\n            certbot -n renew\n            if (( $? == 0 )); then\n                # we only want to reload the live cert if it was actually changed\n                LAST_CHANGE_TS=$(stat -c '%Y' /etc/letsencrypt/live/${EXTERNAL_FQDN}/fullchain.pem)\n                if (( $? != 0 )); then\n                    printerr \"Could not find new certificate for ${EXTERNAL_FQDN}\"\n                    return 1\n                fi\n                if (( $LAST_CHANGE_TS < $RENEW_START_TS )); then\n                    return 0\n                fi\n\n                rm -f ${DSIP_CERTS_DIR}/dsiprouter*\n                cp -f /etc/letsencrypt/live/${EXTERNAL_FQDN}/fullchain.pem ${DSIP_SSL_CERT}\n                cp -f /etc/letsencrypt/live/${EXTERNAL_FQDN}/privkey.pem ${DSIP_SSL_KEY}\n            else\n                printerr \"Failed renewing certificate for ${EXTERNAL_FQDN} using LetsEncrypt\"\n                return 1\n            fi\n            ;;\n        dSIPRouter)\n            openssl req -new -newkey rsa:4096 -x509 -sha256 -days 365 -nodes \\\n                -out ${DSIP_SSL_CERT} \\\n                -keyout ${DSIP_SSL_KEY} \\\n                -subj \"/C=US/ST=MI/L=Detroit/O=dSIPRouter/CN=${EXTERNAL_FQDN}\"\n            if (( $? != 0 )); then\n                printerr \"Failed renewing self-signed certificate for ${EXTERNAL_FQDN}\"\n                return 1\n            fi\n            ;;\n        *)\n            printwarn \"Current X509 certificate for dSIPRouter can not be automatically renewed\"\n            return 1\n            ;;\n    esac\n\n\n    updatePermissions -certs &&\n    kamcmd tls.reload &&\n    return 0 ||\n    return 1\n}\n\nfunction configureSSL() {\n    # Check if certificates already exists.  If so, use them and exit\n    if [[ -f \"${DSIP_SSL_CERT}\" && -f \"${DSIP_SSL_KEY}\" ]]; then\n        printwarn \"Using certificates found in ${DSIP_CERTS_DIR}\"\n        updatePermissions -certs\n        return\n    fi\n\n    # Stop nginx if started so that LetsEncrypt can leverage port 80\n    if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]]; then\n\t    if docker ps | grep -q dsiprouter-nginx; then\n\t    \tdocker stop dsiprouter-nginx 2>/dev/null\n\t    fi\t\n    else\n        firewall-cmd --zone=public --add-port=80/tcp\n    fi\n\n    # Override the hostname if -o or --override=<hostname> is provided\n    if [[ -n \"${DNS_NAME_OVERRIDE}\" ]]; then\n\tEXTERNAL_FQDN=${DNS_NAME_OVERRIDE}\n    fi\n    # Try to create cert using LetsEncrypt's first\n    printdbg \"Generating Certs for ${EXTERNAL_FQDN} using LetsEncrypt\"\n    certbot certonly --standalone --non-interactive --agree-tos -d ${EXTERNAL_FQDN} -m ${DSIP_SSL_EMAIL} \\\n        --server https://acme-v02.api.letsencrypt.org/directory --force-renewal --preferred-chain \"ISRG Root X1\"\n    if (( $? == 0 )); then\n        rm -f ${DSIP_CERTS_DIR}/dsiprouter*\n        cp -f /etc/letsencrypt/live/${EXTERNAL_FQDN}/fullchain.pem ${DSIP_SSL_CERT}\n        cp -f /etc/letsencrypt/live/${EXTERNAL_FQDN}/privkey.pem ${DSIP_SSL_KEY}\n    else\n        printwarn \"Failed Generating Certs for ${EXTERNAL_FQDN} using LetsEncrypt\"\n\n        # Worst case, generate a Self-Signed Certificate\n        printdbg \"Generating dSIPRouter Self-Signed Certificates\"\n        openssl req -new -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out ${DSIP_SSL_CERT} -keyout ${DSIP_SSL_KEY} -subj \"/C=US/ST=MI/L=Detroit/O=dSIPRouter/CN=${EXTERNAL_FQDN}\"\n    fi\n\n    # Add Nightly Cronjob to renew certs if not already there\n    if ! crontab -u root -l 2>/dev/null | grep -q '/usr/bin/dsiprouter renewsslcert' 2>/dev/null; then\n        cronAppend -u root '0 0 * * * /usr/bin/dsiprouter renewsslcert'\n    fi\n    updatePermissions -certs\n\n    # Start nginx if dSIP was installed\n    if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]]; then\n\t    if docker ps | grep -q dsiprouter-nginx; then\n\t    \tdocker stop dsiprouter-nginx 2>/dev/null\n\t    fi\t\n    else\n        firewall-cmd --zone=public --remove-port=80/tcp\n    fi\n}\n\n# updates and settings in kam config that may change\n# should be run after changing settings.py or change in network configurations\n# TODO: support configuring separate asterisk realtime db conns / clusters (would need separate setting in settings.py)\nfunction updateKamailioConfig() {\n    local DSIP_ID=${DSIP_ID:-$(getConfigAttrib 'DSIP_ID' ${DSIP_CONFIG_FILE})}\n    local DSIP_CLUSTER_ID=${DSIP_CLUSTER_ID:-$(getConfigAttrib 'DSIP_CLUSTER_ID' ${DSIP_CONFIG_FILE})}\n    local DSIP_CLUSTER_SYNC=${DSIP_CLUSTER_SYNC:-$([[ \"$(getConfigAttrib 'DSIP_CLUSTER_SYNC' ${DSIP_CONFIG_FILE})\" == \"True\" ]] && echo '1' || echo '0')}\n    local DSIP_VERSION=${DSIP_VERSION:-$(getConfigAttrib 'VERSION' ${DSIP_CONFIG_FILE})}\n    local HOMER_ID=${HOMER_ID:-$(getConfigAttrib 'HOMER_ID' ${DSIP_CONFIG_FILE})}\n    local DSIP_API_BASEURL=\"$(getConfigAttrib 'DSIP_API_PROTO' ${DSIP_CONFIG_FILE})://127.0.0.1:$(getConfigAttrib 'DSIP_API_PORT' ${DSIP_CONFIG_FILE})\"\n    local DSIP_API_TOKEN=${DSIP_API_TOKEN:-$(decryptConfigAttrib 'DSIP_API_TOKEN' ${DSIP_CONFIG_FILE} 2>/dev/null)}\n    local DEBUG=${DEBUG:-$([[ \"$(getConfigAttrib 'DEBUG' ${DSIP_CONFIG_FILE})\" == \"True\" ]] && echo '1' || echo '0')}\n    local ROLE=${ROLE:-$(getConfigAttrib 'ROLE' ${DSIP_CONFIG_FILE})}\n    local TELEBLOCK_GW_ENABLED=${TELEBLOCK_GW_ENABLED:-$(getConfigAttrib 'TELEBLOCK_GW_ENABLED' ${DSIP_CONFIG_FILE})}\n    local TELEBLOCK_GW_IP=${TELEBLOCK_GW_IP:-$(getConfigAttrib 'TELEBLOCK_GW_IP' ${DSIP_CONFIG_FILE})}\n    local TELEBLOCK_GW_PORT=${TELEBLOCK_GW_PORT:-$(getConfigAttrib 'TELEBLOCK_GW_PORT' ${DSIP_CONFIG_FILE})}\n    local TELEBLOCK_MEDIA_IP=${TELEBLOCK_MEDIA_IP:-$(getConfigAttrib 'TELEBLOCK_MEDIA_IP' ${DSIP_CONFIG_FILE})}\n    local TELEBLOCK_MEDIA_PORT=${TELEBLOCK_MEDIA_PORT:-$(getConfigAttrib 'TELEBLOCK_MEDIA_PORT' ${DSIP_CONFIG_FILE})}\n    local KAM_WSS_PORT=${KAM_WSS_PORT:-$(getConfigAttrib 'KAM_WSS_PORT' ${DSIP_CONFIG_FILE})}\n    local KAM_SIP_PORT=${KAM_SIP_PORT:-$(getConfigAttrib 'KAM_SIP_PORT' ${DSIP_CONFIG_FILE})}\n    local KAM_SIPS_PORT=${KAM_SIPS_PORT:-$(getConfigAttrib 'KAM_SIPS_PORT' ${DSIP_CONFIG_FILE})}\n    local KAM_DMQ_PORT=${KAM_DMQ_PORT:-$(getConfigAttrib 'KAM_DMQ_PORT' ${DSIP_CONFIG_FILE})}\n    local HOMER_HEP_HOST=${HOMER_HEP_HOST:-$(getConfigAttrib 'HOMER_HEP_HOST' ${DSIP_CONFIG_FILE})}\n    local HOMER_HEP_PORT=${HOMER_HEP_PORT:-$(getConfigAttrib 'HOMER_HEP_PORT' ${DSIP_CONFIG_FILE})}\n    local NETWORK_MODE=${NETWORK_MODE:-$(getConfigAttrib 'NETWORK_MODE' ${DSIP_CONFIG_FILE})}\n    local RTPENGINE_URI=${RTPENGINE_URI:-$(getConfigAttrib 'RTPENGINE_URI' ${DSIP_CONFIG_FILE})}\n    local RTPENGINE_HOST=$(cut -s -d ':' -f 2 <<<\"$RTPENGINE_URI\")\n\n    # update kamailio config file\n    if (( $DEBUG == 1 )); then\n        enableKamailioConfigAttrib 'WITH_DEBUG' ${DSIP_KAMAILIO_CONFIG_FILE}\n    else\n        disableKamailioConfigAttrib 'WITH_DEBUG' ${DSIP_KAMAILIO_CONFIG_FILE}\n    fi\n    if (( $SIGNAL_SERVERNAT == 1 )); then\n        enableKamailioConfigAttrib 'WITH_SIGNAL_SERVERNAT' ${DSIP_KAMAILIO_CONFIG_FILE}\n    else\n        disableKamailioConfigAttrib 'WITH_SIGNAL_SERVERNAT' ${DSIP_KAMAILIO_CONFIG_FILE}\n    fi\n    if (( $SIGNAL_SERVERNAT6 == 1 )); then\n        enableKamailioConfigAttrib 'WITH_SIGNAL_SERVERNAT6' ${DSIP_KAMAILIO_CONFIG_FILE}\n    else\n        disableKamailioConfigAttrib 'WITH_SIGNAL_SERVERNAT6' ${DSIP_KAMAILIO_CONFIG_FILE}\n    fi\n    if (( $IPV6_ENABLED == 1 )); then\n        enableKamailioConfigAttrib 'WITH_IPV6' ${DSIP_KAMAILIO_CONFIG_FILE}\n    else\n        disableKamailioConfigAttrib 'WITH_IPV6' ${DSIP_KAMAILIO_CONFIG_FILE}\n    fi\n    if (( $NETWORK_MODE == 2 )); then\n        enableKamailioConfigAttrib 'WITH_DMZ' ${DSIP_KAMAILIO_CONFIG_FILE}\n    else\n        disableKamailioConfigAttrib 'WITH_DMZ' ${DSIP_KAMAILIO_CONFIG_FILE}\n    fi\n    if (( $DSIP_CLUSTER_SYNC == 1 )); then\n        enableKamailioConfigAttrib 'WITH_DMQ' ${DSIP_KAMAILIO_CONFIG_FILE}\n        setKamailioConfigSubst 'DMQ_REPLICATE_ENABLED' '1' ${DSIP_KAMAILIO_CONFIG_FILE}\n    else\n        disableKamailioConfigAttrib 'WITH_DMQ' ${DSIP_KAMAILIO_CONFIG_FILE}\n        setKamailioConfigSubst 'DMQ_REPLICATE_ENABLED' '0' ${DSIP_KAMAILIO_CONFIG_FILE}\n    fi\n    if [[ -n \"$HOMER_HEP_HOST\" && -n \"$HOMER_HEP_PORT\" ]]; then\n        enableKamailioConfigAttrib 'WITH_HOMER' ${DSIP_KAMAILIO_CONFIG_FILE}\n    else\n        disableKamailioConfigAttrib 'WITH_HOMER' ${DSIP_KAMAILIO_CONFIG_FILE}\n    fi\n    if [[ -n \"$DSIP_ID\" && \"$DSIP_ID\" != \"None\" ]]; then\n        setKamailioConfigSubst 'DSIP_ID' \"$DSIP_ID\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    fi\n    if [[ -n \"$HOMER_ID\" && \"$HOMER_ID\" != \"None\" ]]; then\n        setKamailioConfigSubst 'HOMER_ID' \"$HOMER_ID\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    fi\n    if lsmod | awk '$1 == \"sctp\" {rc=1; exit;}; END {exit !rc;}'; then\n        enableKamailioConfigAttrib 'WITH_SCTP' ${DSIP_KAMAILIO_CONFIG_FILE}\n    else\n        disableKamailioConfigAttrib 'WITH_SCTP' ${DSIP_KAMAILIO_CONFIG_FILE}\n    fi\n    if isHostLocal \"$RTPENGINE_HOST\"; then\n        enableKamailioConfigAttrib 'WITH_MEDIA_SERVERNAT' ${DSIP_KAMAILIO_CONFIG_FILE}\n    else\n        disableKamailioConfigAttrib 'WITH_MEDIA_SERVERNAT' ${DSIP_KAMAILIO_CONFIG_FILE}\n    fi\n\n    setKamailioConfigSubst 'DSIP_CLUSTER_ID' \"${DSIP_CLUSTER_ID}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'DSIP_VERSION' \"${DSIP_VERSION}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'INTERNAL_IP_ADDR' \"${INTERNAL_IP_ADDR}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'INTERNAL_IP6_ADDR' \"${INTERNAL_IP6_ADDR}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'INTERNAL_IP_NET' \"${INTERNAL_IP_NET}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'INTERNAL_IP6_NET' \"${INTERNAL_IP_NET6}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'EXTERNAL_IP_ADDR' \"${EXTERNAL_IP_ADDR}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'EXTERNAL_IP6_ADDR' \"${EXTERNAL_IP6_ADDR}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'INTERNAL_FQDN' \"${INTERNAL_FQDN}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'EXTERNAL_FQDN' \"${EXTERNAL_FQDN}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'UAC_REG_ADDR' \"${UAC_REG_ADDR}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'WSS_PORT' \"${KAM_WSS_PORT}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'SIP_PORT' \"${KAM_SIP_PORT}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'SIPS_PORT' \"${KAM_SIPS_PORT}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'DMQ_PORT' \"${KAM_DMQ_PORT}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'HOMER_HOST' \"${HOMER_HEP_HOST}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'HEP_PORT' \"${HOMER_HEP_PORT}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigSubst 'RTPENGINE_URI' \"$RTPENGINE_URI\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigGlobal 'server.api_server' \"${DSIP_API_BASEURL}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigGlobal 'server.api_token' \"${DSIP_API_TOKEN}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigGlobal 'server.role' \"${ROLE}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigGlobal 'teleblock.gw_enabled' \"${TELEBLOCK_GW_ENABLED}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigGlobal 'teleblock.gw_ip' \"${TELEBLOCK_GW_IP}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigGlobal 'teleblock.gw_port' \"${TELEBLOCK_GW_PORT}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigGlobal 'teleblock.media_ip' \"${TELEBLOCK_MEDIA_IP}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    setKamailioConfigGlobal 'teleblock.media_port' \"${TELEBLOCK_MEDIA_PORT}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n\n    # hot reloading global settings\n    if systemctl is-active --quiet kamailio 2>/dev/null; then\n        sendKamCmd cfg.sets server role \"${ROLE}\" &>/dev/null\n        sendKamCmd cfg.sets server api_server \"${DSIP_API_BASEURL}\" &>/dev/null\n        sendKamCmd cfg.sets server api_token \"${DSIP_API_TOKEN}\" &>/dev/null\n        sendKamCmd cfg.seti teleblock gw_enabled \"${TELEBLOCK_GW_ENABLED}\" &>/dev/null\n        sendKamCmd cfg.sets teleblock gw_ip \"${TELEBLOCK_GW_IP}\" &>/dev/null\n        sendKamCmd cfg.seti teleblock gw_port \"${TELEBLOCK_GW_PORT}\" &>/dev/null\n        sendKamCmd cfg.sets teleblock media_ip \"${TELEBLOCK_MEDIA_IP}\" &>/dev/null\n        sendKamCmd cfg.seti teleblock media_port \"${TELEBLOCK_MEDIA_PORT}\" &>/dev/null\n    fi\n\n    # check for cluster db connection and set kam db config settings appropriately\n    # note: the '@' symbol must be escaped in perl regex\n    if printf '%s' \"$KAM_DB_HOST\" | grep -q -oP '(\\[.*\\]|.*,.*)'; then\n        # db connection is clustered\n        enableKamailioConfigAttrib 'WITH_DBCLUSTER' ${DSIP_KAMAILIO_CONFIG_FILE}\n\n        # TODO: support different type/user/pass/port/name per connection\n        # TODO: support multiple clusters\n        local KAM_DB_CLUSTER_CONNS=\"\"\n        local KAM_DB_CLUSTER_MODES=\"\"\n        local KAM_DB_CLUSTER_NODES=$(printf '%s' \"$KAM_DB_HOST\" | tr -d '[]'\"'\"'\"' | tr ',' ' ')\n\n        local i=1\n        for NODE in $KAM_DB_CLUSTER_NODES; do\n            KAM_DB_CLUSTER_CONNS+=\"modparam('db_cluster', 'connection', 'c${i}=>${KAM_DB_TYPE}://${KAM_DB_USER}:${KAM_DB_PASS}\\\\@${NODE}:${KAM_DB_PORT}/${KAM_DB_NAME}')\\n\"\n            KAM_DB_CLUSTER_MODES+=\"c${i}=9r9r;\"\n            i=$((i+1))\n        done\n        KAM_DB_CLUSTER_MODES=\"modparam('db_cluster', 'cluster', 'dbcluster=>${KAM_DB_CLUSTER_MODES}')\"\n\n        perl -e \"\\$dbcluster='${KAM_DB_CLUSTER_CONNS}${KAM_DB_CLUSTER_MODES}';\" \\\n            -0777 -i -pe 's~(modparam\\(\"db_cluster\", \"connection\".*\\s)+(modparam\\(\"db_cluster\", \"cluster\".*)~${dbcluster}~gm' ${DSIP_KAMAILIO_CONFIG_FILE}\n    else\n        local DBURL=\"${KAM_DB_TYPE}://${KAM_DB_USER}:${KAM_DB_PASS}@${KAM_DB_HOST}:${KAM_DB_PORT}/${KAM_DB_NAME}\"\n        setKamailioConfigDburl \"DBURL\" \"${DBURL}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n        setKamailioConfigDburl \"SQLCONN_KAM\" \"kam=>${DBURL}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n        setKamailioConfigDburl \"SQLCONN_AST\" \"asterisk=>${DBURL}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n    fi\n\n    # update kamailio TLS config file\n#    if (( ${IPV6_ENABLED} == 1 )); then\n#        perl -e \"\\$external_ip='${EXTERNAL_IP_ADDR}'; \\$wss_port='${KAM_WSS_PORT}'; \"'$ipv6_config=\n#            \"[server:['\"${EXTERNAL_IP6_ADDR}\"']:'\"${KAM_WSS_PORT}\"']\\n\" .\n#            \"method = TLSv1.2+\\n\" .\n#            \"verify_certificate = no\\n\" .\n#            \"require_certificate = no\\n\" .\n#            \"private_key = /etc/dsiprouter/certs/dsiprouter-key.pem\\n\" .\n#            \"certificate = /etc/dsiprouter/certs/dsiprouter-cert.pem\\n\" .\n#            \"ca_list = /etc/dsiprouter/certs/ca-list.pem\\n\" .\n#            \"#crl = /etc/dsiprouter/certs/crl.pem\\n\";' \\\n#            -0777 -i -pe 's%(#========== webrtc_ipv4_start ==========#.*?\\[server:).*?:.*?(\\].*#========== webrtc_ipv4_stop ==========#)%\\1${external_ip}:${wss_port}\\2%s;\n#            s%(#========== webrtc_ipv6_start ==========#[\\s]+).*(#========== webrtc_ipv6_stop ==========#)%\\1${ipv6_config}\\2%s;' \\\n#            ${DSIP_KAMAILIO_TLS_CONFIG_FILE}\n#    else\n#        perl -e \"\\$external_ip='${EXTERNAL_IP_ADDR}'; \\$wss_port='${KAM_WSS_PORT}';\" -0777 -i \\\n#            -pe 's%(#========== webrtc_ipv4_start ==========#.*?\\[server:).*?:.*?(\\].*#========== webrtc_ipv4_stop ==========#)%\\1${external_ip}:${wss_port}\\2%s;\n#            s%(#========== webrtc_ipv6_start ==========#[\\s]+).*(#========== webrtc_ipv6_stop ==========#)%\\1\\2%s;' \\\n#            ${DSIP_KAMAILIO_TLS_CONFIG_FILE}\n#    fi\n\n    return 0\n}\n\n# update kamailio service startup commands accounting for any changes\nfunction updateKamailioStartup {\n    local KAM_UPDATE_OPTS=\"\"\n\n    # update kamailio configs on reboot\n    removeInitCmd \"/usr/bin/dsiprouter updatekamconfig\"\n    addInitCmd \"/usr/bin/dsiprouter updatekamconfig $KAM_UPDATE_OPTS\"\n\n    # make sure dsip-init service runs prior to kamailio service\n    removeDependsOnInit \"kamailio.service\"\n    addDependsOnInit \"kamailio.service\"\n}\n\nfunction generateRtpengineConfig() {\n    mkdir -p ${BACKUPS_DIR}/rtpengine/\n    cp -af ${DSIP_SYSTEM_CONFIG_DIR}/rtpengine/. ${BACKUPS_DIR}/rtpengine/ 2>/dev/null\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/configs/rtpengine.conf ${DSIP_SYSTEM_CONFIG_DIR}/rtpengine/\n    ln -sft ${SYSTEM_RTPENGINE_CONFIG_DIR}/ ${DSIP_SYSTEM_CONFIG_DIR}/rtpengine/*\n}\n\n# updates and settings in rtpengine config that may change\n# should be run after reboot or change in network configurations\nfunction updateRtpengineConfig() {\n    local INTERFACE=\"\"\n    local RTP_PORT_MIN=${RTP_PORT_MIN:-$(getRtpengineConfigAttrib 'RTP_PORT_MIN' ${SYSTEM_RTPENGINE_CONFIG_FILE})}\n    local RTP_PORT_MAX=${RTP_PORT_MAX:-$(getRtpengineConfigAttrib 'RTP_PORT_MAX' ${SYSTEM_RTPENGINE_CONFIG_FILE})}\n    local HOMER_ID=${HOMER_ID:-$(getConfigAttrib 'HOMER_ID' ${DSIP_CONFIG_FILE})}\n    local HOMER_HEP_HOST=${HOMER_HEP_HOST:-$(getConfigAttrib 'HOMER_HEP_HOST' ${DSIP_CONFIG_FILE})}\n    local HOMER_HEP_PORT=${HOMER_HEP_PORT:-$(getConfigAttrib 'HOMER_HEP_PORT' ${DSIP_CONFIG_FILE})}\n\n    if (( ${NETWORK_MODE} == 2 )); then\n        # TODO: ipv6 support broken here\n        INTERFACE=\"public/${EXTERNAL_IP_ADDR}; private/${INTERNAL_IP_ADDR}\"\n    else\n        if (( ${SIGNAL_SERVERNAT} == 1 )); then\n            INTERFACE=\"ipv4/${INTERNAL_IP_ADDR}!${EXTERNAL_IP_ADDR}\"\n        else\n            INTERFACE=\"ipv4/${INTERNAL_IP_ADDR}\"\n        fi\n        if (( ${IPV6_ENABLED} == 1 )); then\n            if (( ${SIGNAL_SERVERNAT6} == 1 )); then\n                INTERFACE=\"${INTERFACE}; ipv6/${INTERNAL_IP6_ADDR}!${EXTERNAL_IP6_ADDR}\"\n            else\n                INTERFACE=\"${INTERFACE}; ipv6/${INTERNAL_IP6_ADDR}\"\n            fi\n        fi\n    fi\n\n    setRtpengineConfigAttrib 'interface' \"$INTERFACE\" ${SYSTEM_RTPENGINE_CONFIG_FILE}\n    setRtpengineConfigAttrib 'port-min' \"$RTP_PORT_MIN\" ${SYSTEM_RTPENGINE_CONFIG_FILE}\n    setRtpengineConfigAttrib 'port-max' \"$RTP_PORT_MAX\" ${SYSTEM_RTPENGINE_CONFIG_FILE}\n    setRtpengineConfigAttrib 'homer' \"${HOMER_HEP_HOST}:${HOMER_HEP_PORT}\" ${SYSTEM_RTPENGINE_CONFIG_FILE}\n\n    if [[ -n \"$HOMER_ID\" && \"$HOMER_ID\" != \"None\" ]]; then\n        setRtpengineConfigAttrib 'homer-id' \"$HOMER_ID\" ${SYSTEM_RTPENGINE_CONFIG_FILE}\n    fi\n\n    if [[ -n \"$HOMER_HEP_HOST\" && -n \"$HOMER_HEP_PORT\" ]]; then\n        enableRtpengineConfigAttrib 'homer' ${SYSTEM_RTPENGINE_CONFIG_FILE}\n        enableRtpengineConfigAttrib 'homer-protocol' ${SYSTEM_RTPENGINE_CONFIG_FILE}\n        enableRtpengineConfigAttrib 'homer-id' ${SYSTEM_RTPENGINE_CONFIG_FILE}\n    else\n        disableRtpengineConfigAttrib 'homer' ${SYSTEM_RTPENGINE_CONFIG_FILE}\n        disableRtpengineConfigAttrib 'homer-protocol' ${SYSTEM_RTPENGINE_CONFIG_FILE}\n        disableRtpengineConfigAttrib 'homer-id' ${SYSTEM_RTPENGINE_CONFIG_FILE}\n    fi\n\n    return 0\n}\n\n# update rtpengine service startup commands accounting for any changes\nfunction updateRtpengineStartup() {\n    reconfigureRtpengineSystemdService\n\n    # always clear out the dsip-init entries for rtpengine\n    removeInitCmd \"/usr/bin/dsiprouter updatertpconfig\"\n    removeDependsOnInit \"rtpengine.service\"\n\n    # conditionally add the dsip-init entries (MEDIA_SERVERNAT==1 only when rtpengine service is local)\n    if (( ${MEDIA_SERVERNAT} == 1 )); then\n        # update rtpengine configs on reboot\n        addInitCmd \"/usr/bin/dsiprouter updatertpconfig\"\n        # make sure dsip-init service runs prior to rtpengine service\n        addDependsOnInit \"rtpengine.service\"\n    fi\n}\n\n# updates DNSmasq configs from DB\n# TODO: dynamically update pacemaker IPs in /etc/hosts using getPacemakerInternalIP()\n#       they would need loaded in the DB prior on boot (can not wait for dsiprouter.py)\n#       because the pacemaker and corosync services start prior to dsiprouter.service\nfunction updateDnsConfig() {\n    local KAM_DB_HOST=${KAM_DB_HOST:-$(getConfigAttrib 'KAM_DB_HOST' ${DSIP_CONFIG_FILE})}\n    local KAM_DB_PORT=${KAM_DB_PORT:-$(getConfigAttrib 'KAM_DB_PORT' ${DSIP_CONFIG_FILE})}\n    local KAM_DB_NAME=${KAM_DB_NAME:-$(getConfigAttrib 'KAM_DB_NAME' ${DSIP_CONFIG_FILE})}\n    local KAM_DB_USER=${KAM_DB_USER:-$(getConfigAttrib 'KAM_DB_USER' ${DSIP_CONFIG_FILE})}\n    local KAM_DB_PASS=${KAM_DB_PASS:-$(decryptConfigAttrib 'KAM_DB_PASS' ${DSIP_CONFIG_FILE})}\n    local DSIP_CLUSTER_ID=${DSIP_CLUSTER_ID:-$(getConfigAttrib 'DSIP_CLUSTER_ID' ${DSIP_CONFIG_FILE})}\n    local DNS_CONFIG=\"\"\n\n    # grab hosts from db\n    # NOTE: we don't add IPV6 addresses here as it is not needed and would only add more traffic to DMQ replication\n    local INTERNAL_CLUSTER_HOSTS=(\n        $(withKamDB mysql -sN \\\n            -e \"SELECT INTERNAL_IP_ADDR FROM dsip_settings WHERE DSIP_CLUSTER_ID = ${DSIP_CLUSTER_ID};\" 2>/dev/null)\n    )\n    local EXTERNAL_CLUSTER_HOSTS=(\n        $(withKamDB mysql -sN \\\n            -e \"SELECT EXTERNAL_IP_ADDR FROM dsip_settings WHERE DSIP_CLUSTER_ID = ${DSIP_CLUSTER_ID};\" 2>/dev/null)\n    )\n    local NUM_HOSTS=${#INTERNAL_CLUSTER_HOSTS[@]}\n\n    # only search through cluster hosts if we got results\n    if (( ${NUM_HOSTS} > 0 )); then\n        # find valid connections on dmq port:\n        # try internal ip first and then external ip\n        for i in $(seq 0 $((${NUM_HOSTS}-1))); do\n            if checkConn ${INTERNAL_CLUSTER_HOSTS[$i]} ${KAM_DMQ_PORT}; then\n                DNS_CONFIG+=\"${INTERNAL_CLUSTER_HOSTS[$i]} local.cluster\\n\"\n            elif checkConn ${EXTERNAL_CLUSTER_HOSTS[$i]} ${KAM_DMQ_PORT}; then\n                DNS_CONFIG+=\"${EXTERNAL_CLUSTER_HOSTS[$i]} local.cluster\\n\"\n            fi\n        done\n    # otherwise make sure local node is resolvable when querying cluster\n    else\n        DNS_CONFIG+=\"${INTERNAL_IP_ADDR} local.cluster\\n\"\n    fi\n\n    # update hosts file\n    perl -e \"\\$cluster_hosts=\\\"${DNS_CONFIG}\\\";\" \\\n        -0777 -i -pe 's|(#+DSIP_CONFIG_START).*?(#+DSIP_CONFIG_END)|\\1\\n${cluster_hosts}\\2|gms' /etc/hosts\n\n    # tell dnsmasq to reload configs\n    if [ -f /run/dnsmasq/dnsmasq.pid ]; then\n        kill -SIGHUP $(cat /run/dnsmasq/dnsmasq.pid) 2>/dev/null\n    elif [ -f /run/dnsmasq.pid ]; then\n        kill -SIGHUP $(cat /run/dnsmasq.pid) 2>/dev/null\n    else\n        kill -SIGHUP $(pidof dnsmasq) 2>/dev/null\n    fi\n\n    return 0\n}\n\nfunction updateCACertsDir() {\n    awk -v dsip_certs_dir=\"${DSIP_CERTS_DIR}\" \\\n        'BEGIN {c=0;}\n        /BEGIN CERT/{c++} {\n            print > dsip_certs_dir \"/ca/cert.\" c \".pem\"\n        }' <${DSIP_SSL_CA} &&\n    openssl rehash ${DSIP_CERTS_DIR}/ca/ &&\n    updatePermissions -certs &&\n    return 0 ||\n    return 1\n}\nexport -f updateCACertsDir\n\nfunction generateKamailioConfig() {\n    local KAM_MAJ_MIN_INT=$(perl -pe 's%^([0-9])\\.([0-9]).*$%\\1\\2%' <<<\"$KAM_VERSION\")\n\n    # Backup kamcfg, generate fresh config from templates, and link it in where kamailio wants it\n    mkdir -p ${BACKUPS_DIR}/kamailio\n    cp -af ${SYSTEM_KAMAILIO_CONFIG_DIR}/. ${BACKUPS_DIR}/kamailio/\n    cp -f ${PROJECT_KAMAILIO_CONFIG_DIR}/*.cfg ${DSIP_SYSTEM_CONFIG_DIR}/kamailio/\n    ln -sft ${SYSTEM_KAMAILIO_CONFIG_DIR}/ ${DSIP_SYSTEM_CONFIG_DIR}/kamailio/*\n\n    # version specific settings\n    if (( ${KAM_MAJ_MIN_INT} >= 52 )); then\n        sed -i -r -e 's~#+(modparam\\([\"'\"'\"']htable[\"'\"'\"'], ?[\"'\"'\"']dmq_init_sync[\"'\"'\"'], ?[0-9]\\))~\\1~g' ${DSIP_KAMAILIO_CONFIG_FILE}\n    fi\n    if (( ${KAM_MAJ_MIN_INT} <= 57 )); then\n        sed -i -r -e 's~#*(modparam\\([\"'\"'\"']rtpengine[\"'\"'\"'], ?[\"'\"'\"']ping_mode[\"'\"'\"'], ?[0-9]\\))~#\\1~g' ${DSIP_KAMAILIO_CONFIG_FILE}\n    fi\n\n    # Fix the mpath and export $mpath\n    fixMPATH\n\n    # non-module features to enable\n    # TODO: add check for WITH_TRANSNEXUS\n    if (( ${WITH_LCR} == 1 )); then\n        enableKamailioConfigAttrib 'WITH_LCR' ${DSIP_KAMAILIO_CONFIG_FILE}\n    else\n        disableKamailioConfigAttrib 'WITH_LCR' ${DSIP_KAMAILIO_CONFIG_FILE}\n    fi\n    if [[ -f ${mpath}/stirshaken.so ]]; then\n        enableKamailioConfigAttrib 'WITH_STIRSHAKEN' ${DSIP_KAMAILIO_CONFIG_FILE}\n    else\n        disableKamailioConfigAttrib 'WITH_STIRSHAKEN' ${DSIP_KAMAILIO_CONFIG_FILE}\n    fi\n\n    updatePermissions -kamailio\n}\n\nfunction configureKamailioDB() {\n    # make sure kamailio user and privileges exist\n    if ! checkDBUserExists \"${KAM_DB_USER}@localhost\"; then\n        withRootDBConn mysql \\\n            -e \"CREATE USER '$KAM_DB_USER'@'localhost' IDENTIFIED BY '$KAM_DB_PASS';\" \\\n            -e \"GRANT ALL PRIVILEGES ON $KAM_DB_NAME.* TO '$KAM_DB_USER'@'localhost';\" \\\n            -e \"FLUSH PRIVILEGES;\"\n    fi\n    if ! checkDBUserExists \"${KAM_DB_USER}@%\"; then\n        withRootDBConn mysql \\\n            -e \"CREATE USER '$KAM_DB_USER'@'%' IDENTIFIED BY '$KAM_DB_PASS';\" \\\n            -e \"GRANT ALL PRIVILEGES ON $KAM_DB_NAME.* TO '$KAM_DB_USER'@'%';\" \\\n            -e \"FLUSH PRIVILEGES;\"\n    fi\n\n    # Install schema for drouting module\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n        -e \"delete from version where table_name in ('dr_gateways','dr_groups','dr_gw_lists','dr_custom_rules','dr_rules')\"\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n        -e \"drop table if exists dr_gateways,dr_groups,dr_gw_lists,dr_custom_rules,dr_rules\"\n    if [ -e  /usr/share/kamailio/mysql/drouting-create.sql ]; then\n        withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n            < /usr/share/kamailio/mysql/drouting-create.sql\n    else\n        sqlscript=$(find / -name '*drouting-create.sql' | grep 'mysql' | head -1)\n        withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n            < $sqlscript\n    fi\n\n    # Update schema for dr_gateways table\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n        < ${PROJECT_DSIP_DEFAULTS_DIR}/dr_gateways.sql\n\n    # Update schema for dr_gw_lists table\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n        < ${PROJECT_DSIP_DEFAULTS_DIR}/dr_gw_lists.sql\n\n    # Update schema for dr_rules table\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n        < ${PROJECT_DSIP_DEFAULTS_DIR}/dr_rules.sql\n\n    # Update schema for dispatcher table\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n        < ${PROJECT_DSIP_DEFAULTS_DIR}/dispatcher.sql\n\n    # Update schema for address table\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n        < ${PROJECT_DSIP_DEFAULTS_DIR}/address.sql\n\n    # Update schema for subscribers table\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n        < ${PROJECT_DSIP_DEFAULTS_DIR}/subscribers.sql\n\n    # Update schema for uacreg table\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n        < ${PROJECT_DSIP_DEFAULTS_DIR}/uacreg.sql\n\n    # Install schema for custom LCR logic\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n        < ${PROJECT_DSIP_DEFAULTS_DIR}/dsip_lcr.sql\n\n    # Install schema for custom MaintMode logic\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n        < ${PROJECT_DSIP_DEFAULTS_DIR}/dsip_maintmode.sql\n\n    # Install schema for gwgroup call settings\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n        < ${PROJECT_DSIP_DEFAULTS_DIR}/dsip_call_settings.sql\n\n    # Install schema for Notifications\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n        < ${PROJECT_DSIP_DEFAULTS_DIR}/dsip_notification.sql\n\n    # Install schema for dsip_gw2gwgroup\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n        < ${PROJECT_DSIP_DEFAULTS_DIR}/dsip_gw2gwgroup.sql\n\n    # Install schema for dsip_gwgroup2lb\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n        < ${PROJECT_DSIP_DEFAULTS_DIR}/dsip_gwgroup2lb.sql\n\n    # Install schema for dsip_cdrinfo\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n        < ${PROJECT_DSIP_DEFAULTS_DIR}/dsip_cdrinfo.sql\n\n    # Install schema for dsip_settings\n    perl -e \"\\$hlen='$HASHED_CREDS_ENCODED_MAX_LEN'; \\$clen='$AESCTR_CREDS_ENCODED_MAX_LEN';\" \\\n        -pe 's%\\@HASHED_CREDS_ENCODED_MAX_LEN%$hlen%g; s%\\@AESCTR_CREDS_ENCODED_MAX_LEN%$clen%g;' \\\n        ${PROJECT_DSIP_DEFAULTS_DIR}/dsip_settings.sql |\n        withRootDBConn --db=\"$KAM_DB_NAME\" mysql\n\n    # Install schema for dsip_hardfwd and dsip_failfwd and dsip_prefix_mapping\n    sed -e \"s|FLT_INBOUND_REPLACE|${FLT_INBOUND}|g\" ${PROJECT_DSIP_DEFAULTS_DIR}/dsip_forwarding.sql |\n        withRootDBConn --db=\"$KAM_DB_NAME\" mysql\n\n    # TODO: we need to test and re-implement this.\n#    # required if tables exist and we are updating\n#    function resetIncrementers {\n#        SQL_TABLES=$(\n#            (for t in \"$@\"; do printf \",'$t'\"; done) | cut -d ',' -f '2-'\n#        )\n#\n#        # reset auto increment for related tables to max btwn the related tables\n#        INCREMENT=$(\n#            withRootDBConn mysql --skip-column-names -e \"\\\n#                SELECT MAX(AUTO_INCREMENT) FROM INFORMATION_SCHEMA.TABLES \\\n#                WHERE TABLE_SCHEMA = '$KAM_DB_NAME' \\\n#                AND TABLE_NAME IN($SQL_TABLES);\"\n#        )\n#        for t in \"$@\"; do\n#            withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n#                -e \"ALTER TABLE $t AUTO_INCREMENT=$INCREMENT\"\n#        done\n#    }\n#\n#    # reset auto incrementers for related tables\n#    resetIncrementers \"dr_gw_lists\"\n#    resetIncrementers \"uacreg\"\n\n    # truncate tables first if kamailio already installed\n    if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]; then\n        withRootDBConn --db=\"$KAM_DB_NAME\" mysql \\\n            -e \"TRUNCATE TABLE dr_gw_lists; TRUNCATE TABLE address; TRUNCATE TABLE dr_gateways; TRUNCATE TABLE dr_rules;\"\n    fi\n\n    # import default carriers and outbound routes\n    mkdir -p /tmp/defaults\n    # generate defaults subbing in dynamic values\n    cp -f ${PROJECT_DSIP_DEFAULTS_DIR}/dr_gw_lists.csv /tmp/defaults/dr_gw_lists.csv\n    sed \"s/FLT_CARRIER/$FLT_CARRIER/g; s/FLT_PBX/$FLT_PBX/g; s/FLT_MSTEAMS/$FLT_MSTEAMS/g\" \\\n        ${PROJECT_DSIP_DEFAULTS_DIR}/address.csv > /tmp/defaults/address.csv\n    sed \"s/FLT_CARRIER/$FLT_CARRIER/g; s/FLT_PBX/$FLT_PBX/g; s/FLT_MSTEAMS/$FLT_MSTEAMS/g\" \\\n        ${PROJECT_DSIP_DEFAULTS_DIR}/dr_gateways.csv > /tmp/defaults/dr_gateways.csv\n    sed \"s/FLT_OUTBOUND/$FLT_OUTBOUND/g; s/FLT_INBOUND/$FLT_INBOUND/g\" \\\n        ${PROJECT_DSIP_DEFAULTS_DIR}/dr_rules.csv > /tmp/defaults/dr_rules.csv\n\n    # import default carriers\n    withRootDBConn mysqlimport \\\n        --fields-terminated-by=';' --ignore-lines=0  -L $KAM_DB_NAME /tmp/defaults/dr_gw_lists.csv\n    withRootDBConn mysqlimport \\\n        --fields-terminated-by=';' --ignore-lines=0  -L $KAM_DB_NAME /tmp/defaults/address.csv\n    withRootDBConn mysqlimport \\\n        --fields-terminated-by=';' --ignore-lines=0  -L $KAM_DB_NAME /tmp/defaults/dr_gateways.csv\n    withRootDBConn mysqlimport \\\n        --fields-terminated-by=',' --ignore-lines=0  -L $KAM_DB_NAME ${PROJECT_DSIP_DEFAULTS_DIR}/dispatcher.csv\n    withRootDBConn mysqlimport \\\n        --fields-terminated-by=';' --ignore-lines=0  -L $KAM_DB_NAME /tmp/defaults/dr_rules.csv\n\n    # cleanup temp files\n    rm -rf /tmp/defaults\n}\n\n# Try to locate the Kamailio modules directory.  It will use the last modules directory found\nfunction fixMPATH() {\n    mpath=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h/' -quit 2>/dev/null)\n\n    if [ \"$mpath\" != '' ]; then\n        setKamailioConfigGlobal 'mpath' \"${mpath}\" ${DSIP_KAMAILIO_CONFIG_FILE}\n        printdbg \"The Kamailio mpath has been updated to: $mpath\"\n    else\n        printerr \"Can't find the module path for Kamailio.  Please ensure Kamailio is installed and try again!\"\n        exit 1\n    fi\n}\n\n# Requirements to run this script / any imported functions\nfunction installScriptRequirements() {\n    if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.requirementsinstalled\" ]; then\n        return\n    fi\n\n    printdbg 'Installing one-time script requirements'\n\n    case \"$DISTRO\" in\n    rocky|almalinux)\n        dnf install -y curl wget gawk perl sed git bind-utils openssl python3.11 jq vim-common coreutils\n        ;;\n    *)\n        if cmdExists 'apt-get'; then\n            apt-get update -y &&\n            apt-get install -y curl wget gawk perl sed git dnsutils openssl python3 jq xxd coreutils\n        elif cmdExists 'dnf'; then\n            dnf install -y curl wget gawk perl sed git bind-utils openssl python3 jq vim-common coreutils\n        elif cmdExists 'yum'; then\n            yum install -y curl wget gawk perl sed git bind-utils openssl python3 jq vim-common coreutils\n        fi\n        ;;\n    esac\n\n    if (( $? != 0 )); then\n        printerr 'Could not install script requirements'\n        exit 1\n    fi\n\n    # initialize the openssl rnd generator\n    dd if=/dev/urandom of=\"${HOME}/.rnd\" bs=1024 count=1 2>/dev/null\n\n    printdbg 'One-time script requirements installed'\n    touch ${DSIP_SYSTEM_CONFIG_DIR}/.requirementsinstalled\n    return 0\n}\n\n# Any setup that needs to be done before the script can run properly\nfunction setupScriptRequiredFiles() {\n    # make sure dirs exist required for this script\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}{,/gui,/kamailio,/rtpengine} ${SRC_DIR} ${DSIP_RUN_DIR} ${DSIP_LIB_DIR} ${DSIP_CERTS_DIR}{,/ca} ${BACKUPS_DIR}\n\n    # only copy the template file over to the DSIP_CONFIG_FILE if it doesn't already exist\n    if [[ ! -f \"${DSIP_CONFIG_FILE}\" ]]; then\n        # copy over the template settings.py to be worked on (used throughout this script as well)\n        cp -f ${DSIP_PROJECT_DIR}/gui/settings.py ${DSIP_CONFIG_FILE}\n    fi\n}\n\nfunction configureSystemPath() {\n    # fix PATH if needed\n    # we are using the default install paths but these may change in the future\n    if [[ ! -e \"$PATH_UPDATE_FILE\" ]]; then\n        mkdir -p $(dirname ${PATH_UPDATE_FILE})\n        (cat << 'EOF'\n#export PATH=\"/usr/local/bin${PATH:+:$PATH}\"\n#export PATH=\"${PATH:+$PATH:}/usr/sbin\"\n#export PATH=\"${PATH:+$PATH:}/usr/bin\"\n#export PATH=\"${PATH:+$PATH:}/sbin\"\nEOF\n        ) > ${PATH_UPDATE_FILE}\n    fi\n\n    # minimalistic approach avoids growing duplicates\n    # enable (uncomment) and import only what we need\n    local PATH_UPDATED=0\n\n    # - sipsak\n    if ! pathCheck /usr/local/bin; then\n        sed -i -r 's|^#(export PATH=\"/usr/local/bin\\$\\{PATH:\\+:\\$PATH\\}\")$|\\1|' ${PATH_UPDATE_FILE}\n        PATH_UPDATED=1\n    fi\n    # - rtpengine\n    if ! pathCheck /usr/sbin; then\n        sed -i -r 's|^#(export PATH=\"\\$\\{PATH:\\+\\$PATH:\\}/usr/sbin\")$|\\1|' ${PATH_UPDATE_FILE}\n        PATH_UPDATED=1\n    fi\n    # - dsiprouter\n    if ! pathCheck /usr/bin; then\n        sed -i -r 's|^#(export PATH=\"\\$\\{PATH:\\+\\$PATH:\\}/usr/bin\")$|\\1|' ${PATH_UPDATE_FILE}\n        PATH_UPDATED=1\n    fi\n    # - kamailio\n    if ! pathCheck /sbin; then\n        sed -i -r 's|^#(export PATH=\"\\$\\{PATH:\\+\\$PATH:\\}/sbin\")$|\\1|' ${PATH_UPDATE_FILE}\n        PATH_UPDATED=1\n    fi\n\n    # import new path definition if it was updated\n    (( ${PATH_UPDATED} == 1 )) &&  . ${PATH_UPDATE_FILE}\n\n    return 0\n}\n\nfunction revertSystemPath() {\n    rm -f ${PATH_UPDATE_FILE}\n\n    return 0\n}\n\n# Configure system repo sources to ensure we get the right package versions\n# TODO: dynamic mirror resolution based on RTT\n# TODO support multiple mirrors in repo configs\n#       - ubuntu refs:\n#       https://repogen.simplylinux.ch/\n#       https://mirrors.ustc.edu.cn/repogen/\n#       https://gist.github.com/rhuancarlos/c4d3c0cf4550db5326dca8edf1e76800\n#       - centos refs:\n#       https://unix.stackexchange.com/questions/52666/how-do-i-install-the-stock-centos-repositories\n#       https://wiki.centos.org/PackageManagement/Yum/Priorities\nfunction configureSystemRepos() {\n    if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.reposconfigured\" ]; then\n        return 0\n    fi\n\n    printdbg 'Configuring system repositories'\n\n    case \"$DISTRO\" in\n    debian|ubuntu)\n        if [[ ! -f \"$APT_DSIP_CONFIG\" ]]; then\n            # default dpkg to noninteractive modes for installs\n            cat <<'EOF' >${APT_DSIP_CONFIG}\nDpkg::Options {\n\"--force-confdef\";\n\"--force-confnew\";\n}\nDpkg::Lock::Timeout \"300\";\nAPT::Get::Fix-Missing \"1\";\nEOF\n        fi\n\n        # comment out cdrom in sources as it can halt install\n        sed -i -E 's/(^\\w.*cdrom.*)/#\\1/g' /etc/apt/sources.list\n\n        apt-get install -y apt-transport-https\n        mv -f ${APT_OFFICIAL_SOURCES} ${APT_OFFICIAL_SOURCES_BAK}\n        mv -f ${APT_OFFICIAL_PREFS} ${APT_OFFICIAL_PREFS_BAK} 2>/dev/null\n        cp -f ${DSIP_PROJECT_DIR}/resources/apt/${DISTRO}/${DISTRO_VER}/official-releases.list ${APT_OFFICIAL_SOURCES}\n        cp -f ${DSIP_PROJECT_DIR}/resources/apt/${DISTRO}/${DISTRO_VER}/official-releases.pref ${APT_OFFICIAL_PREFS}\n        apt-get update -y\n        ;;\n    almalinux)\n        # ref: https://almalinux.org/blog/2023-12-20-almalinux-8-key-update/\n        rpm --import https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux\n        ;;\n    # TODO: create official repo file (rhel/amzn/rocky/alma repo's?)\n    # TODO: install yum priorities plugin\n    # TODO: set priorities on official repo\n    #amzn)\n    #    ;;\n    esac\n\n    if (( $? == 1 )); then\n        printerr 'Could not configure system repositories'\n        exit 1\n    elif (( $? >= 100 )); then\n        printwarn 'Some issues occurred configuring system repositories, attempting to continue...'\n        touch ${DSIP_SYSTEM_CONFIG_DIR}/.reposconfigured\n    else\n        printdbg 'System repositories configured successfully'\n        touch ${DSIP_SYSTEM_CONFIG_DIR}/.reposconfigured\n    fi\n}\n\n# remove dsiprouter system configs\nfunction revertSystemRepos() {\n    if [[ ! -f \"${DSIP_SYSTEM_CONFIG_DIR}/.reposconfigured\" ]]; then\n        case \"$DISTRO\" in\n        debian|ubuntu)\n            if [[ \"$DISTRO\" == \"ubuntu\" ]] && (( ${DISTRO_MAJOR_VER} >= 24 )); then\n                APT_OFFICIAL_SOURCES=\"/etc/apt/sources.d/ubuntu.sources\"\n                APT_OFFICIAL_SOURCES_BAK=\"${BACKUPS_DIR}/original-sources.sources\"\n            else\n                APT_OFFICIAL_SOURCES=\"/etc/apt/sources.list\"\n                APT_OFFICIAL_SOURCES_BAK=\"${BACKUPS_DIR}/original-sources.list\"\n            fi\n            APT_OFFICIAL_PREFS=\"/etc/apt/preferences\"\n            APT_OFFICIAL_PREFS_BAK=\"${BACKUPS_DIR}/original-sources.pref\"\n            APT_DSIP_CONFIG=\"/etc/apt/apt.conf.d/99dsiprouter\"\n\n            mv -f ${APT_OFFICIAL_SOURCES_BAK} ${APT_OFFICIAL_SOURCES}\n            mv -f ${APT_OFFICIAL_PREFS_BAK} ${APT_OFFICIAL_PREFS} 2>/dev/null\n            apt-get update -y\n            ;;\n        esac\n    fi\n    if [[ -f \"$APT_DSIP_CONFIG\" ]]; then\n        rm -f \"$APT_DSIP_CONFIG\"\n    fi\n    rm -rf ${DSIP_SYSTEM_CONFIG_DIR}\n}\n\n# Install and configure mysql server\nfunction installMysql() {\n    if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.mysqlinstalled\" ]]; then\n        printwarn \"MySQL is already installed\"\n        return 0\n    fi\n\n    printdbg \"Attempting to install / configure MySQL...\"\n    ${DSIP_PROJECT_DIR}/mysql/${DISTRO}/${DISTRO_MAJOR_VER}.sh install\n\n    if (( $? != 0 )); then\n        printerr \"MySQL install failed\"\n        exit 1\n    fi\n\n    # Restart MySQL with the new configurations\n    systemctl restart mariadb\n    if systemctl is-active --quiet mariadb; then\n        touch ${DSIP_SYSTEM_CONFIG_DIR}/.mysqlinstalled\n        printdbg \"------------------------------------\"\n        pprint \"MySQL Installation is complete!\"\n        printdbg \"------------------------------------\"\n        return 0\n    else\n        printerr \"MySQL install failed\"\n        exit 1\n    fi\n}\n\n# Remove mysql and its configs\nfunction uninstallMysql() {\n    if [[ ! -f \"${DSIP_SYSTEM_CONFIG_DIR}/.mysqlinstalled\" ]]; then\n        printwarn \"MySQL is not installed - skipping removal\"\n        return 0\n    fi\n\n    printdbg \"Attempting to uninstall MySQL...\"\n    ${DSIP_PROJECT_DIR}/mysql/${DISTRO}/${DISTRO_MAJOR_VER}.sh uninstall\n\n    if (( $? != 0 )); then\n        printerr \"MySQL uninstall failed\"\n        exit 1\n    fi\n\n    # Remove the hidden installed file, which denotes if it's installed or not\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.mysqlinstalled\n    printdbg \"MySQL was uninstalled\"\n}\n\n# Install and configure nginx server\nfunction installNginx() {\n    if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.nginxinstalled\" ]]; then\n        printwarn \"nginx is already installed\"\n        return 0\n    fi\n\n    printdbg \"Attempting to install / configure nginx...\"\n    ${DSIP_PROJECT_DIR}/nginx/${DISTRO}/${DISTRO_MAJOR_VER}.sh install\n\n    if (( $? != 0 )); then\n        printerr \"nginx install failed\"\n        exit 1\n    fi\n\n    # Restart nginx with the new configurations\n    systemctl restart nginx\n    if systemctl is-active --quiet nginx; then\n        touch ${DSIP_SYSTEM_CONFIG_DIR}/.nginxinstalled\n        printdbg \"------------------------------------\"\n        pprint \"nginx Installation is complete!\"\n        printdbg \"------------------------------------\"\n        return 0\n    else\n        printerr \"nginx install failed\"\n        exit 1\n    fi\n}\n\n# Remove nginx and its configs\nfunction uninstallNginx() {\n    if [[ ! -f \"${DSIP_SYSTEM_CONFIG_DIR}/.nginxinstalled\" ]]; then\n        printwarn \"nginx is not installed - skipping removal\"\n        return 0\n    fi\n\n    printdbg \"Attempting to uninstall nginx...\"\n    ${DSIP_PROJECT_DIR}/nginx/${DISTRO}/${DISTRO_MAJOR_VER}.sh uninstall\n\n    if (( $? != 0 )); then\n        printerr \"nginx uninstall failed\"\n        exit 1\n    fi\n\n    # Remove the hidden installed file, which denotes if it's installed or not\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.nginxinstalled\n    printdbg \"nginx was uninstalled\"\n}\n\n# Install the RTPEngine from sipwise\nfunction installRTPEngine() {\n    if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\" ]; then\n        printwarn \"RTPEngine is already installed\"\n        return 0\n    fi\n\n    printdbg \"Attempting to install RTPEngine...\"\n    ${DSIP_PROJECT_DIR}/rtpengine/${DISTRO}/install.sh install\n    if (( $? != 0 )); then\n        printerr \"RTPEngine install failed\"\n        exit 1\n    fi\n\n    generateRtpengineConfig\n    # config updates that are the same across all OS\n    updateRtpengineConfig\n    # add the config updates to dsip-init service\n    updateRtpengineStartup\n\n    # restart RTPEngine with the new configurations\n    systemctl restart rtpengine\n    # did the service actually start with the changes?\n    if ! systemctl is-active --quiet rtpengine; then\n        printerr \"RTPEngine install failed\"\n        exit 1\n    fi\n    # sanity check, did the new kernel module load?\n    if ! lsmod | grep -q 'xt_RTPENGINE' 2>/dev/null; then\n        printwarn \"RTPEngine setup in userspace forwarding mode\"\n        printwarn \"you may need to reboot the system to load the new kernel\"\n    fi\n    # if we got here we know everything installed properly, update kamailio to use rtpengine\n    if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]; then\n        enableKamailioConfigAttrib 'WITH_RTPENGINE' ${DSIP_KAMAILIO_CONFIG_FILE}\n        systemctl restart kamailio\n    fi\n\n    touch ${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\n    printdbg \"------------------------------------\"\n    pprint \"RTPEngine Installation is complete!\"\n    printdbg \"------------------------------------\"\n    return 0\n}\n\n# Remove RTPEngine\nfunction uninstallRTPEngine() {\n    if [ ! -f \"${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\" ]; then\n        printwarn \"RTPEngine is not installed! - uninstalling anyway to be safe\"\n    fi\n\n    printdbg \"Attempting to uninstall RTPEngine...\"\n    ${DSIP_PROJECT_DIR}/rtpengine/${DISTRO}/install.sh uninstall\n\n    if (( $? == 0 )); then\n        if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]; then\n            disableKamailioConfigAttrib 'WITH_RTPENGINE' ${DSIP_KAMAILIO_CONFIG_FILE}\n            systemctl restart kamailio\n        fi\n    else\n        printerr \"RTPEngine uninstall failed\"\n        exit 1\n    fi\n\n    # remove rtpengine service dependencies\n    removeInitCmd \"/usr/bin/dsiprouter updatertpconfig\"\n    removeDependsOnInit \"rtpengine.service\"\n\n    # Remove the hidden installed file, which denotes if it's installed or not\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\n\n    printdbg \"RTPEngine was uninstalled\"\n}\n\nfunction installDsiprouterCli() {\n    local MAN_PROGS_DIR=\"/usr/share/man/man1\"\n\n    if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiproutercliinstalled\" ]; then\n        printwarn \"dSIPRouter CLI is already installed\"\n        return 0\n    fi\n\n    # TODO: add support for 'su' privilege escalation\n    printdbg \"Installing dSIPRouter CLI\"\n    if cmdExists 'apt-get'; then\n        apt-get install -y sudo\n    elif cmdExists 'dnf'; then\n        dnf install -y sudo\n    elif cmdExists 'yum'; then\n        yum install -y sudo\n    else\n        printerr 'Could not install dSIPRouter CLI - failed installing required packages'\n        exit 1\n    fi\n\n    # add dsiprouter CLI command to the path\n    ln -sf ${DSIP_PROJECT_DIR}/dsiprouter.sh /usr/bin/dsiprouter\n    # add specific commands to sudoers that dsiprouter can run with escalated privileges\n    cp -f ${DSIP_PROJECT_DIR}/dsiprouter/sudoers.d/99-dsiprouter ${DSIP_SUDOERS_FILE}\n\n    printdbg \"Installing 'dsiprouter' tab completion\"\n    if cmdExists 'apt-get'; then\n        apt-get install -y bash-completion\n    elif cmdExists 'dnf'; then\n        dnf install -y bash-completion\n    elif cmdExists 'yum'; then\n        yum install -y bash-completion\n    else\n        printerr 'Could not install bash tab completion - failed installing required packages'\n        exit 1\n    fi\n\n    # enable bash command line completion if not already\n    if [[ -f /etc/bash.bashrc ]]; then\n        perl -i -0777 -pe 's%#(if ! shopt -oq posix; then\\n)#([ \\t]+if \\[ -f /usr/share/bash-completion/bash_completion \\]; then\\n)#(.*?\\n)#(.*?\\n)#(.*?\\n)#(.*?\\n)#(.*?\\n)%\\1\\2\\3\\4\\5\\6\\7%s' /etc/bash.bashrc\n    fi\n    # add command line completion for dsiprouter CLI\n    cp -f ${DSIP_PROJECT_DIR}/dsiprouter/dsip_completion.sh /etc/bash_completion.d/dsiprouter\n\n    printdbg \"Installing 'dsiprouter' manpages\"\n    if cmdExists 'apt-get'; then\n        apt-get install -y manpages man-db\n    elif cmdExists 'dnf'; then\n        dnf install -y man-pages man-db man\n    elif cmdExists 'yum'; then\n        yum install -y man-pages man-db man\n    else\n        printerr 'Could not install manpages - failed installing required packages'\n        exit 1\n    fi\n\n    # setup the manpages\n    cp -f ${DSIP_PROJECT_DIR}/resources/man/dsiprouter.1 ${MAN_PROGS_DIR}/ &&\n    gzip -f ${MAN_PROGS_DIR}/dsiprouter.1 &&\n    mandb ||\n    printwarn 'Updating the mandb failed. Continuing the installation..'\n\n    touch \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiproutercliinstalled\"\n    printdbg \"dSIPRouter CLI installed\"\n    echo ''\n    printbold \"To enable tab completion in this terminal session run:\"\n    echo -ne \"source /etc/bash_completion\\n\\n\"\n    return 0\n}\n\nfunction uninstallDsiprouterCli() {\n    local MAN_PROGS_DIR=\"/usr/share/man/man1\"\n\n    if [ ! -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiproutercliinstalled\" ]; then\n        printwarn \"dSIPRouter CLI is not installed, skipping...\"\n        return 0\n    else\n        printdbg \"Uninstalling dSIPRouter CLI\"\n    fi\n\n    # remove dsiprouter and dsiprouterd commands from the path\n    rm -f /usr/bin/dsiprouter\n    # remove command line completion for dsiprouter.sh\n    rm -f /etc/bash_completion.d/dsiprouter\n\n    # remove dsiprouter sudoers file\n    rm -f ${DSIP_SUDOERS_FILE}\n\n    printdbg \"uninstalling dsiprouter manpages\"\n    rm -f ${MAN_PROGS_DIR}/dsiprouter.1\n    rm -f ${MAN_PROGS_DIR}/dsiprouter.1.gz\n    mandb\n    printdbg \"dsiprouter manpages uninstalled\"\n\n    rm -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiproutercliinstalled\"\n    printdbg \"dSIPRouter CLI uninstalled\"\n}\n\n# TODO: move documentation generation into its own separate function\n# TODO: allow password changes on cloud instances (remove password reset after image creation)\n# we should be starting the web server as root and dropping root privilege after\n# this is standard practice, but we would have to consider file permissions\n# it would be easier to manage if we moved dsiprouter configs to /etc/dsiprouter\nfunction installDsiprouter() {\n    local DSIP_CURRENT_PYTHON_VER\n\n    if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]; then\n        printwarn \"dSIPRouter is already installed\"\n        return 0\n    fi\n\n    printdbg \"Attempting to install dSIPRouter...\"\n    ${DSIP_PROJECT_DIR}/dsiprouter/${DISTRO}/${DISTRO_MAJOR_VER}.sh install\n\n    if (( $? != 0 )); then\n        printerr \"dSIPRouter install failed - OS install script failure\"\n        exit 1\n    fi\n\n    # extra check to ensure the OS specific script gave us the required python version\n    # this just needs to be true for the virtual environment so may be different than\n    # the system python version installed\n    DSIP_CURRENT_PYTHON_VER=$(${PYTHON_CMD} -V | cut -d ' ' -f 2)\n    versionCompare $DSIP_CURRENT_PYTHON_VER gteq $DSIP_MIN_PYTHON_VER\n    if (( $? != 0 )); then\n        printerr \"dSIPRouter install failed - minimum python version not installed\"\n        exit 1\n    fi\n\n    printdbg \"Configuring dSIPRouter settings\"\n    if [[ ! -f \"$DSIP_CONFIG_FILE\" ]]; then\n        generateDsiprouterConfig\n    fi\n\n    # Set dsip private key (used for encryption across services) by following precedence:\n    # 1:    set via cmdline arg\n    # 2:    set prior to externally\n    # 3:    generate new key\n    # TODO: create bash-native equivalent function for creating the private key\n    if [[ -n ${SET_DSIP_PRIV_KEY+set} ]]; then\n        printf '%s' \"${SET_DSIP_PRIV_KEY}\" > ${DSIP_PRIV_KEY}\n    elif [ -f \"${DSIP_PRIV_KEY}\" ]; then\n        :\n    elif (( ${RUNNING_UPGRADE:-0} == 0 )); then\n        # only generate if running a fresh install\n        ${PYTHON_CMD} -c \"import os,sys; os.chdir('${DSIP_PROJECT_DIR}/gui'); sys.path.insert(0, '${DSIP_SYSTEM_CONFIG_DIR}/gui'); from util.security import AES_CTR; AES_CTR.genKey()\"\n    fi\n\n    # Set credentials for our services, will either use credentials from CLI or generate them\n    if [[ -z ${SET_DSIP_GUI_PASS+set} ]] && (( ${RUNNING_UPGRADE:-0} == 0 )); then\n        if (( ${IMAGE_BUILD} == 1 || ${RESET_FORCE_INSTANCE_ID:-0} == 1 )); then\n            if [[ -z \"$CLOUD_PLATFORM\" ]]; then\n                printerr \"Cloud Instance password generation requested, but Cloud Platform is unsupported or not found\"\n                exit 1\n            fi\n            SET_DSIP_GUI_PASS=$(getInstanceID)\n        else\n            SET_DSIP_GUI_PASS=$(urandomChars 64)\n        fi\n    fi\n    if [[ -z ${SET_DSIP_API_TOKEN+set} ]] && (( ${RUNNING_UPGRADE:-0} == 0 )); then\n        SET_DSIP_API_TOKEN=$(urandomChars 64)\n    fi\n    if [[ -z ${SET_DSIP_IPC_TOKEN+set} ]] && (( ${RUNNING_UPGRADE:-0} == 0 )); then\n        SET_DSIP_IPC_TOKEN=$(urandomChars 64)\n    fi\n    if [[ -z ${SET_KAM_DB_PASS+set} ]] && (( ${RUNNING_UPGRADE:-0} == 0 )); then\n        SET_KAM_DB_PASS=$(urandomChars 64)\n    fi\n    SET_DSIP_SESSION_KEY=${SET_DSIP_SESSION_KEY:-$(decryptConfigAttrib 'DSIP_SESSION_KEY' ${DSIP_CONFIG_FILE})}\n    if [[ \"$SET_DSIP_SESSION_KEY\" == \"None\" ]] || [[ -z \"$SET_DSIP_SESSION_KEY\" ]]; then\n        SET_DSIP_SESSION_KEY=$(urandomChars 32)\n    fi\n\n    # pass the variables on to setCredentials()\n    SERVICE_RELOAD_DISABLED=1\n    setCredentials\n    unset SERVICE_RELOAD_DISABLED\n\n    # configure dsiprouter modules (must be run after potential credential updates above)\n    installModules\n\n    # update the rest of the settings\n    updateDsiprouterConfig\n\n    # update systemd startup dependencies\n    updateDsiprouterStartup\n\n    # NOTE: some of the previous files/dirs get updated here to allow dsiprouter access\n    updatePermissions -certs -kamailio -dsiprouter\n\n    # for cloud images the instance-id may change (could be a clone)\n    # add to cloud-init startup process a password reset to ensure its set correctly\n    # this is only for cloud image builds and will run when the instance is initialized or the instance-id is changed\n    if (( $IMAGE_BUILD == 1 )) && (( $AWS_ENABLED == 1 || $DO_ENABLED == 1 || $GCE_ENABLED == 1 || $AZURE_ENABLED == 1 || $VULTR_ENABLED == 1 )); then\n        (cat << EOF\n#!/usr/bin/env bash\n\n# reset admin user password\n/usr/bin/dsiprouter resetpassword -q -fid\n\nexit 0\nEOF\n        ) >/var/lib/cloud/scripts/per-instance/99-dsip-reset-guiadminpass.sh\n        chmod +x /var/lib/cloud/scripts/per-instance/99-dsip-reset-guiadminpass.sh\n\n        # Required changes for Debian-based images\n        case \"$DISTRO\" in\n            debian|ubuntu)\n                # Remove debian-sys-maint password for initial image scan\n                sed -i \"s/password =.*/password = /g\" /etc/mysql/debian.cnf\n\n                # Change default password for debian-sys-maint to instance-id at next boot\n                # we must also change the corresponding password in /etc/mysql/debian.cnf\n                # to comply with AWS AMI image standards\n                # this must run at startup as well so create temp script and add to dsip-init\n                (cat << EOF\n#!/usr/bin/env bash\n\n# declare imported functions from library\n$(declare -f isInstanceAMI)\n$(declare -f isInstanceDO)\n$(declare -f isInstanceGCE)\n$(declare -f isInstanceAZURE)\n$(declare -f isInstanceVULTR)\n$(declare -f getInstanceID)\n\n# wait for mysql to start\nwhile ! systemctl is-active --quiet mariadb; do\n    sleep 2\ndone\n\n# reset debian user password\nINSTANCE_ID=\\$(getInstanceID)\nmysql -e \"DROP USER 'debian-sys-maint'@'localhost';\n    CREATE USER 'debian-sys-maint'@'localhost' IDENTIFIED BY '\\${INSTANCE_ID}';\n    GRANT ALL ON *.* TO 'debian-sys-maint'@'localhost' IDENTIFIED BY '\\${INSTANCE_ID}';\n    FLUSH PRIVILEGES;\"\n\nsed -i \"s|password =.*|password = \\${INSTANCE_ID}|g\" /etc/mysql/debian.cnf\n\nexit 0\nEOF\n                ) >/var/lib/cloud/scripts/per-instance/99-dsip-reset-debsysuser.sh\n                chmod +x /var/lib/cloud/scripts/per-instance/99-dsip-reset-debsysuser.sh\n                ;;\n        esac\n    fi\n\n    # generate documentation for the GUI\n    # TODO: we should fix these errors\n    # TODO: we should move generated docs to DSIP_LIB_DIR to keep clean repo\n    (\n        cd ${DSIP_PROJECT_DIR}/docs &&\n        make -j $(nproc) html\n    )\n\n    # Restart mysql / dSIPRouter / nginx / Kamailio with new configurations\n    if [[ -f ${DSIP_SYSTEM_CONFIG_DIR}/.mysqlinstalled ]]; then\n        systemctl restart mariadb\n    fi\n    if [[ -f ${DSIP_SYSTEM_CONFIG_DIR}/.nginxinstalled ]]; then\n        systemctl restart nginx\n    fi\n    if [[ -f ${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled ]]; then\n        systemctl restart kamailio\n    fi\n    systemctl restart dsiprouter\n    if systemctl is-active --quiet dsiprouter; then\n        # custom dsiprouter MOTD banner for ssh logins\n        # only update on successful install so we don't confuse user\n        updateBanner\n\n        touch ${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\n        printdbg \"-------------------------------------\"\n        pprint \"dSIPRouter Installation is complete! \"\n        printdbg \"-------------------------------------\"\n        return 0\n    else\n        printerr \"dSIPRouter install failed\"\n        exit 1\n    fi\n}\n\nfunction uninstallDsiprouter() {\n    cd ${DSIP_PROJECT_DIR}\n\n    if [ ! -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]; then\n        printwarn \"dSIPRouter is not installed or failed during install - uninstalling anyway to be safe\"\n    fi\n\n    # stop the process\n    systemctl stop dsiprouter\n\n    printdbg \"Attempting to uninstall dSIPRouter UI...\"\n    ${DSIP_PROJECT_DIR}/dsiprouter/${DISTRO}/${DISTRO_MAJOR_VER}.sh uninstall\n\n    if [ $? -ne 0 ]; then\n        printerr \"dsiprouter uninstall failed\"\n        exit 1\n    fi\n\n    # Remove all dsiprouter crontab entries\n    printdbg \"Removing dsiprouter crontab entries\"\n    cronRemove -u root 'dsiprouter_cron.py'\n    cronRemove -u dsiprouter 'dsiprouter_cron.py'\n\n    # Remove dsip private key\n    rm -f ${DSIP_PRIV_KEY}\n\n    # revert to previous MOTD ssh login banner\n    revertBanner\n\n    # Remove the hidden installed file, which denotes if it's installed or not\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\n\n    printdbg \"dSIPRouter was uninstalled\"\n}\n\nfunction installKamailio() {\n    local KAMDB_DATABASE_BACKUP_FILE=\"${CURR_BACKUP_DIR}/db.sql\"\n    local KAMDB_USER_BACKUP_FILE=\"${CURR_BACKUP_DIR}/user.sql\"\n\n    if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]; then\n        printwarn \"kamailio is already installed\"\n        return 0\n    else\n        printdbg \"Attempting to install Kamailio...\"\n    fi\n\n    # backup and drop kam db if it exists already\n    mkdir -p ${CURR_BACKUP_DIR}\n\n    if cmdExists 'mysql'; then\n        if checkDB \"$KAM_DB_NAME\"; then\n            printdbg \"Backing up kamailio DB to ${KAMDB_DATABASE_BACKUP_FILE} before fresh install\"\n            dumpDB \"$KAM_DB_NAME\" > ${KAMDB_DATABASE_BACKUP_FILE}\n            withRootDBConn mysql -e \"DROP DATABASE $KAM_DB_NAME;\"\n            printdbg \"Backing up kamailio DB Users to ${KAMDB_USER_BACKUP_FILE} before fresh install\"\n            dumpDBUser \"${KAM_DB_USER}@${KAM_DB_NAME}\" > ${KAMDB_USER_BACKUP_FILE}\n            withRootDBConn mysql -e \"DROP USER IF EXISTS '$KAM_DB_USER'@'%'; DROP USER IF EXISTS '$KAM_DB_USER'@'localhost';\"\n        fi\n    fi\n\n    ${DSIP_PROJECT_DIR}/kamailio/${DISTRO}/${DISTRO_MAJOR_VER}.sh install\n    if (( $? == 0 )); then\n        configureSSL\n        configureKamailioDB\n        generateKamailioConfig\n        updateKamailioConfig\n        updateKamailioStartup\n    else\n        printerr \"kamailio install failed\"\n        exit 1\n    fi\n\n    # Restart Kamailio with the new configurations\n    systemctl restart kamailio\n    if systemctl is-active --quiet kamailio; then\n        touch ${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\n        printdbg \"-----------------------------------\"\n        pprint \"Kamailio Installation is complete!\"\n        printdbg \"-----------------------------------\"\n        return 0\n    else\n        printerr \"Kamailio install failed\"\n        exit 1\n    fi\n}\n\nfunction uninstallKamailio() {\n    cd ${DSIP_PROJECT_DIR}\n\n    if [ ! -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]; then\n        printwarn \"kamailio is not installed or failed during install - uninstalling anyway to be safe\"\n    fi\n\n    # stop the process\n    systemctl stop kamailio\n\n    printdbg \"Attempting to uninstall Kamailio...\"\n    ${DSIP_PROJECT_DIR}/kamailio/${DISTRO}/${DISTRO_MAJOR_VER}.sh uninstall\n\n    if [ $? -ne 0 ]; then\n        printerr \"kamailio uninstall failed\"\n        exit 1\n    fi\n\n    # remove kam service dependencies\n    removeInitCmd \"/usr/bin/dsiprouter updatekamconfig\"\n    removeDependsOnInit \"kamailio.service\"\n\n    # Remove the hidden installed file, which denotes if it's installed or not\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\n\n    printdbg \"kamailio was uninstalled\"\n}\n\n\nfunction installModules() {\n    # Install / Uninstall dSIPModules\n    for dir in ${DSIP_PROJECT_DIR}/gui/modules/*; do\n        if [[ -e ${dir}/install.sh ]]; then\n            ${dir}/install.sh\n        fi\n    done\n\n    # priming function restart() to start/stop services\n    # restart() may or may not be called after this depending on context\n    STOP_DSIPROUTER=1\n    START_DSIPROUTER=1\n    STOP_KAMAILIO=1\n    START_KAMAILIO=1\n    STOP_RTPENGINE=0\n    START_RTPENGINE=0\n\n    return 0\n}\n\nfunction installCron() {\n    if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.croninstalled\" ]; then\n        printwarn \"cron is already installed\"\n        return 0\n    else\n        printdbg \"Attempting to install cron\"\n    fi\n\n    if cmdExists 'apt-get'; then\n        apt-get install -y cron\n    elif cmdExists 'dnf'; then\n        dnf install -y cronie\n    elif cmdExists 'yum'; then\n        yum install -y cronie\n    fi\n\n    if (( $? != 0 )); then\n        printerr \"cron install failed\"\n        exit 1\n    fi\n\n    pprint \"cron was installed\"\n    touch ${DSIP_SYSTEM_CONFIG_DIR}/.croninstalled\n    return 0\n}\n\n# Install Sipsak\n# Used for testing and troubleshooting\nfunction installSipsak() {\n    local NPROC\n\n    if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.sipsakinstalled\" ]; then\n        printwarn \"SipSak is already installed\"\n        return 0\n    else\n        printdbg \"Attempting to install SipSak\"\n    fi\n\n    # Install sipsak requirements\n    if cmdExists 'apt-get'; then\n        apt-get install -y make gcc g++ automake autoconf openssl check git dirmngr pkg-config dh-autoreconf\n    elif cmdExists 'dnf'; then\n        dnf install -y make gcc gcc-c++ automake autoconf openssl check git perl-core\n    elif cmdExists 'yum'; then\n        yum install -y make gcc gcc-c++ automake autoconf openssl check git perl-core\n    fi\n\n    if (( $? != 0 )); then\n        printwarn \"SipSak install failed.. continuing without it\"\n        return 0\n    fi\n\n    NPROC=$(nproc)\n\n    # Install cpanm and perl deps (faster than cpan)\n    curl -L http://cpanmin.us | perl - --self-upgrade\n    cpanm URI::Escape\n\n    # compile and install from src\n    if [[ ! -d ${SRC_DIR}/sipsak ]]; then\n        git clone https://github.com/nils-ohlmeier/sipsak.git ${SRC_DIR}/sipsak\n    fi\n    (\n        cd ${SRC_DIR}/sipsak &&\n        autoreconf -i &&\n        ./configure &&\n        make -j $NPROC &&\n        make -j $NPROC install &&\n        exit 0 || exit 1\n    )\n\n    if (( $? == 0 )); then\n        pprint \"SipSak was installed\"\n        touch ${DSIP_SYSTEM_CONFIG_DIR}/.sipsakinstalled\n    else\n        printwarn \"SipSak install failed.. continuing without it\"\n    fi\n    return 0\n}\n\n# Remove Sipsak from the machine completely\nfunction uninstallSipsak() {\n    local START_DIR=\"$(pwd)\"\n\n    if [ ! -f \"${DSIP_SYSTEM_CONFIG_DIR}/.sipsakinstalled\" ]; then\n        printwarn \"sipsak is not installed or failed during install - uninstalling anyway to be safe\"\n    fi\n\n    if [ -d ${SRC_DIR}/sipsak ]; then\n        cd ${SRC_DIR}/sipsak\n        make uninstall\n        rm -rf ${SRC_DIR}/sipsak\n    fi\n\n    # Remove the hidden installed file, which denotes if it's installed or not\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.sipsakinstalled\n\n    cd ${START_DIR}\n}\n\n# Install DNSmasq stub resolver for local DNS\n# - used by kamailio dmq replication\nfunction installDnsmasq() {\n    if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dnsmasqinstalled\" ]; then\n        printwarn \"DNSmasq is already installed\"\n        return 0\n    fi\n\n    # create dnsmasq user and group\n    # output removed, some cloud providers (DO) use caching and output is misleading\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel dnsmasq &>/dev/null; groupdel dnsmasq &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"DNSmasq DNS Resolver\" dnsmasq &>/dev/null\n\n    printdbg \"Attempting to install DNSmasq...\"\n    if (( ${DISTRO_VER} == 12 )); then\n        ${DSIP_PROJECT_DIR}/dnsmasq/${DISTRO}/${DISTRO_VER}.sh install\n    else\n        ${DSIP_PROJECT_DIR}/dnsmasq/${DISTRO}/install.sh install\n    fi\n\n    if (( $? != 0 )); then\n        printerr \"DNSmasq install failed - OS install script failure\"\n        exit 1\n    fi\n\n    # make sure run dir is created with correct permissions\n    updatePermissions -dnsmasq\n\n    # setup hosts in cluster node is resolvable\n    # cron and kam service will configure these dynamically\n    if grep -q 'DSIP_CONFIG_START' /etc/hosts 2>/dev/null; then\n        perl -e \"\\$int_ip='${INTERNAL_IP_ADDR}'; \\$ext_ip='${EXTERNAL_IP_ADDR}'; \\$int_fqdn='${INTERNAL_FQDN}'; \\$ext_fqdn='${EXTERNAL_FQDN}';\" \\\n            -0777 -i -pe 's|(#+DSIP_CONFIG_START).*?(#+DSIP_CONFIG_END)|\\1\\n${int_ip} ${int_fqdn} local.cluster\\n${ext_ip} ${ext_fqdn} local.cluster\\n\\2|gms' /etc/hosts\n    else\n        printf '\\n%s\\n%s\\n%s\\n%s\\n' \\\n            '#####DSIP_CONFIG_START' \\\n            \"${INTERNAL_IP_ADDR} ${INTERNAL_FQDN} local.cluster\" \\\n            \"${EXTERNAL_IP_ADDR} ${EXTERNAL_FQDN} local.cluster\" \\\n            '#####DSIP_CONFIG_END' >>/etc/hosts\n    fi\n    # add section for the pacemaker features\n    if ! grep -q 'PACEMAKER_CONFIG_START' /etc/hosts 2>/dev/null; then\n        printf '\\n%s\\n%s\\n' \\\n            '#####PACEMAKER_CONFIG_START' \\\n            '#####PACEMAKER_CONFIG_END' >>/etc/hosts\n    fi\n\n    # update DNS hosts prior to dSIPRouter startup\n    addInitCmd \"/usr/bin/dsiprouter updatednsconfig\"\n    # update DNS hosts every minute\n    if ! crontab -u root -l 2>/dev/null | grep -q '/usr/bin/dsiprouter updatednsconfig' 2>/dev/null; then\n        cronAppend -u root '0 * * * * /usr/bin/dsiprouter updatednsconfig'\n    fi\n\n    systemctl restart dnsmasq\n    if systemctl is-active --quiet dnsmasq; then\n        touch ${DSIP_SYSTEM_CONFIG_DIR}/.dnsmasqinstalled\n        pprint \"DNSmasq was installed\"\n        return 0\n    else\n        printerr \"DNSmasq install failed\"\n        exit 1\n    fi\n}\n\nfunction uninstallDnsmasq() {\n    if [ ! -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dnsmasqinstalled\" ]; then\n        printwarn \"DNSmasq is not installed or failed during install - uninstalling anyway to be safe\"\n    fi\n\n    printdbg \"Attempting to uninstall DNSmasq...\"\n    ${DSIP_PROJECT_DIR}/dnsmasq/${DISTRO}/install.sh uninstall\n\n    if (( $? != 0 )); then\n        printerr \"DNSmasq uninstall failed - OS install script failure\"\n        exit 1\n    fi\n\n    # remove dnsmasq configuration\n    rm -f /etc/dnsmasq.conf /etc/dnsmasq.conf.bak 2>/dev/null\n\n    # remove localhost from name servers\n    sed -ir -e '/#+DSIP_CONFIG_START/,/#+DSIP_CONFIG_END/d' /etc/dhcp/dhclient.conf\n    sed -i -e '/nameserver 127.0.0.1/d' /etc/resolv.conf\n\n    # remove cluster hosts from /etc/hosts\n    sed -ir -e '/#+DSIP_CONFIG_START/,/#+DSIP_CONFIG_END/d' /etc/hosts\n    # remove pacemaker section from /etc/hosts\n    sed -ir -e '/#+PACEMAKER_CONFIG_START/,/#+PACEMAKER_CONFIG_END/d' /etc/hosts\n\n    # remove cron job and init command\n    removeInitCmd \"/usr/bin/dsiprouter updatednsconfig\"\n    cronRemove -u root '/usr/bin/dsiprouter updatednsconfig'\n\n    # Remove the hidden installed file, which denotes if it's installed or not\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.dnsmasqinstalled\n    printdbg \"DNSmasq was uninstalled\"\n\n    return 0\n}\n\nfunction start() {\n    local START_DSIPROUTER=${START_DSIPROUTER:-1}\n    local START_KAMAILIO=${START_KAMAILIO:-0}\n    local START_RTPENGINE=${START_RTPENGINE:-0}\n\n    # Start Kamailio if told to and installed\n    if (( $START_KAMAILIO == 1 )) && [ -e ${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled ]; then\n        systemctl start kamailio\n        # Make sure process is still running\n        if ! systemctl is-active --quiet kamailio; then\n            printerr \"Unable to start Kamailio\"\n            exit 1\n        else\n            pprint \"Kamailio was started\"\n        fi\n    fi\n\n    # Start RTPEngine if told to and installed\n    if (( $START_RTPENGINE == 1 )) && [ -e ${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled ]; then\n        systemctl start rtpengine\n        # Make sure process is still running\n        if ! systemctl is-active --quiet rtpengine; then\n            printerr \"Unable to start RTPEngine\"\n            exit 1\n        else\n            pprint \"RTPEngine was started\"\n        fi\n    fi\n\n    # Start dSIPRouter if told to and installed\n    if (( $START_DSIPROUTER == 1 )) && [ -e ${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled ]; then\n        # update runtime settings from CLI args\n        updateDsiprouterConfigRuntimeSettings\n\n        if (( $DEBUG == 1 )); then\n            # start the reverse proxy first\n            systemctl start nginx\n            # perform pre-startup commands systemd would normally do in dsiprouter.service\n            updatePermissions -dsiprouter\n            # keep dSIPRouter in the foreground, only used for debugging issues (blocking)\n            sudo -E -u dsiprouter -g dsiprouter ${PYTHON_CMD} ${DSIP_PROJECT_DIR}/gui/dsiprouter.py\n            exit $?\n        else\n            # start the reverse proxy first\n            systemctl start nginx\n            # normal startup, fork dSIPRouter as background process\n            systemctl start dsiprouter\n            # Make sure process is still running\n            if ! systemctl is-active --quiet dsiprouter || ! systemctl is-active --quiet nginx; then\n                printerr \"Unable to start dSIPRouter\"\n                exit 1\n            else\n                pprint \"dSIPRouter was started\"\n            fi\n        fi\n    fi\n}\n\nfunction stop() {\n    local STOP_DSIPROUTER=${STOP_DSIPROUTER:-1}\n    local STOP_KAMAILIO=${STOP_KAMAILIO:-0}\n    local STOP_RTPENGINE=${STOP_RTPENGINE:-0}\n\n    # Stop Kamailio if told to and installed\n    if (( $STOP_KAMAILIO == 1 )) && [ -e ${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled ]; then\n        systemctl stop kamailio\n        # Make sure process is not running\n        if systemctl is-active --quiet kamailio; then\n            printerr \"Unable to stop Kamailio\"\n            exit 1\n        else\n            pprint \"Kamailio was stopped\"\n        fi\n    fi\n\n    # Stop RTPEngine if told to and installed\n    if (( $STOP_RTPENGINE == 1 )) && [ -e ${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled ]; then\n        systemctl stop rtpengine\n        # Make sure process is not running\n        if systemctl is-active --quiet rtpengine; then\n            printerr \"Unable to stop RTPEngine\"\n            exit 1\n        else\n            pprint \"RTPEngine was stopped\"\n        fi\n    fi\n\n    # Stop the dSIPRouter if told to and installed\n    if (( $STOP_DSIPROUTER == 1 )) && [ -e ${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled ]; then\n        systemctl stop nginx\n        # if started in debug mode we have to manually kill the process\n        if ! systemctl is-active --quiet dsiprouter; then\n            pkill -SIGTERM -f dsiprouter.py\n            if pgrep -f 'nginx|dsiprouter.py' &>/dev/null; then\n                printerr \"Unable to stop dSIPRouter\"\n                exit 1\n            else\n                pprint \"dSIPRouter was stopped\"\n            fi\n        else\n            systemctl stop nginx\n            systemctl stop dsiprouter\n            if systemctl is-active --quiet dsiprouter || systemctl is-active --quiet nginx; then\n                printerr \"Unable to stop dSIPRouter\"\n                exit 1\n            else\n                pprint \"dSIPRouter was stopped\"\n            fi\n        fi\n    fi\n}\n\nfunction restart() {\n    # escape the systemd control group if told to daemonize\n    if (( ${RESTART_DAEMONIZE:-0} == 1 )); then\n        systemd-run --unit='dsip-daemon' --collect --slice=user.slice $0 ${RESTART_ARGS[@]}\n        exit 0\n    fi\n\n    stop\n    start\n}\n\nfunction displayLoginInfo() {\n    local DSIP_USERNAME=${DSIP_USERNAME:-$(getConfigAttrib 'DSIP_USERNAME' ${DSIP_CONFIG_FILE})}\n    local DSIP_PASSWORD=${DSIP_PASSWORD:-\"<HASH CAN NOT BE UNDONE> (reset password if you forgot it)\"}\n    local DSIP_API_TOKEN=${DSIP_API_TOKEN:-$(decryptConfigAttrib 'DSIP_API_TOKEN' ${DSIP_CONFIG_FILE})}\n    local DSIP_IPC_SOCK=\"$(getConfigAttrib 'DSIP_IPC_SOCK' ${DSIP_CONFIG_FILE})\"\n    local DSIP_IPC_PASS=${DSIP_IPC_PASS:-$(decryptConfigAttrib 'DSIP_IPC_PASS' ${DSIP_CONFIG_FILE})}\n    local KAM_DB_USER=${KAM_DB_USER:-$(getConfigAttrib 'KAM_DB_USER' ${DSIP_CONFIG_FILE})}\n    local KAM_DB_PASS=${KAM_DB_PASS:-$(decryptConfigAttrib 'KAM_DB_PASS' ${DSIP_CONFIG_FILE})}\n\n    echo -ne '\\n'\n    printdbg \"Your systems credentials are below (keep in a safe place)\"\n    pprint \"dSIPRouter GUI Username: ${DSIP_USERNAME}\"\n    pprint \"dSIPRouter GUI Password: ${DSIP_PASSWORD}\"\n    pprint \"dSIPRouter API Token: ${DSIP_API_TOKEN}\"\n    pprint \"dSIPRouter IPC Password: ${DSIP_IPC_PASS}\"\n    pprint \"Kamailio DB Username: ${KAM_DB_USER}\"\n    pprint \"Kamailio DB Password: ${KAM_DB_PASS}\"\n    echo -ne '\\n'\n\n    printdbg \"You can access the dSIPRouter WEB GUI here\"\n    pprint \"Domain Name: ${DSIP_PROTO}://${EXTERNAL_FQDN}:${DSIP_PORT}\"\n    if [ \"$EXTERNAL_IP_ADDR\" != \"$INTERNAL_IP_ADDR\" ];then\n        pprint \"External IP: ${DSIP_PROTO}://${EXTERNAL_IP_ADDR}:${DSIP_PORT}\"\n        pprint \"Internal IP: ${DSIP_PROTO}://${INTERNAL_IP_ADDR}:${DSIP_PORT}\"\n    else\n        pprint \"IP Address: ${DSIP_PROTO}://${EXTERNAL_IP_ADDR}:${DSIP_PORT}\"\n    fi\n    echo -ne '\\n'\n\n    printdbg \"You can access the dSIPRouter REST API here\"\n    if [ \"$EXTERNAL_IP_ADDR\" != \"$INTERNAL_IP_ADDR\" ];then\n        pprint \"External IP: ${DSIP_API_PROTO}://${EXTERNAL_IP_ADDR}:${DSIP_PORT}\"\n        pprint \"Internal IP: ${DSIP_API_PROTO}://${INTERNAL_IP_ADDR}:${DSIP_PORT}\"\n    else\n        pprint \"IP Address: ${DSIP_API_PROTO}://${EXTERNAL_IP_ADDR}:${DSIP_PORT}\"\n    fi\n    echo -ne '\\n'\n\n    printdbg \"You can access the dSIPRouter IPC API here\"\n    pprint \"UNIX Domain Socket: ${DSIP_IPC_SOCK}\"\n    echo -ne '\\n'\n\n    printdbg \"You can access the Kamailio DB here\"\n    pprint \"Database Host: ${KAM_DB_HOST}:${KAM_DB_PORT}\"\n    pprint \"Database Name: ${KAM_DB_NAME}\"\n    echo -ne '\\n'\n}\n\n# updates credentials in dsip / kam config files / kam db\n# also exports credentials to variables for latter commands\n# note: updating KAM_DB_HOST will implicitly update ROOT_DB_HOST if not set\n# TODO: currently there is no way to set values to the empty string\nfunction setCredentials() {\n    printdbg 'Setting credentials'\n\n    # variables that can be set prior to running\n    # SET_DSIP_GUI_USER\n    # SET_DSIP_GUI_PASS\n    # SET_DSIP_API_TOKEN\n    # SET_DSIP_MAIL_USER\n    # SET_DSIP_MAIL_PASS\n    # SET_DSIP_IPC_TOKEN\n    # SET_KAM_DB_USER\n    # SET_KAM_DB_PASS\n    # SET_KAM_DB_HOST\n    # SET_KAM_DB_PORT\n    # SET_KAM_DB_NAME\n    # SET_ROOT_DB_USER\n    # SET_ROOT_DB_PASS\n    # SET_ROOT_DB_HOST\n    # SET_ROOT_DB_PORT\n    # SET_ROOT_DB_NAME\n    # SET_DSIP_SESSION_KEY\n    # SERVICE_RELOAD_DISABLED\n    if [[ -z ${SET_ROOT_DB_HOST+unset} && -n ${SET_KAM_DB_HOST+set} ]]; then\n        SET_ROOT_DB_HOST=\"$SET_KAM_DB_HOST\"\n    fi\n\n    local LOAD_SETTINGS_FROM=${LOAD_SETTINGS_FROM:-$(getConfigAttrib 'LOAD_SETTINGS_FROM' ${DSIP_CONFIG_FILE})}\n    local DSIP_ID=${DSIP_ID:-$(getConfigAttrib 'DSIP_ID' ${DSIP_CONFIG_FILE})}\n    local DSIP_CLUSTER_ID=${DSIP_CLUSTER_ID:-$(getConfigAttrib 'DSIP_CLUSTER_ID' ${DSIP_CONFIG_FILE})}\n    local DSIP_CLUSTER_SYNC=${DSIP_CLUSTER_SYNC:-$([[ \"$(getConfigAttrib 'DSIP_CLUSTER_SYNC' ${DSIP_CONFIG_FILE})\" == \"True\" ]] && echo '1' || echo '0')}\n    # the commands to execute for these updates\n    local SHELL_CMDS=() SQL_STATEMENTS=() DEFERRED_SQL_STATEMENTS=()\n    # how settings will be propagated to live systems\n    # 0 == no reload required, 1 == hot reload required, 2 == service reload required\n    # note that parsing variables for higher numbered reloading should take precedence\n    local DSIP_RELOAD_TYPE=1 KAM_RELOAD_TYPE=0 MYSQL_RELOAD_TYPE=0\n    # if calling script does not want changes propagated yet, set this variable\n    local SERVICE_RELOAD_DISABLED=${SERVICE_RELOAD_DISABLED:-0}\n    # whether or not we will be running logic to update settings on the DB\n    local RUN_SQL_STATEMENTS=1\n    # the type of mysql DB server detected\n    local MYSQL_VARIANT='mariadb'\n    local TMP_VAL\n\n    # sanity check, can we connect to the DB as the root user?\n    # we determine if user already changed DB creds (and just want dsiprouter to store them accordingly)\n    if withGivenDB --user=\"$ROOT_DB_USER\" --pass=\"$ROOT_DB_PASS\" --host=\"$ROOT_DB_HOST\" --port=\"$ROOT_DB_PORT\" \\\n    mysql -e 'SELECT VERSION();' &>/dev/null; then\n        :\n    elif withGivenDB --user=\"${SET_ROOT_DB_USER:-$ROOT_DB_USER}\" --pass=\"${SET_ROOT_DB_PASS:-$ROOT_DB_PASS}\" \\\n    --host=\"${SET_ROOT_DB_HOST:-$ROOT_DB_HOST}\" --port=\"${SET_ROOT_DB_PORT:-$ROOT_DB_PORT}\" \\\n    --db=\"${SET_ROOT_DB_NAME:-$ROOT_DB_NAME}\" mysql -e 'SELECT VERSION();' &>/dev/null; then\n        ROOT_DB_HOST=${SET_ROOT_DB_HOST:-$ROOT_DB_HOST}\n        ROOT_DB_PORT=${SET_ROOT_DB_PORT:-$ROOT_DB_PORT}\n        ROOT_DB_USER=${SET_ROOT_DB_USER:-$ROOT_DB_USER}\n        ROOT_DB_PASS=${SET_ROOT_DB_PASS:-$ROOT_DB_PASS}\n        ROOT_DB_NAME=${SET_ROOT_DB_NAME:-$ROOT_DB_NAME}\n    else\n        # allow for updating settings prior to mysql being started but make sure it would be a valid update\n        # no update that requires the DB access will work if we reached here so we validate or exit\n        if [[ \"$LOAD_SETTINGS_FROM\" == \"db\" ]] ||\n        [[ -n ${SET_KAM_DB_USER+set} ]] ||\n        [[ -n ${SET_KAM_DB_PASS+set} ]] ||\n        [[ -n ${SET_KAM_DB_HOST+set} ]] ||\n        [[ -n ${SET_KAM_DB_PORT+set} ]] ||\n        [[ -n ${SET_KAM_DB_NAME+set} ]] ||\n        [[ -n ${SET_ROOT_DB_USER+set} ]] ||\n        [[ -n ${SET_ROOT_DB_PASS+set} ]] ||\n        [[ -n ${SET_ROOT_DB_HOST+set} ]] ||\n        [[ -n ${SET_ROOT_DB_PORT+set} ]] ||\n        [[ -n ${SET_ROOT_DB_NAME+set} ]]; then\n            printerr 'Connection to DB failed'\n            exit 1\n        fi\n        # no DB updates necessary\n        RUN_SQL_STATEMENTS=0\n    fi\n\n    # determine type of DB server if we need to make updates\n    if (( $RUN_SQL_STATEMENTS == 1 )); then\n        if ! withRootDBConn mysql -se \"SELECT PASSWORD('');\" &>/dev/null; then\n            MYSQL_VARIANT='mysql'\n        fi\n        sqlAsTransaction --user=\"$ROOT_DB_USER\" --pass=\"$ROOT_DB_PASS\" --host=\"$ROOT_DB_HOST\" --port=\"$ROOT_DB_PORT\" \"${SQL_STATEMENTS[@]}\"\n    fi\n\n    # update non-encrypted settings locally and gather statements for updating DB\n    if [[ -n ${SET_DSIP_GUI_USER+set} ]]; then\n        SQL_STATEMENTS+=(\"update kamailio.dsip_settings SET DSIP_USERNAME='$SET_DSIP_GUI_USER' WHERE DSIP_ID='$DSIP_ID';\")\n        if (( $DSIP_CLUSTER_SYNC == 1 )); then\n            SQL_STATEMENTS+=(\"update kamailio.dsip_settings SET DSIP_USERNAME='$SET_DSIP_GUI_USER' WHERE DSIP_CLUSTER_ID='$DSIP_CLUSTER_ID' AND DSIP_CLUSTER_SYNC='1' AND DSIP_ID!='$DSIP_ID';\")\n        fi\n        SHELL_CMDS+=(\"setConfigAttrib 'DSIP_USERNAME' '$SET_DSIP_GUI_USER' ${DSIP_CONFIG_FILE} -q;\")\n    fi\n    if [[ -n ${SET_DSIP_GUI_PASS+set} ]]; then\n        TMP_VAL=$(hashCreds \"$SET_DSIP_GUI_PASS\")\n        SQL_STATEMENTS+=(\"update kamailio.dsip_settings SET DSIP_PASSWORD='$TMP_VAL' WHERE DSIP_ID='$DSIP_ID';\")\n        if (( $DSIP_CLUSTER_SYNC == 1 )); then\n            SQL_STATEMENTS+=(\"update kamailio.dsip_settings SET DSIP_PASSWORD='$TMP_VAL' WHERE DSIP_CLUSTER_ID='$DSIP_CLUSTER_ID' AND DSIP_CLUSTER_SYNC='1' AND DSIP_ID!='$DSIP_ID';\")\n        fi\n        SHELL_CMDS+=(\"setConfigAttrib 'DSIP_PASSWORD' '$TMP_VAL' ${DSIP_CONFIG_FILE} -qb;\")\n    fi\n\n    if [[ -n ${SET_DSIP_MAIL_USER+set} ]]; then\n        SQL_STATEMENTS+=(\"update kamailio.dsip_settings SET MAIL_USERNAME='$SET_DSIP_MAIL_USER' WHERE DSIP_ID='$DSIP_ID';\")\n        if (( $DSIP_CLUSTER_SYNC == 1 )); then\n            SQL_STATEMENTS+=(\"update kamailio.dsip_settings SET MAIL_USERNAME='$SET_DSIP_MAIL_USER' WHERE DSIP_CLUSTER_ID='$DSIP_CLUSTER_ID' AND DSIP_CLUSTER_SYNC='1' AND DSIP_ID!='$DSIP_ID';\")\n        fi\n        SHELL_CMDS+=(\"setConfigAttrib 'MAIL_USERNAME' '$SET_DSIP_MAIL_USER' ${DSIP_CONFIG_FILE} -q;\")\n    fi\n    if [[ -n ${SET_DSIP_MAIL_PASS+set} ]]; then\n        TMP_VAL=$(encryptCreds -kf \"$DSIP_PRIV_KEY\" \"$SET_DSIP_MAIL_PASS\")\n        SQL_STATEMENTS+=(\"update kamailio.dsip_settings SET MAIL_PASSWORD='$TMP_VAL' WHERE DSIP_ID='$DSIP_ID';\")\n        if (( $DSIP_CLUSTER_SYNC == 1 )); then\n            SQL_STATEMENTS+=(\"update kamailio.dsip_settings SET MAIL_PASSWORD='$TMP_VAL' WHERE DSIP_CLUSTER_ID='$DSIP_CLUSTER_ID' AND DSIP_CLUSTER_SYNC='1' AND DSIP_ID!='$DSIP_ID';\")\n        fi\n        SHELL_CMDS+=(\"setConfigAttrib 'MAIL_PASSWORD' '$TMP_VAL' ${DSIP_CONFIG_FILE} -qb;\")\n    fi\n    if [[ -n ${SET_DSIP_API_TOKEN+set} ]]; then\n        TMP_VAL=$(encryptCreds -kf \"$DSIP_PRIV_KEY\" \"$SET_DSIP_API_TOKEN\")\n        SQL_STATEMENTS+=(\"update kamailio.dsip_settings SET DSIP_API_TOKEN='$TMP_VAL' WHERE DSIP_ID='$DSIP_ID';\")\n        if (( $DSIP_CLUSTER_SYNC == 1 )); then\n            SQL_STATEMENTS+=(\"update kamailio.dsip_settings SET DSIP_API_TOKEN='$TMP_VAL' WHERE DSIP_CLUSTER_ID='$DSIP_CLUSTER_ID' AND DSIP_CLUSTER_SYNC='1' AND DSIP_ID!='$DSIP_ID';\")\n        fi\n        SHELL_CMDS+=(\"setConfigAttrib 'DSIP_API_TOKEN' '$TMP_VAL' ${DSIP_CONFIG_FILE} -qb;\")\n\n        KAM_RELOAD_TYPE=1\n    fi\n    if [[ -n ${SET_DSIP_IPC_TOKEN+set} ]]; then\n        TMP_VAL=$(encryptCreds -kf \"$DSIP_PRIV_KEY\" \"$SET_DSIP_IPC_TOKEN\")\n        SQL_STATEMENTS+=(\"update kamailio.dsip_settings SET DSIP_IPC_PASS='$TMP_VAL' WHERE DSIP_ID='$DSIP_ID';\")\n        if (( $DSIP_CLUSTER_SYNC == 1 )); then\n            SQL_STATEMENTS+=(\"update kamailio.dsip_settings SET DSIP_IPC_PASS='$TMP_VAL' WHERE DSIP_CLUSTER_ID='$DSIP_CLUSTER_ID' AND DSIP_CLUSTER_SYNC='1' AND DSIP_ID!='$DSIP_ID';\")\n        fi\n        SHELL_CMDS+=(\"setConfigAttrib 'DSIP_IPC_PASS' '$TMP_VAL' ${DSIP_CONFIG_FILE} -qb;\")\n\n        DSIP_RELOAD_TYPE=2\n    fi\n    if [[ -n ${SET_KAM_DB_USER+set} ]]; then\n        DEFERRED_SQL_STATEMENTS+=(\"DROP USER IF EXISTS '$KAM_DB_USER'@'localhost';\")\n        DEFERRED_SQL_STATEMENTS+=(\"DROP USER IF EXISTS '$KAM_DB_USER'@'%';\")\n        DEFERRED_SQL_STATEMENTS+=(\"DROP USER IF EXISTS '$SET_KAM_DB_USER'@'localhost';\")\n        DEFERRED_SQL_STATEMENTS+=(\"DROP USER IF EXISTS '$SET_KAM_DB_USER'@'%';\")\n        DEFERRED_SQL_STATEMENTS+=(\"CREATE USER '$SET_KAM_DB_USER'@'localhost' IDENTIFIED BY '${SET_KAM_DB_PASS:-$KAM_DB_PASS}';\")\n        DEFERRED_SQL_STATEMENTS+=(\"GRANT ALL PRIVILEGES ON $KAM_DB_NAME.* TO '$SET_KAM_DB_USER'@'localhost';\")\n        DEFERRED_SQL_STATEMENTS+=(\"CREATE USER '$SET_KAM_DB_USER'@'%' IDENTIFIED BY '${SET_KAM_DB_PASS:-$KAM_DB_PASS}';\")\n        DEFERRED_SQL_STATEMENTS+=(\"GRANT ALL PRIVILEGES ON $KAM_DB_NAME.* TO '$SET_KAM_DB_USER'@'%';\")\n\n        SQL_STATEMENTS+=(\"UPDATE kamailio.dsip_settings SET KAM_DB_USER='$SET_KAM_DB_USER' WHERE DSIP_ID='$DSIP_ID';\")\n        if (( $DSIP_CLUSTER_SYNC == 1 )); then\n            SQL_STATEMENTS+=(\"UPDATE kamailio.dsip_settings SET KAM_DB_USER='$SET_KAM_DB_USER' WHERE DSIP_CLUSTER_ID='$DSIP_CLUSTER_ID' AND DSIP_CLUSTER_SYNC='1' AND DSIP_ID!='$DSIP_ID';\")\n        fi\n        SHELL_CMDS+=(\"setConfigAttrib 'KAM_DB_USER' '$SET_KAM_DB_USER' ${DSIP_CONFIG_FILE} -q;\")\n\n        DSIP_RELOAD_TYPE=2\n        KAM_RELOAD_TYPE=2\n    fi\n    if [[ -n ${SET_KAM_DB_PASS+set} ]]; then\n        if [[ \"$MYSQL_VARIANT\" == 'mysql' ]]; then\n            DEFERRED_SQL_STATEMENTS+=(\"ALTER USER '${SET_KAM_DB_USER:-$KAM_DB_USER}'@'localhost' IDENTIFIED BY '$SET_KAM_DB_PASS';\")\n            DEFERRED_SQL_STATEMENTS+=(\"ALTER USER '${SET_KAM_DB_USER:-$KAM_DB_USER}'@'%' IDENTIFIED BY '$SET_KAM_DB_PASS';\")\n        else\n            DEFERRED_SQL_STATEMENTS+=(\"SET PASSWORD FOR '${SET_KAM_DB_USER:-$KAM_DB_USER}'@'localhost' = PASSWORD('$SET_KAM_DB_PASS');\")\n            DEFERRED_SQL_STATEMENTS+=(\"SET PASSWORD FOR '${SET_KAM_DB_USER:-$KAM_DB_USER}'@'%' = PASSWORD('$SET_KAM_DB_PASS');\")\n        fi\n\n        TMP_VAL=$(encryptCreds -kf \"$DSIP_PRIV_KEY\" \"$SET_KAM_DB_PASS\")\n        SQL_STATEMENTS+=(\"update kamailio.dsip_settings SET KAM_DB_PASS='$TMP_VAL' WHERE DSIP_ID='$DSIP_ID';\")\n        if (( $DSIP_CLUSTER_SYNC == 1 )); then\n            SQL_STATEMENTS+=(\"update kamailio.dsip_settings SET KAM_DB_PASS='$TMP_VAL' WHERE DSIP_CLUSTER_ID='$DSIP_CLUSTER_ID' AND DSIP_CLUSTER_SYNC='1' AND DSIP_ID!='$DSIP_ID';\")\n        fi\n        SHELL_CMDS+=(\"setConfigAttrib 'KAM_DB_PASS' '$TMP_VAL' ${DSIP_CONFIG_FILE} -qb;\")\n\n        DSIP_RELOAD_TYPE=2\n        KAM_RELOAD_TYPE=2\n    fi\n    # NOTE: since the host is required in the DB URI when parsing args we also check if it actually changed to determine if we need to run this logic\n    if [[ -n ${SET_KAM_DB_HOST+set} ]]; then\n        SHELL_CMDS+=(\"setConfigAttrib 'KAM_DB_HOST' '$SET_KAM_DB_HOST' ${DSIP_CONFIG_FILE} -q;\")\n\n        if [[ \"${SET_KAM_DB_HOST}\" != \"${KAM_DB_HOST}\" ]]; then\n            reconfigureMysqlSystemdService\n            MYSQL_RELOAD_TYPE=2\n        fi\n\n        DSIP_RELOAD_TYPE=2\n        KAM_RELOAD_TYPE=2\n    fi\n    if [[ -n ${SET_KAM_DB_PORT+set} ]]; then\n        SHELL_CMDS+=(\"setConfigAttrib 'KAM_DB_PORT' '$SET_KAM_DB_PORT' ${DSIP_CONFIG_FILE} -q;\")\n\n        DSIP_RELOAD_TYPE=2\n        KAM_RELOAD_TYPE=2\n    fi\n    # TODO: allow changing live database name\n    if [[ -n ${SET_KAM_DB_NAME+set} ]]; then\n        SHELL_CMDS+=(\"setConfigAttrib 'KAM_DB_NAME' '$SET_KAM_DB_NAME' ${DSIP_CONFIG_FILE} -q;\")\n\n        DSIP_RELOAD_TYPE=2\n        KAM_RELOAD_TYPE=2\n    fi\n    # we do not support creating new root accounts, therefore we have a few extra checks here\n    if [[ -n ${SET_ROOT_DB_USER+set} ]] && [[ \"$ROOT_DB_USER\" != \"$SET_ROOT_DB_USER\" ]]; then\n        if checkDBUserExists \"${ROOT_DB_USER}@localhost\"; then\n            DEFERRED_SQL_STATEMENTS+=(\"RENAME USER '${ROOT_DB_USER}'@'localhost' TO '${SET_ROOT_DB_USER}'@'localhost';\")\n        fi\n        if checkDBUserExists \"${ROOT_DB_USER}@%\"; then\n            DEFERRED_SQL_STATEMENTS+=(\"RENAME USER '${ROOT_DB_USER}'@'%' TO '${SET_ROOT_DB_USER}'@'%';\")\n        fi\n\n        SHELL_CMDS+=(\"setConfigAttrib 'ROOT_DB_USER' '$SET_ROOT_DB_USER' ${DSIP_CONFIG_FILE} -q;\")\n    fi\n    if [[ -n ${SET_ROOT_DB_PASS+set} ]]; then\n        if [[ -n ${SET_ROOT_DB_USER+set} ]]; then\n            TMP_VAL=\"$SET_ROOT_DB_USER\"\n        else\n            TMP_VAL=\"$ROOT_DB_USER\"\n        fi\n        if [[ \"$MYSQL_VARIANT\" == 'mysql' ]]; then\n            DEFERRED_SQL_STATEMENTS+=(\"ALTER USER '$TMP_VAL'@'localhost' IDENTIFIED BY '$SET_ROOT_DB_PASS';\")\n            if checkDBUserExists \"$TMP_VAL@%\"; then\n                DEFERRED_SQL_STATEMENTS+=(\"ALTER USER '$TMP_VAL'@'%' IDENTIFIED BY '$SET_ROOT_DB_PASS';\")\n            fi\n        else\n            DEFERRED_SQL_STATEMENTS+=(\"SET PASSWORD FOR '$TMP_VAL'@'localhost' = PASSWORD('$SET_ROOT_DB_PASS');\")\n            if checkDBUserExists \"$TMP_VAL@%\"; then\n                DEFERRED_SQL_STATEMENTS+=(\"SET PASSWORD FOR '$TMP_VAL'@'%' = PASSWORD('$SET_ROOT_DB_PASS');\")\n            fi\n        fi\n\n        TMP_VAL=$(encryptCreds -kf \"$DSIP_PRIV_KEY\" \"$SET_ROOT_DB_PASS\")\n        SHELL_CMDS+=(\"setConfigAttrib 'ROOT_DB_PASS' '$TMP_VAL' ${DSIP_CONFIG_FILE} -qb;\")\n    fi\n    if [[ -n ${SET_ROOT_DB_HOST+set} ]]; then\n        SHELL_CMDS+=(\"setConfigAttrib 'ROOT_DB_HOST' '$SET_ROOT_DB_HOST' ${DSIP_CONFIG_FILE} -q;\")\n    fi\n    if [[ -n ${SET_ROOT_DB_PORT+set} ]]; then\n        SHELL_CMDS+=(\"setConfigAttrib 'ROOT_DB_PORT' '$ROOT_KAM_DB_PORT' ${DSIP_CONFIG_FILE} -q;\")\n    fi\n    if [[ -n ${SET_ROOT_DB_NAME+set} ]]; then\n        SHELL_CMDS+=(\"setConfigAttrib 'ROOT_DB_NAME' '$SET_ROOT_DB_NAME' ${DSIP_CONFIG_FILE} -q;\")\n    fi\n    if [[ -n ${SET_DSIP_SESSION_KEY+set} ]]; then\n        TMP_VAL=$(encryptCreds -kf \"$DSIP_PRIV_KEY\" \"$SET_DSIP_SESSION_KEY\")\n        SHELL_CMDS+=(\"setConfigAttrib 'DSIP_SESSION_KEY' '$TMP_VAL' ${DSIP_CONFIG_FILE} -q;\")\n\n        DSIP_RELOAD_TYPE=2\n    fi\n    DEFERRED_SQL_STATEMENTS+=(\"flush privileges;\")\n\n    # allow settings that don't require DB to be running to be updated (we verified at the start of this func whether we needed DB)\n    if (( ${RUN_SQL_STATEMENTS} == 1 )); then\n        # update non-encrypted settings on DB\n        sqlAsTransaction --user=\"$ROOT_DB_USER\" --pass=\"$ROOT_DB_PASS\" --host=\"$ROOT_DB_HOST\" --port=\"$ROOT_DB_PORT\" \"${SQL_STATEMENTS[@]}\"\n        if (( $? != 0 )); then\n            printerr 'Failed setting credentials on DB'\n            exit 1\n        fi\n\n        # update live DB settings (DB user passwords, privileges, etc..)\n        sqlAsTransaction --user=\"$ROOT_DB_USER\" --pass=\"$ROOT_DB_PASS\" --host=\"$ROOT_DB_HOST\" --port=\"$ROOT_DB_PORT\" \"${DEFERRED_SQL_STATEMENTS[@]}\"\n        if (( $? != 0 )); then\n            printerr 'Failed setting credentials on DB'\n            exit 1\n        fi\n    fi\n\n    # finally update the local config files\n    eval \"${SHELL_CMDS[@]}\"\n\n    # export variables for later usage in this script\n    export DSIP_USERNAME=${SET_DSIP_GUI_USER:-$DSIP_USERNAME}\n    export DSIP_PASSWORD=${SET_DSIP_GUI_PASS:-$DSIP_PASSWORD}\n    export DSIP_API_TOKEN=${SET_DSIP_API_TOKEN:-$DSIP_API_TOKEN}\n    export MAIL_USERNAME=${SET_DSIP_MAIL_USER:-$MAIL_USERNAME}\n    export MAIL_PASSWORD=${SET_DSIP_MAIL_PASS:-$MAIL_PASSWORD}\n    export DSIP_IPC_PASS=${SET_DSIP_IPC_TOKEN:-$DSIP_IPC_PASS}\n    export KAM_DB_USER=${SET_KAM_DB_USER:-$KAM_DB_USER}\n    export KAM_DB_PASS=${SET_KAM_DB_PASS:-$KAM_DB_PASS}\n    export KAM_DB_HOST=${SET_KAM_DB_HOST:-$KAM_DB_HOST}\n    export KAM_DB_PORT=${SET_KAM_DB_PORT:-$KAM_DB_PORT}\n    export KAM_DB_NAME=${SET_KAM_DB_NAME:-$KAM_DB_NAME}\n    export ROOT_DB_USER=${SET_ROOT_DB_USER:-$ROOT_DB_USER}\n    export ROOT_DB_PASS=${SET_ROOT_DB_PASS:-$ROOT_DB_PASS}\n    export ROOT_DB_HOST=${SET_ROOT_DB_HOST:-$ROOT_DB_HOST}\n    export ROOT_DB_PORT=${SET_ROOT_DB_PORT:-$ROOT_DB_PORT}\n    export ROOT_DB_NAME=${SET_ROOT_DB_NAME:-$ROOT_DB_NAME}\n\n    # reload/synchronize settings for each service\n    # note: we reload the service only if it is currently running (otherwise it messes with boot ordering)\n    # note: updateKamailioConfig() combines configuring kam config and hot reloading in the same function\n    if (( ${KAM_RELOAD_TYPE} > 0 )); then\n        updateKamailioConfig\n    fi\n    if (( $SERVICE_RELOAD_DISABLED == 0 )); then\n        if (( ${MYSQL_RELOAD_TYPE} == 2 )); then\n            if systemctl is-active --quiet mariadb; then\n                systemctl restart mariadb\n            fi\n        fi\n        if (( ${KAM_RELOAD_TYPE} == 2 )); then\n            if systemctl is-active --quiet kamailio; then\n                systemctl restart kamailio\n            fi\n        fi\n        if (( ${DSIP_RELOAD_TYPE} == 1 )); then\n            # synchronize settings (between local disk, DB, and cluster)\n            systemctl kill -s SIGUSR1 dsiprouter\n        elif (( ${DSIP_RELOAD_TYPE} == 2 )); then\n            if systemctl is-active --quiet dsiprouter; then\n                systemctl restart dsiprouter\n            fi\n        fi\n    fi\n\n    printdbg 'Credentials have been updated'\n}\n\n# update MOTD banner for ssh login\nfunction updateBanner() {\n    # don't write multiple times\n    if [ -f /etc/update-motd.d/00-dsiprouter ]; then\n        return 0\n    fi\n\n    # move old banner files\n    mkdir -p /etc/update-motd.d\n    cp -f /etc/motd ${BACKUPS_DIR}/motd.bak\n    truncate -s 0 /etc/motd\n    chmod -x /etc/update-motd.d/* 2>/dev/null\n\n    # add our custom banner script (dynamically updates MOTD banner)\n    (cat << EOF\n#!/usr/bin/env bash\n\n# redefine variables and functions here\nESC_SEQ=\"$ESC_SEQ\"\nANSI_NONE=\"$ANSI_NONE\"\nANSI_GREEN=\"$ANSI_GREEN\"\nIPV6_ENABLED=${IPV6_ENABLED:-0}\n$(declare -f printdbg)\n$(declare -f getConfigAttrib)\n$(declare -f displayLogo)\n\n# updated variables on login\nINTERNAL_IP_ADDR=\\$(getConfigAttrib 'INTERNAL_IP_ADDR' ${DSIP_CONFIG_FILE})\nEXTERNAL_IP_ADDR=\\$(getConfigAttrib 'EXTERNAL_IP_ADDR' ${DSIP_CONFIG_FILE})\nDSIP_PORT=\\$(getConfigAttrib 'DSIP_PORT' ${DSIP_CONFIG_FILE})\nDSIP_GUI_PROTOCOL=\\$(getConfigAttrib 'DSIP_PROTO' ${DSIP_CONFIG_FILE})\nVERSION=\\$(getConfigAttrib 'VERSION' ${DSIP_CONFIG_FILE})\n\n# displaying information to user\nclear\ndisplayLogo\nprintdbg \"Version: \\$VERSION\"\nprintf '\\n'\nprintdbg \"You can access the dSIPRouter GUI by going to:\"\nprintdbg \"External IP: \\${DSIP_GUI_PROTOCOL}://\\${EXTERNAL_IP_ADDR}:\\${DSIP_PORT}\"\nif [ \"\\$EXTERNAL_IP_ADDR\" != \"\\$INTERNAL_IP_ADDR\" ];then\n    printdbg \"Internal IP: \\${DSIP_GUI_PROTOCOL}://\\${INTERNAL_IP_ADDR}:\\${DSIP_PORT}\"\nfi\nprintf '\\n'\n\nexit 0\nEOF\n    ) > /etc/update-motd.d/00-dsiprouter\n\n    chmod +x /etc/update-motd.d/00-dsiprouter\n\n    # debian-based distro's will update it automatically\n    # for rhel-based distro's we simply update it via cronjob\n    case \"$DISTRO\" in\n        amzn|rhel|almalinux|rocky)\n            /etc/update-motd.d/00-dsiprouter > /etc/motd\n            if ! crontab -u root -l 2>/dev/null | grep -q \"/etc/update-motd.d/00-dsiprouter\" 2>/dev/null; then\n                cronAppend -u root '*/5 * * * *  /etc/update-motd.d/00-dsiprouter >/etc/motd'\n            fi\n            ;;\n    esac\n}\n\n# revert to old MOTD banner for ssh logins\nfunction revertBanner() {\n    mv -f ${BACKUPS_DIR}/motd.bak /etc/motd\n    rm -f /etc/update-motd.d/00-dsiprouter\n    chmod +x /etc/update-motd.d/* 2>/dev/null\n\n    # remove cron entry for rhel-based distros\n    case \"$DISTRO\" in\n        amzn|rhel|almalinux|rocky)\n            cronRemove -u root '/etc/update-motd.d/00-dsiprouter'\n            ;;\n    esac\n}\n\n# =================\n# dsip-init service\n# =================\n#\n# Initially the init service does nothing but startup required services on boot\n#\n# 1. Primary usage is to ensure required services are started for dependent services\n# 2. Secondary usage is to add startup commands to run on reboot (init cmds for services)\n#\n# This service will ensure the following services are started:\n# - networking\n# - syslog\n# - mysql\n# - dnsmasq\n# - nginx\n# TODO: replace this with a systemd target instead (dsip-init.target)\nfunction createInitService() {\n    # imported from dsip_lib.sh\n    local DSIP_INIT_FILE=\"$DSIP_INIT_FILE\"\n\n    # only create if it doesn't exist\n    if [[ -f \"$DSIP_INIT_FILE\" ]]; then\n        printwarn \"dsip-init service already exists\"\n        return 0\n    else\n        printdbg \"creating dsip-init service\"\n    fi\n\n    # configure cloud-init to work with alongside our init services\n    if [[ -n \"$CLOUD_PLATFORM\" ]]; then\n        cp -f ${DSIP_PROJECT_DIR}/cloud/cloud-init/configs/${CLOUD_PLATFORM}.cfg /etc/cloud/cloud.cfg.d/99-dsip-init.cfg\n        cp -f ${DSIP_PROJECT_DIR}/cloud/cloud-init/templates/hosts.${DISTRO}.tmpl $(${DSIP_PROJECT_DIR}/cloud/find_hosts_tmpl.sh)\n\n        # patch for cloud-init.service circular ordering dependency\n        # TODO: commit this upstream to cloud-init project\n        case \"$DISTRO\" in\n            debian|ubuntu)\n                perl -i -pe 's%(Before\\=sysinit\\.target)%#\\1%' /lib/systemd/system/cloud-init.service\n                systemctl daemon-reload\n                ;;\n        esac\n    fi\n\n    case \"$DISTRO\" in\n        amzn|rhel|almalinux|rocky)\n            # TODO: this should be moved to a separate install dir called syslog\n            # alias and link rsyslog to syslog service as in debian\n            # allowing rsyslog to be accessible via syslog namespace\n            # the settings are already there just commented out by default\n            sed -i -r 's|^[;](.*)|\\1|g' /lib/systemd/system/rsyslog.service\n            ln -sf /lib/systemd/system/rsyslog.service /etc/systemd/system/syslog.service\n            systemctl daemon-reload\n            ;;\n        *)\n            ;;\n    esac\n\n    cp -f ${DSIP_PROJECT_DIR}/dsiprouter/dsip-net-cfg.py /usr/sbin/dsip-net-cfg\n    chmod +x /usr/sbin/dsip-net-cfg\n\n    (cat << EOF\n[Unit]\nDescription=dSIPRouter Init Service\nDefaultDependencies=no\nRequires=basic.target network.target\nWants=rsyslog.service mariadb.service dnsmasq.service nginx.service\nAfter=network.target network-online.target systemd-journald.socket basic.target cloud-init.target\nAfter=networking.service systemd-networkd.service NetworkManager.service\nAfter=rsyslog.service mariadb.service dnsmasq.service nginx.service\nBefore=\nReloadPropagatedFrom=networking.service systemd-networkd.service NetworkManager.service\n\n[Service]\nType=oneshot\nExecStart=/usr/sbin/dsip-net-cfg\nRemainAfterExit=true\nTimeoutSec=0\n\n[Install]\nWantedBy=multi-user.target\nWantedBy=networking.service systemd-networkd.service NetworkManager.service\nEOF\n    ) > ${DSIP_INIT_FILE}\n\n    # set default permissions\n    chmod 0644 ${DSIP_INIT_FILE}\n\n    # enable dsip-init service on boot\n    systemctl daemon-reload\n    systemctl enable dsip-init\n}\n\nfunction removeInitService() {\n    # imported from dsip_lib.sh\n    local DSIP_INIT_FILE=\"$DSIP_INIT_FILE\"\n\n    # remove our custom cloud-init configs\n    rm -f /etc/cloud/cloud.cfg.d/99-dsip-init.cfg\n\n    rm -f /usr/sbin/dsip-net-cfg\n\n    systemctl stop dsip-init\n    systemctl disable dsip-init\n    rm -f $DSIP_INIT_FILE\n    systemctl daemon-reload\n\n    printdbg \"dsip-init service removed\"\n}\n\nfunction upgrade() {\n    local UPGRADE_VER CURRENT_VERSION UPGRADE_DEPENDS\n    local REPO_URL=${UPGRADE_REPO:-\"$GIT_REPO_URL\"}\n    REPO_URL=${REPO_URL:-https://github.com/dOpensource/dsiprouter.git}\n    local TAG_NAME=\"${UPGRADE_RELEASE}-rel\"\n    export NEW_PROJECT_DIR=/tmp/dsiprouter\n    export RUNNING_UPGRADE=1\n\n    # make sure mask is reset to be more permissive\n    # repo must be created with permissions set in the remote repo\n    # and we want to keep permissions from backup files as well\n    umask 022\n\n    printdbg 'downloading new dSIPRouter project files'\n    rm -rf \"$NEW_PROJECT_DIR\" 2>/dev/null\n    git clone --depth 1 -c advice.detachedHead=false -b \"$TAG_NAME\" \"$REPO_URL\" \"$NEW_PROJECT_DIR\" || {\n        printerr 'failed downloading new project files'\n        exit 1\n    }\n\n    printdbg 'verifying version requirements'\n    UPGRADE_VER=$(jq -r -e '.version' <\"${NEW_PROJECT_DIR}/resources/upgrade/${UPGRADE_RELEASE}/settings.json\")\n    CURRENT_VERSION=$(getConfigAttrib \"VERSION\" \"${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py\")\n    UPGRADE_DEPENDS=( $(jq -r -e '.depends[]' <\"${NEW_PROJECT_DIR}/resources/upgrade/${UPGRADE_RELEASE}/settings.json\") )\n    (\n        for VER in ${UPGRADE_DEPENDS[@]}; do\n            if [[ \"$CURRENT_VERSION\" == \"$VER\" ]]; then\n                exit 0\n            fi\n        done\n        exit 1\n    ) || {\n        printerr \"unsupported upgrade scenario ($CURRENT_VERSION -> $UPGRADE_VER)\"\n        exit 1\n    }\n\n    if systemctl is-active -q dsiprouter; then\n        # check shared memory\n        if [[ $(${PYTHON_CMD} -c \"\nimport os\nos.chdir('${DSIP_PROJECT_DIR}/gui')\nfrom modules.api.licensemanager.functions import getLicenseStatus\nprint(getLicenseStatus(license_tag='DSIP_CORE'))\n        \") != \"3\" ]]; then\n            printerr 'dSPIRouter core license is not valid'\n            exit 1\n        fi\n    else\n        # manually grab license status\n        if [[ $(${PYTHON_CMD} -c \"\nimport os, sys\nos.chdir('${DSIP_PROJECT_DIR}/gui')\nsys.path.insert(0, '${DSIP_SYSTEM_CONFIG_DIR}/gui')\nfrom modules.api.licensemanager.functions import licenseDictToStateDict, getLicenseStatusFromStateDict\nimport settings\nprint(getLicenseStatusFromStateDict(licenseDictToStateDict(settings.DSIP_LICENSE_STORE), 'DSIP_CORE'))\n        \") != \"3\" ]]; then\n            printerr 'dSPIRouter core license is not valid'\n            exit 1\n        fi\n    fi\n\n    printdbg \"starting migration from $CURRENT_VERSION to $UPGRADE_VER\"\n    ${NEW_PROJECT_DIR}/resources/upgrade/${UPGRADE_RELEASE}/scripts/migrate.sh\n    return $?\n}\n\n# TODO: deprecated code requiring review, marked for review in v0.80\n#    DSIP_CLUSTER_ID=${DSIP_CLUSTER_ID:-$(getConfigAttrib 'DSIP_CLUSTER_ID' ${DSIP_CONFIG_FILE})}\n#\n#    CURRENT_RELEASE=$(getConfigAttrib 'VERSION' ${DSIP_CONFIG_FILE})\n#\n#    # Check if already upgraded\n#    #rel = $((`echo \"$CURRENT_RELEASE\" == \"$UPGRADE_RELEASE\" | bc`))\n#    #if [ $rel -eq 1 ]; then\n#\n#\n#    #    pprint \"dSIPRouter is already updated to $UPGRADE_RELEASE!\"\n#    #    return\n#\n#    #fi\n#\n#    # Return an error if the release doesn't exist\n#   if ! git branch -a --format='%(refname:short)' | grep -qE \"^${UPGRADE_RELEASE}\\$\" 2>/dev/null; then\n#        printdbg \"The $UPGRADE_RELEASE release doesn't exist. Please select another release\"\n#        return 1\n#   fi\n#\n#    BACKUP_DIR=\"/var/backups\"\n#    CURR_BACKUP_DIR=\"${BACKUP_DIR}/$(date '+%Y-%m-%d')\"\n#    mkdir -p ${BACKUP_DIR} ${CURR_BACKUP_DIR}\n#    mkdir -p ${CURR_BACKUP_DIR}/{etc,var/lib,${HOME},$(dirname \"$DSIP_PROJECT_DIR\")}\n#\n#    cp -r ${DSIP_PROJECT_DIR} ${CURR_BACKUP_DIR}/${DSIP_PROJECT_DIR}\n#    cp -r ${SYSTEM_KAMAILIO_CONFIG_DIR} ${CURR_BACKUP_DIR}/${SYSTEM_KAMAILIO_CONFIG_DIR}\n#\n#    #Stash any changes so that GUI will allow us to pull down a new release\n#    #git stash\n#    #git checkout $UPGRADE_RELEASE\n#    #git stash apply\n#\n#    generateKamailioConfig\n#    updateKamailioConfig\n#    updateKamailioStartup\n#\n#    if (( $? == 0 )); then\n#        # Upgrade the version\n#       setConfigAttrib 'VERSION' \"$UPGRADE_RELEASE\" ${DSIP_CONFIG_FILE} -q\n#\n#        # Restart Kamailio\n#        systemctl restart kamailio\n#        systemctl restart dsiprouter\n#    fi\n\n# TODO: this is unfinished\n#function upgradeOld {\n#    # TODO: set / handle parsed args\n#    UPGRADE_RELEASE=\"v0.51\"\n#\n#    BACKUP_DIR=\"/var/backups\"\n#    CURR_BACKUP_DIR=\"${BACKUP_DIR}/$(date '+%Y-%m-%d')\"\n#    mkdir -p ${BACKUP_DIR} ${CURR_BACKUP_DIR}\n#    mkdir -p ${CURR_BACKUP_DIR}/{etc,var/lib,${HOME},$(dirname \"$DSIP_PROJECT_DIR\")}\n#\n#    # TODO: more cross platform / cloud RDBMS friendly dump, such as the following:\n##    VIEWS=$(mysql --skip-column-names --batch -D information_schema -e 'select table_name from tables where table_schema=\"kamailio\" and table_type=\"VIEW\"' | perl -0777 -pe 's/\\n(?!\\Z)/|/g')\n##    mysqldump -B kamailio --routines --triggers --hex-blob | sed -e 's|DEFINER=`[a-z0-9A-Z]*`@`[a-z0-9A-Z]*`||g' | perl -0777 -pe 's|(CREATE TABLE `?(?:'\"${VIEWS}\"')`?.*?)ENGINE=\\w+|\\1|sgm' > kamdump.sql\n#\n#    mysqldump --single-transaction --opt --events --routines --triggers --all-databases --add-drop-database --flush-privileges \\\n#        --user=\"$ROOT_DB_USER\" --password=\"$ROOT_DB_PASS\" --host=\"$ROOT_DB_HOST\" --port=\"$ROOT_DB_PORT\" > ${CURR_BACKUP_DIR}/mysql_full.sql\n#    mysqldump --single-transaction --skip-triggers --skip-add-drop-table --insert-ignore \\\n#        --user=\"$ROOT_DB_USER\" --password=\"$ROOT_DB_PASS\" --host=\"$ROOT_DB_HOST\" --port=\"$ROOT_DB_PORT\" ${KAM_DB_NAME} \\\n#        | perl -0777 -pi -e 's/CREATE TABLE (`(.+?)`.+?;)/CREATE TABLE IF NOT EXISTS \\1\\n\\nTRUNCATE TABLE `\\2`;\\n/gs' \\\n#        > ${CURR_BACKUP_DIR}/kamdb_merge.sql\n#\n#    systemctl stop rtpengine\n#    systemctl stop kamailio\n#    systemctl stop dsiprouter\n#    systemctl stop mariadb\n#\n#    mv -f ${DSIP_PROJECT_DIR} ${CURR_BACKUP_DIR}/${DSIP_PROJECT_DIR}\n#    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR} ${CURR_BACKUP_DIR}/${SYSTEM_KAMAILIO_CONFIG_DIR}\n#    # in case mysqldumps failed silently, backup mysql binary data\n#    mv -f /var/lib/mysql ${CURR_BACKUP_DIR}/var/lib/\n#    cp -f /etc/my.cnf* ${CURR_BACKUP_DIR}/etc/\n#    cp -rf /etc/my.cnf* ${CURR_BACKUP_DIR}/etc/\n#    cp -rf /etc/mysql ${CURR_BACKUP_DIR}/etc/\n#    cp -f ${HOME}/.my.cnf* ${CURR_BACKUP_DIR}/${HOME}/\n#\n#    iptables-save > ${CURR_BACKUP_DIR}/iptables.dump\n#    ip6tables-save > ${CURR_BACKUP_DIR}/ip6tables.dump\n#\n#    git clone https://github.com/dOpensource/dsiprouter.git --branch=\"$UPGRADE_RELEASE\" ${DSIP_PROJECT_DIR}\n#    cd ${DSIP_PROJECT_DIR}\n#\n#    # TODO: figure out what settings they installed with previously\n#    # or we can simply store them in a text file (./installed)\n#    # after a succesfull install completes\n#    ./dsiprouter.sh uninstall\n#    ./dsiprouter.sh install\n#\n#    mysql --user=\"$ROOT_DB_USER\" --password=\"$ROOT_DB_PASS\" --host=\"$ROOT_DB_HOST\" --port=\"$ROOT_DB_PORT\" ${KAM_DB_NAME} < ${CURR_BACKUP_DIR}/kamdb_merge.sql\n#\n#    # TODO: fix any conflicts that would arise from our new modules / tables in KAMDB\n#\n#    # TODO: print backup location info to user\n#\n#    # TODO: transfer / merge backup configs to new configs\n#    # kam configs\n#    # dsip configs\n#    # iptables configs\n#    # mysql configs\n#\n#    # TODO: restart services, check for good startup\n#}\n\n# TODO: add bash cmd completion for new options provided by gitwrapper.sh\n# TODO: move installing of testing dependencies here\nfunction configGitDevEnv() {\n    ${PYTHON_CMD} -m pip install pipreqs\n\n    mkdir -p ${BACKUPS_DIR}/git/info ${BACKUPS_DIR}/git/hooks\n    mkdir -p ${DSIP_PROJECT_DIR}/.git/info ${DSIP_PROJECT_DIR}/.git/hooks\n\n    cp -f ${DSIP_PROJECT_DIR}/.git/info/attributes ${BACKUPS_DIR}/git/info/attributes 2>/dev/null\n    cat ${DSIP_PROJECT_DIR}/resources/git/gitattributes >> ${DSIP_PROJECT_DIR}/.git/info/attributes\n\n    cp -f ${DSIP_PROJECT_DIR}/.git/config ${BACKUPS_DIR}/git/config 2>/dev/null\n    cat ${DSIP_PROJECT_DIR}/resources/git/gitconfig >> ${DSIP_PROJECT_DIR}/.git/config\n\n    cp -f ${DSIP_PROJECT_DIR}/.git/info/exclude ${BACKUPS_DIR}/git/info/exclude 2>/dev/null\n    cp -f ${DSIP_PROJECT_DIR}/resources/git/gitignore ${DSIP_PROJECT_DIR}/.git/info/exclude\n\n    cp -f ${DSIP_PROJECT_DIR}/.git/commit-msg ${BACKUPS_DIR}/git/commit-msg 2>/dev/null\n    cp -f ${DSIP_PROJECT_DIR}/resources/git/commit-msg ${DSIP_PROJECT_DIR}/.git/commit-msg\n\n    cp -f ${DSIP_PROJECT_DIR}/.git/hooks/pre-commit ${BACKUPS_DIR}/git/hooks/pre-commit 2>/dev/null\n    cp -f ${DSIP_PROJECT_DIR}/resources/git/hooks/pre-commit ${DSIP_PROJECT_DIR}/.git/hooks/pre-commit\n    chmod +x ${DSIP_PROJECT_DIR}/.git/hooks/pre-commit\n\n    cp -f ${DSIP_PROJECT_DIR}/.git/hooks/prepare-commit-msg ${BACKUPS_DIR}/git/hooks/prepare-commit-msg 2>/dev/null\n    cp -f ${DSIP_PROJECT_DIR}/resources/git/hooks/prepare-commit-msg ${DSIP_PROJECT_DIR}/.git/hooks/prepare-commit-msg\n    chmod +x ${DSIP_PROJECT_DIR}/.git/hooks/prepare-commit-msg\n\n    cp -f ${DSIP_PROJECT_DIR}/.git/hooks/commit-msg ${BACKUPS_DIR}/git/hooks/commit-msg 2>/dev/null\n    cp -f ${DSIP_PROJECT_DIR}/resources/git/hooks/commit-msg ${DSIP_PROJECT_DIR}/.git/hooks/commit-msg\n    chmod +x ${DSIP_PROJECT_DIR}/.git/hooks/commit-msg\n\n    cp -f ${DSIP_PROJECT_DIR}/.git/hooks/post-commit ${BACKUPS_DIR}/git/hooks/post-commit 2>/dev/null\n    cp -f ${DSIP_PROJECT_DIR}/resources/git/hooks/post-commit ${DSIP_PROJECT_DIR}/.git/hooks/post-commit\n    chmod +x ${DSIP_PROJECT_DIR}/.git/hooks/post-commit\n\n    cp -f ${DSIP_PROJECT_DIR}/.git/hooks/pre-push ${BACKUPS_DIR}/git/hooks/pre-push 2>/dev/null\n    cp -f ${DSIP_PROJECT_DIR}/resources/git/hooks/pre-push ${DSIP_PROJECT_DIR}/.git/hooks/pre-push\n    chmod +x ${DSIP_PROJECT_DIR}/.git/hooks/pre-push\n\n    cp -f ${DSIP_PROJECT_DIR}/resources/git/merge-changelog.sh /usr/local/bin/_merge-changelog\n    chmod +x /usr/local/bin/_merge-changelog\n\n    cp -f ${DSIP_PROJECT_DIR}/resources/git/check_syntax.py /usr/local/bin/_git_check_syntax\n    chmod +x /usr/local/bin/_git_check_syntax\n\n    cp -f ${DSIP_PROJECT_DIR}/resources/git/gitwrapper.sh ${GIT_UPDATE_FILE}\n    . ${GIT_UPDATE_FILE}\n}\n\nfunction cleanGitDevEnv() {\n    mv -f ${BACKUPS_DIR}/git/info/attributes ${DSIP_PROJECT_DIR}/.git/info/attributes 2>/dev/null\n    mv -f ${BACKUPS_DIR}/git/config ${DSIP_PROJECT_DIR}/.git/config 2>/dev/null\n    mv -f ${BACKUPS_DIR}/git/info/exclude ${DSIP_PROJECT_DIR}/.git/info/exclude 2>/dev/null\n    mv -f ${BACKUPS_DIR}/git/commit-msg ${DSIP_PROJECT_DIR}/.git/commit-msg 2>/dev/null\n    mv -f ${BACKUPS_DIR}/git/hooks/pre-commit ${DSIP_PROJECT_DIR}/.git/hooks/pre-commit 2>/dev/null\n    mv -f ${BACKUPS_DIR}/git/hooks/prepare-commit-msg ${DSIP_PROJECT_DIR}/.git/hooks/prepare-commit-msg 2>/dev/null\n    mv -f ${BACKUPS_DIR}/git/hooks/commit-msg ${DSIP_PROJECT_DIR}/.git/hooks/commit-msg 2>/dev/null\n    mv -f ${BACKUPS_DIR}/git/hooks/post-commit ${DSIP_PROJECT_DIR}/.git/hooks/post-commit 2>/dev/null\n    mv -f ${BACKUPS_DIR}/git/hooks/pre-push ${DSIP_PROJECT_DIR}/.git/hooks/pre-push 2>/dev/null\n    rm -f /usr/local/bin/_merge-changelog\n    rm -f /usr/local/bin/_git_check_syntax\n    rm -f ${GIT_UPDATE_FILE}\n}\n\n# run install commands across a cluster of nodes\n# TODO: parallel ssh execution for install cmds\n# TODO: need to handle re-attempt better, when one node fails and others did not\n#       we could overwrite key, attempt install (will pass on already installed configs), re-encrypt\n#       this would require some way of knowing whether the credentials changed\n#       or we could check for install and decrypt/store creds before replacing key and re-encrypting\n# TODO: add support for calling various cluster scripts in HA directory\n# TODO: on new cluster install the 2nd / tertiary DBs don't exist when updating dsip_settings, so the credentials don't match\n#       kamailio db settings should not be synced in cluster sync mode (since config settings are stored there it would break the cluster)\n# TODO: handle re-running cluster install after failure (must reset GUI pass and probably should reset configs)\nfunction clusterInstall() { (\n    local i j\n    local USER PASS HOST PORT SSH_REMOTE_HOST\n    local CLUSTER_GUI_USER CLUSTER_GUI_PASS CLUSTER_API_TOKEN CLUSTER_MAIL_USER CLUSTER_MAIL_PASS CLUSTER_IPC_TOKEN\n    local CLUSTER_KAM_DB_USER CLUSTER_KAM_DB_PASS CLUSTER_KAM_DB_NAME CLUSTER_ROOT_DB_USER CLUSTER_ROOT_DB_PASS CLUSTER_ROOT_DB_NAME\n    local SSH_CMD=() RSYNC_CMD=()\n    local TMP_PRIV_KEY=\"/tmp/dsip_privkey\"\n    local CLUSTER_SYNC=0\n    # default ssh options\n    local SSH_OPTS=(-o StrictHostKeyChecking=no -o CheckHostIp=no -o UserKnownHostsFile=/dev/null -o ServerAliveInterval=5 -o ServerAliveCountMax=2 -x)\n    local RSYNC_OPTS=()\n    # allow local project to be located anywhere\n    local LOCAL_PROJECT_DIR=\"$DSIP_PROJECT_DIR\"\n    DSIP_PROJECT_DIR=\"/opt/dsiprouter\"\n\n    if ! cmdExists 'ssh' || ! cmdExists 'rsync' || ! cmdExists 'sshpass'; then\n        printdbg 'Installing local requirements for cluster install'\n        if cmdExists 'apt-get'; then\n            sudo apt-get install -y openssh-client sshpass rsync\n        elif cmdExists 'dnf'; then\n            sudo dnf install --enablerepo=epel -y openssh-clients sshpass rsync\n        elif cmdExists 'yum'; then\n            sudo yum install --enablerepo=epel -y openssh-clients sshpass rsync\n        else\n            printerr \"Your local OS is currently not supported\"\n            exit 1\n        fi\n    fi\n\n    # sanity check\n    if (( $? != 0 )); then\n        printerr 'Could not install requirements for cluster install'\n        exit 1\n    fi\n\n    # we need to know if cluster sync will be enabled beforehand\n    j=0\n    while (( $j < ${#SSH_SYNC_ARGS[@]} )); do\n        case \"${SSH_SYNC_ARGS[$j]}\" in\n            -dsipcsync|--dsip-clustersync=*)\n                if grep -q '=' 2>/dev/null <<<\"${SSH_SYNC_ARGS[$j]}\"; then\n                    CLUSTER_SYNC=$(cut -d '=' -f 2 <<<\"${SSH_SYNC_ARGS[$j]}\")\n                else\n                    CLUSTER_SYNC=\"${SSH_SYNC_ARGS[$((j + 1))]}\"\n                fi\n                break\n                ;;\n        esac\n        j=$((j + 1))\n    done\n\n    # if installing in cluster sync mode GUI pass must generate it beforehand (can't undo the hash later)\n    # can still be overwritten by user provided args (probably not wise though)\n    if (( $CLUSTER_SYNC == 1 )); then\n        CLUSTER_GUI_PASS=$(urandomChars 64)\n    fi\n\n    # create private key if not set on cmdline\n    if [[ -n \"${SET_DSIP_PRIV_KEY}\" ]]; then\n        printf '%s' \"${SET_DSIP_PRIV_KEY}\" > ${TMP_PRIV_KEY}\n        unset SET_DSIP_PRIV_KEY\n    else\n        dd bs=1 count=32 if=/dev/urandom of=${TMP_PRIV_KEY} 2>/dev/null\n    fi\n    # protect it until destroyed\n    chmod 0400 ${TMP_PRIV_KEY}\n\n    # guarantee key will be destroyed when subshell exits\n    cleanupHandler() {\n        rm -f ${TMP_PRIV_KEY}\n        trap - EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n    }\n    trap 'cleanupHandler $?' EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n\n    # loop through nodes to:\n    #  - validate conn\n    #  - validate unattended ssh\n    #  - collect ssh/scp cmds and pwds\n    i=0\n    while (( $i < ${#SSH_SYNC_NODES[@]} )); do\n        # parse node info\n        USER=$(printf '%s' \"${SSH_SYNC_NODES[$i]}\" | cut -s -d '@' -f -1 | cut -d ':' -f -1)\n        PASS=$(printf '%s' \"${SSH_SYNC_NODES[$i]}\" | cut -s -d '@' -f -1 | cut -s -d ':' -f 2-)\n        HOST=$(printf '%s' \"${SSH_SYNC_NODES[$i]}\" | cut -d '@' -f 2- | cut -d ':' -f -1)\n        PORT=$(printf '%s' \"${SSH_SYNC_NODES[$i]}\" | cut -d '@' -f 2- | cut -s -d ':' -f 2-)\n\n        # default user is root for ssh\n        USER=${USER:-root}\n        # default port is 22 for ssh\n        PORT=${PORT:-22}\n        # host is required per node\n        if [[ -z \"$HOST\" ]]; then\n            printerr \"Node [${SSH_SYNC_NODES[$i]}] does not contain a host\"\n            usageOptions\n            exit 1\n        fi\n        SSH_REMOTE_HOST=\"${USER}@${HOST}\"\n\n        # select auth method and set vars accordingly\n        if [[ -n \"$PASS\" ]]; then\n            export SSHPASS=\"${PASS}\"\n            SSH_CMD=(sshpass -e ssh)\n            RSYNC_CMD=(sshpass -e rsync)\n            SSH_OPTS+=(-o PreferredAuthentications=password)\n        else\n            SSH_CMD=(ssh)\n            RSYNC_CMD=(rsync)\n            if [[ -n \"$SSH_KEY_FILE\" ]]; then\n                SSH_OPTS+=(-o PreferredAuthentications=publickey -i $SSH_KEY_FILE)\n            else\n                SSH_OPTS+=(-o PreferredAuthentications=publickey)\n            fi\n        fi\n\n        # finalize options\n        RSYNC_OPTS+=(--port=${PORT} -z --exclude=\".*\")\n        SSH_OPTS+=(-p ${PORT})\n\n#        printdbg \"Validating tcp connection to ${HOST}\"\n#        if ! checkConn ${HOST} ${PORT}; then\n#            printerr \"Could not establish connection to host [${HOST}] on port [${PORT}]\"\n#            exit 1\n#        fi\n\n        printdbg \"Validating unattended ssh connection to ${HOST}\"\n        if ! checkSSH ${SSH_CMD[@]} ${SSH_OPTS[@]} ${SSH_REMOTE_HOST}; then\n            printerr \"Could not establish unattended ssh connection to [${SSH_REMOTE_HOST}] on port [${PORT}]\"\n            exit 1\n        fi\n\n        printdbg \"Installing remote requirements for cluster install\"\n        ${SSH_CMD[@]} ${SSH_OPTS[@]} ${SSH_REMOTE_HOST} bash 2>&1 <<- EOSSH\n            $(typeset -f cmdExists)\n\n            if cmdExists 'apt-get'; then\n                apt-get install -y rsync\n            elif cmdExists 'dnf'; then\n                dnf install -y rsync\n            elif cmdExists 'yum'; then\n                yum install -y rsync\n            else\n                exit 1\n            fi\n            exit 0\nEOSSH\n\n        if (( $? != 0 )); then\n            printerr \"Failed installing requirements on remote node ${HOST_LIST[$i]}\"\n            exit 1\n        fi\n\n        printdbg \"Starting remote install on ${HOST}\"\n        # password used by ssh/scp\n        if [[ -n \"$PASS\" ]]; then\n            export SSHPASS=\"$PASS\"\n        fi\n\n        printdbg \"Copying project files to ${HOST}\"\n        ${RSYNC_CMD[@]} ${RSYNC_OPTS[@]} --rsh=\"ssh ${SSH_OPTS[*]} -o IPQoS=throughput\" -a ${LOCAL_PROJECT_DIR}/ ${SSH_REMOTE_HOST}:/tmp/dsiprouter/ 2>&1 &&\n        ${RSYNC_CMD[@]} ${RSYNC_OPTS[@]} --rsh=\"ssh ${SSH_OPTS[*]} -o IPQoS=throughput\" ${TMP_PRIV_KEY} ${SSH_REMOTE_HOST}:${TMP_PRIV_KEY} 2>&1\n        if (( $? != 0 )); then\n            printerr \"Copying files to ${HOST} failed\"\n            exit 1\n        fi\n\n        printdbg \"Running remote install on ${HOST}\"\n        ${SSH_CMD[@]} ${SSH_OPTS[@]} ${SSH_REMOTE_HOST} bash 2>&1 <<- EOSSH\n            # debug the remote commands\n            if (( $DEBUG == 1 )); then\n                set -x\n            fi\n\n            # setting up project files on node\n            mkdir -p ${DSIP_PROJECT_DIR}\n            cp -af /tmp/dsiprouter/. ${DSIP_PROJECT_DIR}/\n            rm -rf /tmp/dsiprouter\n\n            if [ ! -f \"${DSIP_SYSTEM_CONFIG_DIR}/.clusterinstallcomplete\" ]; then\n                # setup cluster private key on node\n                mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}\n                mv -f ${TMP_PRIV_KEY} ${DSIP_PRIV_KEY}\n                chown root:root ${DSIP_PRIV_KEY}\n                chmod 0400 ${DSIP_PRIV_KEY}\n\n                # export any settings we wish to override in the install script\n                # these settings are usually set after the first node (in cluster sync mode) is done installing\n                [[ -n \"$CLUSTER_GUI_USER\" ]] && export SET_DSIP_GUI_USER=\"$CLUSTER_GUI_USER\"\n                [[ -n \"$CLUSTER_GUI_PASS\" ]] && export SET_DSIP_GUI_PASS=\"$CLUSTER_GUI_PASS\"\n                [[ -n \"$CLUSTER_API_TOKEN\" ]] && export SET_DSIP_API_TOKEN=\"$CLUSTER_API_TOKEN\"\n                [[ -n \"$CLUSTER_MAIL_USER\" ]] && export SET_DSIP_MAIL_USER=\"$CLUSTER_MAIL_USER\"\n                [[ -n \"$CLUSTER_MAIL_PASS\" ]] && export SET_DSIP_MAIL_PASS=\"$CLUSTER_MAIL_PASS\"\n                [[ -n \"$CLUSTER_IPC_TOKEN\" ]] && export SET_DSIP_IPC_TOKEN=\"$CLUSTER_IPC_TOKEN\"\n                [[ -n \"$CLUSTER_KAM_DB_USER\" ]] && export SET_KAM_DB_USER=\"$CLUSTER_KAM_DB_USER\"\n                [[ -n \"$CLUSTER_KAM_DB_PASS\" ]] && export SET_KAM_DB_PASS=\"$CLUSTER_KAM_DB_PASS\"\n                [[ -n \"$CLUSTER_KAM_DB_NAME\" ]] && export SET_KAM_DB_NAME=\"$CLUSTER_KAM_DB_NAME\"\n                [[ -n \"$CLUSTER_ROOT_DB_USER\" ]] && export SET_ROOT_DB_USER=\"$CLUSTER_ROOT_DB_USER\"\n                [[ -n \"$CLUSTER_ROOT_DB_PASS\" ]] && export SET_ROOT_DB_PASS=\"$CLUSTER_ROOT_DB_PASS\"\n                [[ -n \"$CLUSTER_ROOT_DB_NAME\" ]] && export SET_ROOT_DB_NAME=\"$CLUSTER_ROOT_DB_NAME\"\n\n                # run script command\n                ${DSIP_PROJECT_DIR}/dsiprouter.sh install -dns ${SSH_SYNC_ARGS[@]}\n\n                # create indicator in case we iterate over this node again (if this command is re-run)\n                RETVAL=\\$?\n                (( \\$RETVAL == 0 )) && touch ${DSIP_SYSTEM_CONFIG_DIR}/.clusterinstallcomplete\n                exit \\$RETVAL\n            else\n                # if this node was already configured, reconfigure the encrypted credentials to use the new private key\n                source ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\n                export SET_DSIP_API_TOKEN=\\${SET_DSIP_API_TOKEN:-\\$(decryptConfigAttrib DSIP_API_TOKEN ${DSIP_CONFIG_FILE})}\n                export SET_DSIP_MAIL_PASS=\\${SET_DSIP_MAIL_PASS:-\\$(decryptConfigAttrib MAIL_PASSWORD ${DSIP_CONFIG_FILE})}\n                export SET_DSIP_IPC_TOKEN=\\${SET_DSIP_IPC_TOKEN:-\\$(decryptConfigAttrib DSIP_IPC_PASS ${DSIP_CONFIG_FILE})}\n                export SET_KAM_DB_PASS=\\${SET_KAM_DB_PASS:-\\$(decryptConfigAttrib KAM_DB_PASS ${DSIP_CONFIG_FILE})}\n                export SET_ROOT_DB_PASS=\\${SET_ROOT_DB_PASS:-\\$(decryptConfigAttrib ROOT_DB_PASS ${DSIP_CONFIG_FILE})}\n\n                # setup cluster private key on node\n                mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}\n                mv -f ${TMP_PRIV_KEY} ${DSIP_PRIV_KEY}\n                chown root:root ${DSIP_PRIV_KEY}\n                chmod 0400 ${DSIP_PRIV_KEY}\n\n                dsiprouter setcredentials\n            fi\nEOSSH\n\n        # sanity check, was the install script successful?\n        if (( $? != 0 )); then\n            printerr \"Remote install on ${HOST} failed (install script failed)\"\n            exit 1\n        fi\n\n        # if installing in cluster sync mode reuse the credentials set on the first node\n        # can still be overwritten by user provided args (probably not wise though)\n        if (( $i == 0 )) && (( $CLUSTER_SYNC == 1 )); then\n            . <(\n                ${SSH_CMD[@]} ${SSH_OPTS[@]} ${SSH_REMOTE_HOST} bash 2>/dev/null <<- EOSSH\n                    DSIP_PROJECT_DIR=\"$DSIP_PROJECT_DIR\"\n                    DSIP_SYSTEM_CONFIG_DIR=\"$DSIP_SYSTEM_CONFIG_DIR\"\n                    $(typeset -f getConfigAttrib)\n                    $(typeset -f decryptConfigAttrib)\n                    echo \"CLUSTER_GUI_USER='\\$(getConfigAttrib DSIP_USERNAME ${DSIP_CONFIG_FILE})'\"\n                    echo \"CLUSTER_API_TOKEN='\\$(decryptConfigAttrib DSIP_API_TOKEN ${DSIP_CONFIG_FILE})'\"\n                    echo \"CLUSTER_MAIL_USER='\\$(getConfigAttrib MAIL_USERNAME ${DSIP_CONFIG_FILE})'\"\n                    echo \"CLUSTER_MAIL_PASS='\\$(decryptConfigAttrib MAIL_PASSWORD ${DSIP_CONFIG_FILE})'\"\n                    echo \"CLUSTER_IPC_TOKEN='\\$(decryptConfigAttrib DSIP_IPC_PASS ${DSIP_CONFIG_FILE})'\"\n                    echo \"CLUSTER_KAM_DB_USER='\\$(getConfigAttrib KAM_DB_USER ${DSIP_CONFIG_FILE})'\"\n                    echo \"CLUSTER_KAM_DB_PASS='\\$(decryptConfigAttrib KAM_DB_PASS ${DSIP_CONFIG_FILE})'\"\n                    echo \"CLUSTER_KAM_DB_NAME='\\$(getConfigAttrib KAM_DB_NAME ${DSIP_CONFIG_FILE})'\"\n                    echo \"CLUSTER_ROOT_DB_USER='\\$(getConfigAttrib ROOT_DB_USER ${DSIP_CONFIG_FILE})'\"\n                    echo \"CLUSTER_ROOT_DB_PASS='\\$(decryptConfigAttrib ROOT_DB_PASS ${DSIP_CONFIG_FILE})'\"\n                    echo \"CLUSTER_ROOT_DB_NAME='\\$(getConfigAttrib ROOT_DB_NAME ${DSIP_CONFIG_FILE})'\"\nEOSSH\n            )\n\n            # sanity check, were we able to get the settings from the remote node?\n            if [[ -z \"$CLUSTER_GUI_USER$CLUSTER_API_TOKEN\" ]]; then\n                printerr \"Remote install on ${HOST} failed (could not get cluster credentials)\"\n                exit 1\n            fi\n        fi\n\n        i=$((i + 1))\n    done\n); exit $?; }\n\n# $@ == subset of permissions to update\n# TODO: update systemd ExecStartPre commands to use this logic instead\nfunction updatePermissions() {\n    local OPT=\"\"\n\n    # set permissions on the X509 certs used by dsiprouter and kamailio\n    # [special use case]: testing kamailio service startup\n    # in this case kamailio needs access before dsiprouter user is created\n    setCertPerms() {\n        if id -u dsiprouter &>/dev/null; then\n            # dsiprouter needs to have control over the certs to allow changes\n            # note that nginx should never have write access\n            chown -R dsiprouter:kamailio ${DSIP_CERTS_DIR}\n        else\n            # dsiprouter user does not yet exist so make sure kamailio user has access\n            chown -R root:kamailio ${DSIP_CERTS_DIR}\n        fi\n        find ${DSIP_CERTS_DIR}/ -type f -exec chmod 640 {} +\n    }\n    # set permissions for files/dirs used by dnsmasq\n    setDnsmasqPerms() {\n        mkdir -p /run/dnsmasq\n        chown -R dnsmasq:dnsmasq /run/dnsmasq\n        chmod 770 /run/dnsmasq\n    }\n    # set permissions for files/dirs used by nginx\n    setNginxPerms() {\n        mkdir -p /run/nginx\n        chown -R nginx:nginx /run/nginx\n        chmod 770 /run/nginx\n\n        # dsiprouter needs to be able to dynamically update the provisioning site\n        chown root:dsiprouter /etc/nginx/sites-enabled/\n        chmod 775 /etc/nginx/sites-enabled/\n    }\n    # set permissions for files/dirs used by kamailio\n    setKamailioPerms() {\n        mkdir -p /run/kamailio\n        chown -R kamailio:kamailio /run/kamailio\n        chmod 770 /run/kamailio\n\n        # dsiprouter needs to have control over the kamailio dir\n        # this allows dsiprouter to update kamailio dynamically\n        # kamailio configs will contain plaintext passwords / tokens\n        # in the case where the dsiprouter user does not yet exist we set stricter permissions\n        if id -u dsiprouter &>/dev/null; then\n            chown -R dsiprouter:kamailio ${DSIP_SYSTEM_CONFIG_DIR}/kamailio/\n        else\n            chown -R root:kamailio ${DSIP_SYSTEM_CONFIG_DIR}/kamailio/\n        fi\n        find ${DSIP_SYSTEM_CONFIG_DIR}/kamailio/ -type f -exec chmod 640 {} +\n    }\n    # set permissions for files/dirs used by dsiprouter\n    setDsiprouterPerms() {\n        mkdir -p ${DSIP_RUN_DIR}\n        chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n        chmod 770 ${DSIP_RUN_DIR}\n\n        # dsiprouter user is the only one making backups\n        chown -R dsiprouter:root ${BACKUPS_DIR}\n        # dsiprouter private key only readable by dsiprouter\n        chown dsiprouter:root ${DSIP_PRIV_KEY}\n        chmod 400 ${DSIP_PRIV_KEY}\n        # dsiprouter gui files readable and writable only by dsiprouter\n        chown -R dsiprouter:root ${DSIP_SYSTEM_CONFIG_DIR}/gui/\n        find ${DSIP_SYSTEM_CONFIG_DIR}/gui/ -type f -exec chmod 600 {} +\n\n        # project files can only be edited by root\n        chown -R root:root ${DSIP_PROJECT_DIR}/\n        # files that should be executable\n        chmod +x ${DSIP_PROJECT_DIR}/dsiprouter.sh\n        chmod +x ${DSIP_PROJECT_DIR}/resources/upgrade/*/scripts/migrate.sh\n    }\n    # set permissions for files/dirs used by rtpengine\n    setRtpenginePerms() {\n        mkdir -p /run/rtpengine\n        chown -R rtpengine:rtpengine /run/rtpengine\n        chmod 770 /run/rtpengine\n\n        if id -u dsiprouter &>/dev/null; then\n            chown -R dsiprouter:rtpengine ${DSIP_SYSTEM_CONFIG_DIR}/rtpengine/\n        else\n            chown -R root:rtpengine ${DSIP_SYSTEM_CONFIG_DIR}/rtpengine/\n        fi\n        find ${DSIP_SYSTEM_CONFIG_DIR}/rtpengine/ -type f -exec chmod 640 {} +\n    }\n\n    # no args given set permissions for all services\n    if (( $# == 0 )); then\n        if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dnsmasqinstalled\" ]; then\n            setDnsmasqPerms\n        fi\n        if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.nginxinstalled\" ]; then\n            setNginxPerms\n        fi\n        if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]; then\n            setKamailioPerms\n        fi\n        if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]; then\n            setDsiprouterPerms\n        fi\n        if [ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\" ]; then\n            setRtpenginePerms\n        fi\n        setCertPerms\n        return 0\n    fi\n\n    # parse args and select subset of permissions to set\n    while (( $# > 0 )); do\n        OPT=\"$1\"\n        shift\n        case \"$OPT\" in\n            -certs)\n                setCertPerms\n                ;;\n            -dnsmasq)\n                setDnsmasqPerms\n                ;;\n            -nginx)\n                setNginxPerms\n                ;;\n            -kamailio)\n                setKamailioPerms\n                ;;\n            -dsiprouter)\n                setDsiprouterPerms\n                ;;\n            -rtpengine)\n                setRtpenginePerms\n                ;;\n            *)\n                printerr \"$0(): Invalid argument [$ARG]\"\n                return 1\n                ;;\n        esac\n    done\n\n    return 0\n}\nexport -f updatePermissions\n\n# really only useful on systems with limited RAM (where we usually test)\nfunction createSwapFile() {\n    local SWAP_FILE=\"${DSIP_LIB_DIR}/swap\"\n\n    if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.memupdatescomplete\" ]]; then\n        return 0\n    fi\n\n    # only create if system has less than 2GB RAM and no existing swap files\n    if (( $(awk '/^MemTotal/ {print int($2/1024/1024)}' /proc/meminfo) < 2 )) && [[ -z \"$(swapon --show=SIZE --noheadings)\" ]]; then\n        printdbg 'memory constraints require swapfile, creating now..'\n\n        # 2GB of swap space\n        dd if=/dev/zero of=${SWAP_FILE} bs=64M count=32 &&\n        chmod 600 ${SWAP_FILE} &&\n        mkswap ${SWAP_FILE} &&\n        swapon ${SWAP_FILE} &&\n        (\n            grep -vF \"$SWAP_FILE\" /etc/fstab\n            echo \"${SWAP_FILE} none swap sw 0 0\"\n        ) >/tmp/fstab &&\n        mv -f /tmp/fstab /etc/fstab &&\n        printdbg 'swapfile created successfully' || {\n            printerr 'failed creating swap file'\n            exit 1\n        }\n    fi\n\n    touch \"${DSIP_SYSTEM_CONFIG_DIR}/.memupdatescomplete\"\n}\n\nfunction removeSwapFile() {\n    local SWAP_FILE=\"${DSIP_LIB_DIR}/swap\"\n\n    if [[ ! -f \"${DSIP_SYSTEM_CONFIG_DIR}/.memupdatescomplete\" ]]; then\n        return 0\n    fi\n    if [[ ! -e \"$SWAP_FILE\" ]]; then\n        return 0\n    fi\n\n    swapoff ${SWAP_FILE} &&\n    sed -i \"\\%^${SWAP_FILE}%d\" /etc/fstab &&\n    printdbg 'swapfile removed' || {\n        printerr 'failed removing swap file'\n        exit 1\n    }\n\n    rm -f \"${DSIP_SYSTEM_CONFIG_DIR}/.memupdatescomplete\"\n}\n\nfunction licenseManager() {\n    ${PYTHON_CMD} ${DSIP_PROJECT_DIR}/gui/modules/api/licensemanager/cli.py \"$@\"\n    return $?\n}\n\nfunction usageOptions() {\n    linebreak() {\n        printf '_%.0s' $(seq 1 ${COLUMNS:-100}) && echo ''\n    }\n\n    linebreak\n    printf '\\n%s\\n%s\\n' \\\n        \"$(pprint -n USAGE:)\" \\\n        \"dsiprouter <command> [options]\"\n\n    linebreak\n    printf \"\\n%-s%24s%s\\n\" \\\n        \"$(pprint -n COMMAND)\" \" \" \"$(pprint -n OPTIONS)\"\n    printf \"%-30s %s\\n%-30s %s\\n%-30s %s\\n%-30s %s\\n%-30s %s\\n\" \\\n        \"install\" \"[-debug|-all|--all|-kam|--kamailio|-dsip|--dsiprouter|-rtp|--rtpengine|-dns|--dnsmasq\" \\\n        \" \" \"-dmz <pub iface>,<priv iface>|--dmz=<pub iface>,<priv iface>|-netm <mode>|--network-mode=<mode>|-homer <homerhost[:heplifyport]>|\" \\\n        \" \" \"-db <[user[:pass]@]dbhost[:port][/dbname]>|--database=<[user[:pass]@]dbhost[:port][/dbname]>|-dsipcid <num>|--dsip-clusterid=<num>|\" \\\n        \" \" \"-dbadmin <[user[:pass]@]dbhost[:port][/dbname]>|--database-admin=<[user[:pass]@]dbhost[:port][/dbname]>|-dsipcsync <num>|\" \\\n        \" \" \"--dsip-clustersync=<num>|-dsipkey <32 chars>|--dsip-privkey=<32 chars>|-with_lcr|--with_lcr=<num>|-with_dev|--with_dev=<num>]\"\n    printf \"%-30s %s\\n\" \\\n        \"uninstall\" \"[-debug|-all|--all|-kam|--kamailio|-dsip|--dsiprouter|-rtp|--rtpengine]\"\n    printf \"%-30s %s\\n\" \\\n        \"clusterinstall\" \"[-debug] [-i <ssh key file>] <[user1[:pass1]@]node1[:port1]> <[user2[:pass2]@]node2[:port2]> ... -- [INSTALL OPTIONS]\"\n    printf \"%-30s %s\\n\" \\\n        \"upgrade\" \"[-debug|-dsipcid <num>|--dsip-clusterid=<num>|-url <repo url>|--repo-url=<repo url>] <-rel <release number>|--release=<release number>>\"\n    printf \"%-30s %s\\n\" \\\n        \"start\" \"[-debug|-all|--all|-kam|--kamailio|-dsip|--dsiprouter|-rtp|--rtpengine]\"\n    printf \"%-30s %s\\n\" \\\n        \"stop\" \"[-debug|-all|--all|-kam|--kamailio|-dsip|--dsiprouter|-rtp|--rtpengine]\"\n    printf \"%-30s %s\\n\" \\\n        \"restart\" \"[-debug|-all|--all|-kam|--kamailio|-dsip|--dsiprouter|-rtp|--rtpengine]\"\n    printf \"%-30s %s\\n\" \\\n        \"chown\" \"[-debug|-certs|-dnsmasq|-nginx|-kamailio|-dsiprouter|-rtpengine]\"\n    printf \"%-30s %s\\n\" \\\n        \"configurekam\" \"[-debug]\"\n    printf \"%-30s %s\\n\" \\\n        \"configuredsip\" \"[-debug]\"\n    printf \"%-30s %s\\n\" \\\n        \"configurertp\" \"[-debug]\"\n    printf \"%-30s %s\\n\" \\\n        \"renewsslcert\" \"[-debug]\"\n    printf \"%-30s %s\\n\" \\\n        \"configuresslcert\" \"[-debug|-f|--force|-o|--override=<[FQDN]>]\"\n    printf \"%-30s %s\\n\" \\\n        \"installmodules\" \"[-debug]\"\n    printf \"%-30s %s\\n\" \\\n        \"resetpassword\" \"[-debug|-q|--quiet|-all|--all|-dc|--dsip-creds|-ac|--api-creds|-kc|--kam-creds|-ic|--ipc-creds|-fid|--force-instance-id]\"\n    printf \"%-30s %s\\n%-30s %s\\n%-30s %s\\n%-30s %s\\n%-30s %s\\n\" \\\n        \"setcredentials\" \"[-debug|-dc <[user][:pass]>|--dsip-creds=<[user][:pass]>|-ac <token>|--api-creds=<token>|\" \\\n        \" \" \"-kc <[user[:pass]@]dbhost[:port][/dbname]>|--kam-creds=<[user[:pass]@]dbhost[:port][/dbname]>|\" \\\n        \" \" \"-mc <[user][:pass]>|--mail-creds=<[user][:pass]>|-ic <token>|--ipc-creds=<token>]|\" \\\n        \" \" \"-dac <[user[:pass]@]dbhost[:port][/dbname]>|--db-admin-creds=<[user[:pass]@]dbhost[:port][/dbname]>|\" \\\n        \" \" \"-sc <key>|--session-creds=<key>]\"\n    printf \"%-30s %s\\n%-30s %s\\n%-30s %s\\n\" \\\n        \"licensemanager\" \"[-debug] -list|-retrieve <license_key or 'tag=<tag>'>|\" \\\n        \" \" \"-activate <license_key>|-import <file containing keys>|\" \\\n        \" \" \"-clear|-deactivate <license_key or 'tag=<tag>'>|\" \\\n        \" \" \"-check <license_key or 'tag=<tag>'>\"\n    printf \"%-30s %s\\n\" \\\n        \"backup\" \"[-debug] [-f <output file>]\"\n    printf \"%-30s %s\\n\" \\\n        \"restore\" \"[-debug] [-f <input file>]\"\n    printf \"%-30s %s\\n\" \\\n        \"version|-v|--version\" \"\"\n    printf \"%-30s %s\\n\" \\\n        \"help|-h|--help\" \"\"\n\n    linebreak\n    printf '\\n%s\\n%s\\n%s\\n%s\\n%s\\n%s\\n%s\\n' \\\n        \"$(pprint -n SUMMARY:)\" \\\n        \"dSIPRouter is a Web Management GUI for Kamailio based on use case design, with a focus on ITSP and Carrier use cases.\" \\\n        \"This means that we aren’t a general purpose GUI for Kamailio.\" \\\n        \"If that's required then use Siremis, which is located at http://siremis.asipto.com/\" \\\n        \"This script is used for installing, uninstalling, managing, and configuring dSIPRouter and the various services it manages.\" \\\n        \"That includes starting/stopping/executing the Web GUI, manaing the Nginx reverse proxy, managing Kamailio, manaing RTPEngine and much more.\" \\\n        \"This script can also be used to sync service settings with dSIPRouter, install new modules, renew TLS certs, and configure a cluster.\"\n\n    linebreak\n    printf '\\n%s\\n%s\\n%s\\n%s\\n%s\\n\\n' \\\n        \"$(pprint -n MORE INFO:)\" \\\n        \"The full documentation available locally on your system: ${DSIP_PROTO}://${EXTERNAL_FQDN}:${DSIP_PORT}/docs/index.html\" \\\n        \"We also provide the documentation online for your convenience: https://dsiprouter.readthedocs.io\" \\\n        \"Drop by the project website for the latest information on the project: https://dsiprouter.org/\" \\\n        \"Support is available from dOpenSource. Visit us at https://dopensource.com/dsiprouter or call us at 888-907-2085\"\n\n    linebreak\n    printf '\\n%s\\n%s\\n%s\\n\\n' \\\n        \"$(pprint -n PROVIDED BY:)\" \\\n        \"dOpenSource | A Flyball Company\" \\\n        \"Made in Detroit, MI USA\"\n\n    linebreak\n}\n\n# make the output a little cleaner\nfunction setDebugMode() {\n    if [[ \"$*\" == *\"-debug\"* ]]; then\n        export DEBUG=1\n        # start debugging after this function exits\n        debugHandler() {\n            set -x\n            trap - RETURN\n        }\n        trap debugHandler RETURN\n    fi\n\n#    if [[ \"$*\" != *\"-debug\"* ]]; then\n#        # quiet pkg managers when not debugging\n#        if cmdExists 'apt-get'; then\n#            function apt-get() {\n#                command apt-get -qq \"$@\"\n#            }\n#            export -f apt-get\n#        fi\n#        if cmdExists 'yum'; then\n#            function yum() {\n#                command yum -q -e 0 \"$@\"\n#            }\n#            export -f yum\n#        fi\n#        if cmdExists 'dnf'; then\n#            function dnf() {\n#                command dnf -q -e 0 \"$@\"\n#            }\n#            export -f dnf\n#        fi\n#        # quiet make when not debugging\n#        function make() {\n#            command make -s \"$@\"\n#        }\n#        export -f make\n#    fi\n    return 0\n}\n\n# prep before processing command\nfunction preprocessCMD() {\n    # Display usage options if no command is specified\n    if (( $# == 0 )); then\n        usageOptions\n        exit 1\n    fi\n\n    setDebugMode \"$@\"\n\n    # Do not run the extra prep on these commands\n    # we only need a portion of the script settings\n    case \"$1\" in\n        chown|exec|clusterinstall|licensemanager|version|-v|--version|help|-h|--help)\n            setStaticScriptSettings\n            ;;\n        *)\n            initialChecks \"$@\"\n            ;;\n    esac\n}\n\n# process the commands to be executed\n# TODO: add help options for each command (with subsection usage info for that command)\n# TODO: move cli arg parsing to start of dsiprouter.sh (split out into its own file)\n# TODO: move cli arg/option definitions to separate shared JSON file\nfunction processCMD() {\n    # pre-processing / initial checks\n    preprocessCMD \"$@\"\n\n    # use options to add commands in any order needed\n    # 1 == defaults on, 0 == defaults off\n    local DISPLAY_LOGIN_INFO=0\n    # for install / uninstall, if no selections are chosen use some sane defaults\n    local DEFAULT_SERVICES=1\n\n    # process all options before running commands\n    declare -a RUN_COMMANDS\n    local ARG=\"$1\" OPT=\"\" RETVAL=0\n    case $ARG in\n        install)\n            # always add official repo's, set platform, and create init service\n            RUN_COMMANDS+=(configureSystemPath setCloudPlatform createInitService createSwapFile installDsiprouterCli)\n            shift\n\n            local NEW_ROOT_DB_USER=\"\" NEW_ROOT_DB_PASS=\"\" NEW_ROOT_DB_NAME=\"\" DB_CONN_URI=\"\" TMP_ARG=\"\"\n\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    -dns|--dnsmasq)\n                        RUN_COMMANDS+=(installDnsmasq)\n                        shift\n                        ;;\n                    -mysql|--mysql)\n                        DEFAULT_SERVICES=0\n                        RUN_CMMANDS+=(installMysql)\n                        shift\n                        ;;\n                    -kam|--kamailio)\n                        DEFAULT_SERVICES=0\n                        RUN_COMMANDS+=(installSipsak installCron installKamailio)\n                        shift\n                        ;;\n                    -dsip|--dsiprouter)\n                        DEFAULT_SERVICES=0\n                        DISPLAY_LOGIN_INFO=1\n                        RUN_COMMANDS+=(installSipsak installCron installNginx installDsiprouter)\n                        shift\n                        ;;\n                    -rtp|--rtpengine)\n                        DEFAULT_SERVICES=0\n                        RUN_COMMANDS+=(installCron installRTPEngine)\n                        shift\n                        ;;\n                    -all|--all)\n                        DEFAULT_SERVICES=0\n                        DISPLAY_LOGIN_INFO=1\n                        RUN_COMMANDS+=(installSipsak installCron installMysql installKamailio installNginx installDsiprouter installRTPEngine)\n                        shift\n                        ;;\n                    # DEPRECATED: marked for removal in v0.80\n                    -dmz|--dmz=*)\n                        NETWORK_MODE=2\n\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            TMP=$(echo \"$1\" | cut -d '=' -f 2)\n                            shift\n                        else\n                            shift\n                            TMP=\"$1\"\n                            shift\n                        fi\n\n                        PUBLIC_IFACE=$(echo \"$TMP\" | cut -d ',' -f 1)\n                        PRIVATE_IFACE=$(echo \"$TMP\" | cut -d ',' -f 2)\n                        ;;\n                    -netm|--network-mode=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            NETWORK_MODE=$(echo \"$1\" | cut -d '=' -f 2)\n                            shift\n                        else\n                            shift\n                            NETWORK_MODE=\"$1\"\n                            shift\n                        fi\n                        ;;\n                    -db|--database=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            DB_CONN_URI=$(printf '%s' \"$1\" | cut -d '=' -f 2)\n                            shift\n                        else\n                            shift\n                            DB_CONN_URI=\"$1\"\n                            shift\n                        fi\n\n                        TMP_VAL=$(parseDBConnURI -user \"$DB_CONN_URI\") && {\n                            export SET_KAM_DB_USER=\"$TMP_VAL\"\n                            export KAM_DB_USER=\"$TMP_VAL\"\n                        }\n                        TMP_VAL=$(parseDBConnURI -pass \"$DB_CONN_URI\") && {\n                            export SET_KAM_DB_PASS=\"$TMP_VAL\"\n                            export KAM_DB_PASS=\"$TMP_VAL\"\n                        }\n                        TMP_VAL=$(parseDBConnURI -host \"$DB_CONN_URI\") && {\n                            export SET_KAM_DB_HOST=\"$TMP_VAL\"\n                            export KAM_DB_HOST=\"$TMP_VAL\"\n                        }\n                        TMP_VAL=$(parseDBConnURI -port \"$DB_CONN_URI\") && {\n                            export SET_KAM_DB_PORT=\"$TMP_VAL\"\n                            export KAM_DB_PORT=\"$TMP_VAL\"\n                        }\n                        TMP_VAL=$(parseDBConnURI -name \"$DB_CONN_URI\") && {\n                            export SET_KAM_DB_NAME=\"$TMP_VAL\"\n                            export KAM_DB_NAME=\"$TMP_VAL\"\n                        }\n                        ;;\n                    -dsipcid|--dsip-clusterid=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            DSIP_CLUSTER_ID=\"$(echo \"$1\" | cut -d '=' -f 2)\"\n                            shift\n                        else\n                            shift\n                            DSIP_CLUSTER_ID=\"$1\"\n                            shift\n                        fi\n                        ;;\n                    -dbadmin|--database-admin=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            DB_CONN_URI=$(printf '%s' \"$1\" | cut -d '=' -f 2)\n                            shift\n                        else\n                            shift\n                            DB_CONN_URI=\"$1\"\n                            shift\n                        fi\n\n                        TMP_VAL=$(parseDBConnURI -user \"$DB_CONN_URI\") && export ROOT_DB_USER=\"$TMP_VAL\"\n                        TMP_VAL=$(parseDBConnURI -pass \"$DB_CONN_URI\") && export ROOT_DB_PASS=\"$TMP_VAL\"\n                        TMP_VAL=$(parseDBConnURI -host \"$DB_CONN_URI\") && export ROOT_DB_HOST=\"$TMP_VAL\"\n                        TMP_VAL=$(parseDBConnURI -port \"$DB_CONN_URI\") && export ROOT_DB_PORT=\"$TMP_VAL\"\n                        TMP_VAL=$(parseDBConnURI -name \"$DB_CONN_URI\") && export ROOT_DB_NAME=\"$TMP_VAL\"\n                        ;;\n                    -dsipcsync|--dsip-clustersync=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            DSIP_CLUSTER_SYNC=\"$(echo \"$1\" | cut -d '=' -f 2)\"\n                            shift\n                        else\n                            shift\n                            DSIP_CLUSTER_SYNC=\"$1\"\n                            shift\n                        fi\n\n                        # sanity check value for cluster sync\n                        case \"$DSIP_CLUSTER_SYNC\" in\n                            0|1)\n                                :\n                                ;;\n                            *)\n                                printerr 'Invalid value for setting DSIP_CLUSTER_SYNC'\n                                exit 1\n                                ;;\n                        esac\n\n                        # change default for loading settings to db\n                        LOAD_SETTINGS_FROM='db'\n                        ;;\n                    -dsipkey|--dsip-privkey=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            SET_DSIP_PRIV_KEY=\"$(printf '%s' \"$1\" | cut -d '=' -f 2)\"\n                            shift\n                        else\n                            shift\n                            SET_DSIP_PRIV_KEY=\"$1\"\n                            shift\n                        fi\n                        # sanity check\n                        if (( $(printf '%s' \"${SET_DSIP_PRIV_KEY}\" | wc -c) != 32 )); then\n                            printerr 'dSIPRouter private key must be 32 bytes'\n                            exit 1\n                        fi\n                        ;;\n                    -with_lcr|--with_lcr=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            if (( $(echo \"$1\" | cut -d '=' -f 2) > 0 )); then\n                                WITH_LCR=1\n                            else\n                                WITH_LCR=0\n                            fi\n                            shift\n                        else\n                            WITH_LCR=1\n                            shift\n                        fi\n                        ;;\n                    -with_dev|--with_dev=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            if (( $(echo \"$1\" | cut -d '=' -f 2) > 0 )); then\n                                RUN_COMMANDS+=(configGitDevEnv)\n                            fi\n                            shift\n                        else\n                            RUN_COMMANDS+=(configGitDevEnv)\n                            shift\n                        fi\n                        ;;\n                    -homer)\n                        shift\n                        export HOMER_HEP_HOST=$(printf '%s' \"$1\" | cut -d ':' -f -1)\n                        TMP_ARG=\"$(printf '%s' \"$1\" | cut -s -d ':' -f 2)\"\n                        [[ -n \"$TMP_ARG\" ]] && export HOMER_HEP_PORT=\"$TMP_ARG\"\n                        shift\n                        # sanity check\n                        if [[ -z \"$HOMER_HEP_HOST\" ]]; then\n                            printerr 'Missing required argument <homer_host> to option -homer'\n                            exit 1\n                        fi\n                        ;;\n                    --rtpengine-uri=*)\n                        RTPENGINE_URI=$(cut -s -d '=' -f 2- <<<\"$1\")\n                        shift\n                        # sanity check\n                        if [[ -z \"$RTPENGINE_URI\" ]]; then\n                            printerr 'Missing required argument to option \"--rtpengine-uri=\"'\n                            exit 1\n                        fi\n                        RUN_CMMANDS+=(updateRtpengineStartup)\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n\n            # only use defaults if no discrete services specified\n            if (( ${DEFAULT_SERVICES} == 1 )); then\n                DISPLAY_LOGIN_INFO=1\n                RUN_COMMANDS+=(installSipsak installMysql installKamailio installNginx installDsiprouter)\n            fi\n\n            # add displaying logo and login info to deferred commands\n            RUN_COMMANDS+=(displayLogo)\n            if (( ${DISPLAY_LOGIN_INFO} == 1 )); then\n                RUN_COMMANDS+=(displayLoginInfo)\n            fi\n            ;;\n        uninstall)\n            RUN_COMMANDS+=(setCloudPlatform)\n            shift\n\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    -dns|--dnsmasq)\n                        RUN_COMMANDS+=(uninstallDnsmasq)\n                        shift\n                        ;;\n                    -rtp|--rtpengine)\n                        DEFAULT_SERVICES=0\n                        RUN_COMMANDS+=(uninstallRTPEngine)\n                        shift\n                        ;;\n                    -dsip|--dsiprouter)\n                        DEFAULT_SERVICES=0\n                        RUN_COMMANDS+=(uninstallDsiprouter uninstallNginx)\n                        shift\n                        ;;\n                    -kam|--kamailio)\n                        DEFAULT_SERVICES=0\n                        RUN_COMMANDS+=(uninstallKamailio)\n                        shift\n                        ;;\n                    # only remove init and system config dir if all services will be removed (dependency for others)\n                    # same goes for official repo configs, we only remove if all dsiprouter configs are being removed\n                    -all|--all)\n                        DEFAULT_SERVICES=0\n                        RUN_COMMANDS+=(uninstallRTPEngine uninstallDsiprouter uninstallNginx uninstallKamailio uninstallMysql uninstallDnsmasq uninstallSipsak uninstallDsiprouterCli removeSwapFile removeInitService revertSystemRepos revertSystemPath)\n                        shift\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n\n            # only use defaults if no discrete services specified\n            if (( ${DEFAULT_SERVICES} == 1 )); then\n                RUN_COMMANDS+=(uninstallDsiprouter uninstallNginx uninstallKamailio uninstallMysql uninstallSipsak uninstallDsiprouterCli removeSwapFile removeInitService)\n            fi\n\n            # clean dev environment if configured\n            if [[ -e /usr/local/bin/_merge-changelog ]]; then\n                RUN_COMMANDS+=(cleanGitDevEnv)\n            fi\n\n            # display logo after install / uninstall commands\n            RUN_COMMANDS+=(displayLogo)\n            ;;\n        clusterinstall)\n            # install across remote cluster\n            RUN_COMMANDS+=(clusterInstall)\n            shift\n\n            SSH_SYNC_NODES=()\n            SSH_SYNC_ARGS=()\n\n            # loop through args and grab nodes\n            while (( $# > 0 )); do\n                ARG=\"$1\"\n                case $ARG in\n                    --)\n                        # scrap the --\n                        shift\n                        break\n                        ;;\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    -i)\n                        shift\n                        SSH_KEY_FILE=\"$1\"\n                        shift\n                        ;;\n                    *)  # add to list of nodes\n                        SSH_SYNC_NODES+=( \"$ARG\" )\n                        shift\n                        ;;\n                esac\n            done\n\n            # loop through args and grab install options\n            while (( $# > 0 )); do\n                ARG=\"$1\"\n                case $ARG in\n                    # we will transport securely instead\n                    -dsipkey|--dsip-privkey=*)\n                        if echo \"$ARG\" | grep -q '=' 2>/dev/null; then\n                            SET_DSIP_PRIV_KEY=\"$(printf '%s' \"$ARG\" | cut -d '=' -f 2)\"\n                            shift\n                        else\n                            shift\n                            SET_DSIP_PRIV_KEY=\"$1\"\n                            shift\n                        fi\n                        # sanity check\n                        if (( $(printf '%s' \"${SET_DSIP_PRIV_KEY}\" | wc -c) != 32 )); then\n                            printerr 'dSIPRouter private key must be 32 bytes'\n                            exit 1\n                        fi\n                        ;;\n                    *)  # add to list of args\n                        SSH_SYNC_ARGS+=( \"$ARG\" )\n                        shift\n                        ;;\n                esac\n            done\n\n            # sanity check\n            if (( ${#SSH_SYNC_NODES[@]} < 1 )); then\n                printerr \"At least 2 nodes are required to setup cluster\"\n                usageOptions\n                exit 1\n            fi\n            ;;\n        upgrade)\n            # upgrade dsiprouter version\n            RUN_COMMANDS+=(upgrade)\n            shift\n\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    -dsipcid|--dsip-clusterid=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            DSIP_CLUSTER_ID=\"$(echo \"$1\" | cut -d '=' -f 2)\"\n                            shift\n                        else\n                            shift\n                            DSIP_CLUSTER_ID=\"$1\"\n                            shift\n                        fi\n                        ;;\n                    -rel|--release=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            export UPGRADE_RELEASE=\"$(echo \"$1\" | cut -d '=' -f 2)\"\n                            shift\n                        else\n                            shift\n                            export UPGRADE_RELEASE=\"$1\"\n                            shift\n                        fi\n\n                        if [[ -z \"$UPGRADE_RELEASE\" ]]; then\n                            printerr \"Invalid upgrade release specified\"\n                            usageOptions\n                            exit 1\n                        fi\n\n                        # format as per branch name if given as version number\n                        if [[ \"${UPGRADE_RELEASE:0:1}\" != \"v\" ]]; then\n                            UPGRADE_RELEASE=\"v${UPGRADE_RELEASE}\"\n                        fi\n                        ;;\n                    -url|--repo-url=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            export UPGRADE_REPO=\"$(echo \"$1\" | cut -d '=' -f 2)\"\n                            shift\n                        else\n                            shift\n                            export UPGRADE_REPO=\"$1\"\n                            shift\n                        fi\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n\n            # repo we are upgrading from could have been provided on the CLI\n            if [[ -n \"$UPGRADE_REPO\" ]]; then\n                UPGRADE_RELEASE_URL=\"https://api.github.com/repos/$(rev <<<\"$UPGRADE_REPO\" | cut -d '/' -f -2 | cut -d '.' -f 2- | rev)/releases\"\n            else\n                UPGRADE_RELEASE_URL=\"$GIT_RELEASE_URL\"\n            fi\n\n            # use latest release if none specified\n            if [[ -z \"$UPGRADE_RELEASE\" ]]; then\n                TMP=$(curl -s \"$UPGRADE_RELEASE_URL\") &&\n                TMP=$(jq -e -r  '.[].tag_name | gsub(\"^(?<rel>v[0-9]+\\\\.[0-9]+).*?$\"; .rel)' <<<\"$TMP\") &&\n                UPGRADE_RELEASE=$(sort -gur <<<\"$TMP\" | head -1) || {\n                    printerr \"Could not retrieve latest release candidate\"\n                    exit 1\n                }\n            fi\n            ;;\n        start)\n            # start installed services\n            RUN_COMMANDS+=(start)\n            shift\n\n            DEFAULT_START_OPTIONS=1\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    -all|--all)\n                        DEFAULT_START_OPTIONS=0\n                        START_DSIPROUTER=1\n                        START_KAMAILIO=1\n                        START_RTPENGINE=1\n                        shift\n                        ;;\n                    -dsip|--dsiprouter)\n                        DEFAULT_START_OPTIONS=0\n                        START_DSIPROUTER=1\n                        shift\n                        ;;\n                    -kam|--kamailio)\n                        DEFAULT_START_OPTIONS=0\n                        START_KAMAILIO=1\n                        shift\n                        ;;\n                    -rtp|--rtpengine)\n                        DEFAULT_START_OPTIONS=0\n                        START_RTPENGINE=1\n                        shift\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n\n            # default to only starting dsip gui\n            if (( $DEFAULT_START_OPTIONS == 1 )); then\n                START_DSIPROUTER=1\n            fi\n            ;;\n        stop)\n            # stop installed services\n            RUN_COMMANDS+=(stop)\n            shift\n\n            DEFAULT_STOP_OPTIONS=1\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    -all|--all)\n                        DEFAULT_STOP_OPTIONS=0\n                        STOP_DSIPROUTER=1\n                        STOP_KAMAILIO=1\n                        STOP_RTPENGINE=1\n                        shift\n                        ;;\n                    -dsip|--dsiprouter)\n                        DEFAULT_STOP_OPTIONS=0\n                        STOP_DSIPROUTER=1\n                        shift\n                        ;;\n                    -kam|--kamailio)\n                        DEFAULT_STOP_OPTIONS=0\n                        STOP_KAMAILIO=1\n                        shift\n                        ;;\n                    -rtp|--rtpengine)\n                        DEFAULT_STOP_OPTIONS=0\n                        STOP_RTPENGINE=1\n                        shift\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n\n            # default to only stopping dsip gui\n            if (( $DEFAULT_STOP_OPTIONS == 1 )); then\n                STOP_DSIPROUTER=1\n            fi\n            ;;\n        restart)\n            RESTART_ARGS=(restart)\n            RESTART_DAEMONIZE=0\n\n            # restart installed services\n            RUN_COMMANDS+=(restart)\n            shift\n\n            DEFAULT_RESTART_OPTIONS=1\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        RESTART_ARGS+=(\"$OPT\")\n                        shift\n                        ;;\n                    -all|--all)\n                        DEFAULT_RESTART_OPTIONS=0\n                        STOP_DSIPROUTER=1\n                        START_DSIPROUTER=1\n                        STOP_KAMAILIO=1\n                        START_KAMAILIO=1\n                        STOP_RTPENGINE=1\n                        START_RTPENGINE=1\n                        RESTART_ARGS+=(\"$OPT\")\n                        shift\n                        ;;\n                    -dsip|--dsiprouter)\n                        DEFAULT_RESTART_OPTIONS=0\n                        STOP_DSIPROUTER=1\n                        START_DSIPROUTER=1\n                        RESTART_ARGS+=(\"$OPT\")\n                        shift\n                        ;;\n                    -kam|--kamailio)\n                        DEFAULT_RESTART_OPTIONS=0\n                        STOP_KAMAILIO=1\n                        START_KAMAILIO=1\n                        RESTART_ARGS+=(\"$OPT\")\n                        shift\n                        ;;\n                    -rtp|--rtpengine)\n                        DEFAULT_RESTART_OPTIONS=0\n                        STOP_RTPENGINE=1\n                        START_RTPENGINE=1\n                        RESTART_ARGS+=(\"$OPT\")\n                        shift\n                        ;;\n                    # internal usage only, no need for user to be calling with this option\n                    -daemonize)\n                        RESTART_DAEMONIZE=1\n                        shift\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n\n            # default to only restarting dsip gui\n            if (( $DEFAULT_RESTART_OPTIONS == 1 )); then\n                STOP_DSIPROUTER=1\n                START_DSIPROUTER=1\n            fi\n            ;;\n        # internal command, replace this process with the GUI server immediately\n        exec)\n            exec ${PYTHON_CMD} ${DSIP_PROJECT_DIR}/gui/dsiprouter.py\n            ;;\n        chown)\n            shift\n\n            # pop off the -debug option if provided\n            OPTS=(\"$@\")\n            for IDX in \"${!OPTS[@]}\"; do\n                case ${OPTS[$IDX]} in\n                    -debug)\n                        # already processed by setDebugMode()\n                        unset OPTS[$IDX]\n                        ;;\n                esac\n            done\n            set -- \"${OPTS[@]}\"\n\n            # pass the rest of the user args to the local function\n            # TODO: figure out how to pass variables into staged commands\n            updatePermissions \"$@\"\n            exit $?\n            ;;\n        configurertp)\n            # reconfigure rtpengine configs\n            RUN_COMMANDS+=(generateRtpengineConfig updateRtpengineConfig updateRtpengineStartup)\n            shift\n\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n            ;;\n        configurekam)\n            # reconfigure kamailio configs\n            RUN_COMMANDS+=(generateKamailioConfig updateKamailioConfig updateKamailioStartup)\n            shift\n\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n            ;;\n        configuredsip)\n            # reconfigure dsiprouter configs\n            RUN_COMMANDS+=(generateDsiprouterConfig updateDsiprouterConfig updateDsiprouterStartup)\n            shift\n\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n            ;;\n        renewsslcert)\n            # reconfigure ssl configs\n            RUN_COMMANDS+=(renewSSLCert)\n            shift\n\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n            ;;\n        configuresslcert)\n            # reconfigure ssl configs\n            RUN_COMMANDS+=(configureSSL)\n            shift\n\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    -f|--force)\n                        rm -f $DSIP_CERTS_DIR/dsiprouter-cert.pem\n                        rm -f $DSIP_CERTS_DIR/dsiprouter-key.pem\n                        shift\n                        ;;\n                    -o|--override=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            DNS_NAME_OVERRIDE=$(echo \"$1\" | cut -d '=' -f 2)\n                            shift\n                        else\n                            shift\n                            DNS_NAME_OVERRIDE=\"$1\"\n                            shift\n                        fi\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n            ;;\n        installmodules)\n            # reconfigure dsiprouter modules\n            RUN_COMMANDS+=(installModules restart)\n            shift\n\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n            ;;\n        resetpassword)\n            # reset secure credentials\n            RUN_COMMANDS+=(setCloudPlatform setCredentials)\n            shift\n\n            # by default we display the new login information\n            DISPLAY_LOGIN_INFO=1\n\n            # we default to resetting only the dsip gui password\n            # otherwise only the credentials specified are reset\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    -q|--quiet)\n                        DISPLAY_LOGIN_INFO=0\n                        shift\n                        ;;\n                    -all|--all)\n                        RESET_DSIP_GUI_PASS=1\n                        RESET_DSIP_API_TOKEN=1\n                        RESET_KAM_DB_PASS=1\n                        RESET_DSIP_IPC_TOKEN=1\n                        shift\n                        ;;\n                    -dc|--dsip-creds)\n                        RESET_DSIP_GUI_PASS=1\n                        shift\n                        ;;\n                    -ac|--api-creds)\n                        RESET_DSIP_API_TOKEN=1\n                        RESET_DSIP_GUI_PASS=${RESET_DSIP_GUI_PASS:-0}\n                        shift\n                        ;;\n                    -kc|--kam-creds)\n                        RESET_KAM_DB_PASS=1\n                        RESET_DSIP_GUI_PASS=${RESET_DSIP_GUI_PASS:-0}\n                        shift\n                        ;;\n                    -ic|--ipc-creds)\n                        RESET_DSIP_IPC_TOKEN=1\n                        RESET_DSIP_GUI_PASS=${RESET_DSIP_GUI_PASS:-0}\n                        shift\n                        ;;\n                    -fid|--force-instance-id)\n                        RESET_FORCE_INSTANCE_ID=1\n                        shift\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n\n            # preconditions checks and setting variables to pass to setCredentials()\n            if (( ${RESET_DSIP_GUI_PASS:-1} == 1 )); then\n                if (( ${IMAGE_BUILD} == 1 || ${RESET_FORCE_INSTANCE_ID:-0} == 1 )); then\n                    SET_DSIP_GUI_PASS=$(getInstanceID)\n                    if [[ -z \"$SET_DSIP_GUI_PASS\" ]]; then\n                        printerr \"Could not retrieve the instance ID for password reset\"\n                        exit 1\n                    fi\n                else\n                    SET_DSIP_GUI_PASS=$(urandomChars 64)\n                fi\n            fi\n            if (( ${RESET_DSIP_API_TOKEN:-0} == 1 )); then\n                SET_DSIP_API_TOKEN=$(urandomChars 64)\n            fi\n            if (( ${RESET_DSIP_IPC_TOKEN:-0} == 1 )); then\n                SET_DSIP_IPC_TOKEN=$(urandomChars 64)\n            fi\n            if (( ${RESET_KAM_DB_PASS:-0} == 1 )); then\n                export SET_KAM_DB_PASS=$(urandomChars 64)\n            fi\n\n            # display if not in quiet mode\n            if (( ${DISPLAY_LOGIN_INFO} == 1 )); then\n                RUN_COMMANDS+=(displayLoginInfo)\n            fi\n            ;;\n        setcredentials)\n            # set secure credentials to fixed values\n            RUN_COMMANDS+=(setCredentials)\n            shift\n\n            local TMP_VAL=''\n\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    -dc|--dsip-creds=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            CREDS_URI=$(echo \"$1\" | cut -d '=' -f 2)\n                            shift\n                        else\n                            shift\n                            CREDS_URI=\"$1\"\n                            shift\n                        fi\n\n                        SET_DSIP_GUI_USER=$(perl -pe 's%([^:/\\t\\r\\n\\v\\f]+)?(?::([^/\\t\\r\\n\\v\\f]*))?%\\1%' <<<\"$CREDS_URI\")\n                        SET_DSIP_GUI_PASS=$(perl -pe 's%([^:/\\t\\r\\n\\v\\f]+)?(?::([^/\\t\\r\\n\\v\\f]*))?%\\2%' <<<\"$CREDS_URI\")\n                        ;;\n                    -ac|--api-creds=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            SET_DSIP_API_TOKEN=$(echo \"$1\" | cut -d '=' -f 2)\n                            shift\n                        else\n                            shift\n                            SET_DSIP_API_TOKEN=\"$1\"\n                            shift\n                        fi\n                        ;;\n                    -kc|--kam-creds=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            DB_CONN_URI=$(echo \"$1\" | cut -d '=' -f 2)\n                            shift\n                        else\n                            shift\n                            DB_CONN_URI=\"$1\"\n                            shift\n                        fi\n\n                        # sanity check\n                        if [[ -z \"${DB_CONN_URI}\" ]]; then\n                            printerr \"Credentials must be given for option $OPT\"\n                            exit 1\n                        fi\n\n                        TMP_VAL=$(parseDBConnURI -user \"$DB_CONN_URI\") && export SET_KAM_DB_USER=\"$TMP_VAL\"\n                        TMP_VAL=$(parseDBConnURI -pass \"$DB_CONN_URI\") && export SET_KAM_DB_PASS=\"$TMP_VAL\"\n                        TMP_VAL=$(parseDBConnURI -host \"$DB_CONN_URI\") && export SET_KAM_DB_HOST=\"$TMP_VAL\"\n                        TMP_VAL=$(parseDBConnURI -port \"$DB_CONN_URI\") && export SET_KAM_DB_PORT=\"$TMP_VAL\"\n                        TMP_VAL=$(parseDBConnURI -name \"$DB_CONN_URI\") && export SET_KAM_DB_NAME=\"$TMP_VAL\"\n                        ;;\n                    -mc|--mail-creds=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            CREDS_URI=$(echo \"$1\" | cut -d '=' -f 2)\n                            shift\n                        else\n                            shift\n                            CREDS_URI=\"$1\"\n                            shift\n                        fi\n\n                        SET_DSIP_MAIL_USER=$(perl -pe 's%([^:/\\t\\r\\n\\v\\f]+)?(?::([^/\\t\\r\\n\\v\\f]*))?%\\1%' <<<\"$CREDS_URI\")\n                        SET_DSIP_MAIL_PASS=$(perl -pe 's%([^:/\\t\\r\\n\\v\\f]+)?(?::([^/\\t\\r\\n\\v\\f]*))?%\\2%' <<<\"$CREDS_URI\")\n                        ;;\n                    -ic|--ipc-creds=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            SET_DSIP_IPC_TOKEN=$(echo \"$1\" | cut -d '=' -f 2)\n                            shift\n                        else\n                            shift\n                            SET_DSIP_IPC_TOKEN=\"$1\"\n                            shift\n                        fi\n                        ;;\n                    -dac|--database-admin-creds=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            DB_CONN_URI=$(printf '%s' \"$1\" | cut -d '=' -f 2)\n                            shift\n                        else\n                            shift\n                            DB_CONN_URI=\"$1\"\n                            shift\n                        fi\n\n                        # sanity check\n                        if [[ -z \"${DB_CONN_URI}\" ]]; then\n                            printerr \"Credentials must be given for option $OPT\"\n                            exit 1\n                        fi\n\n                        TMP_VAL=$(parseDBConnURI -user \"$DB_CONN_URI\") && export SET_ROOT_DB_USER=\"$TMP_VAL\"\n                        TMP_VAL=$(parseDBConnURI -pass \"$DB_CONN_URI\") && export SET_ROOT_DB_PASS=\"$TMP_VAL\"\n                        TMP_VAL=$(parseDBConnURI -host \"$DB_CONN_URI\") && export SET_ROOT_DB_HOST=\"$TMP_VAL\"\n                        TMP_VAL=$(parseDBConnURI -port \"$DB_CONN_URI\") && export SET_ROOT_DB_PORT=\"$TMP_VAL\"\n                        TMP_VAL=$(parseDBConnURI -name \"$DB_CONN_URI\") && export SET_ROOT_DB_NAME=\"$TMP_VAL\"\n                        ;;\n                    -sc|--session-creds=*)\n                        if echo \"$1\" | grep -q '=' 2>/dev/null; then\n                            SET_DSIP_SESSION_KEY=$(echo \"$1\" | cut -d '=' -f 2)\n                            shift\n                        else\n                            shift\n                            SET_DSIP_SESSION_KEY=\"$1\"\n                            shift\n                        fi\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n            ;;\n        # DEPRECATED: in favor of using configurekam command, marked for removal in v0.80\n        generatekamconfig)\n            # generate kamailio configs from templates\n            RUN_COMMANDS+=(generateKamailioConfig)\n            shift\n\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n            ;;\n        # internal command, update kamailio config dynamically\n        updatekamconfig)\n            # update kamailio config\n            RUN_COMMANDS+=(updateKamailioConfig)\n            shift\n\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n            ;;\n        # internal command, update dsiprouter config dynamically\n        updatedsipconfig)\n            # update kamailio config\n            RUN_COMMANDS+=(updateDsiprouterConfig)\n            shift\n\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n            ;;\n        # internal command, update rtpengine config dynamically\n        updatertpconfig)\n            # update rtpengine config\n            RUN_COMMANDS+=(updateRtpengineConfig)\n            shift\n\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n            ;;\n        # internal command, update dnsmasq config dynamically\n        updatednsconfig)\n            # update dnsmasq config\n            RUN_COMMANDS+=(updateDnsConfig)\n            shift\n\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n            ;;\n        # internal command, generate CA dir from CA bundle file\n        updatecacertsdir)\n            # update dnsmasq config\n            RUN_COMMANDS+=(updateCACertsDir)\n            shift\n\n            while (( $# > 0 )); do\n                OPT=\"$1\"\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    *)  # fail on unknown option\n                        printerr \"Invalid option [$OPT] for command [$ARG]\"\n                        usageOptions\n                        exit 1\n                        shift\n                        ;;\n                esac\n            done\n            ;;\n        licensemanager)\n            shift\n\n            # handle the debug option here\n            for OPT in \"$@\"; do\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                esac\n            done\n\n            # pass the rest of the user args to the local function\n            licenseManager \"$@\"\n            exit $?\n            ;;\n        backup)\n            shift\n\n            for OPT in \"$@\"; do\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    -f)\n                        shift\n                        DUMP_FILE=\"$1\"\n                        shift\n                        ;;\n                esac\n            done\n\n            if [[ -z \"$DUMP_FILE\" ]]; then\n                DUMP_FILE=${CURR_BACKUP_DIR}/db.sql\n            fi\n\n            printdbg \"backing up database to $DUMP_FILE\"\n            dumpDB \"$KAM_DB_NAME\" >\"$DUMP_FILE\"\n            (( $? != 0 )) && {\n                printerr 'failed backing up database'\n                exit 1\n            }\n            chown dsiprouter:root \"$DUMP_FILE\"\n            pprint 'database backup created'\n            exit 0\n            ;;\n        restore)\n            shift\n\n            for OPT in \"$@\"; do\n                case $OPT in\n                    -debug)\n                        # already processed by setDebugMode()\n                        shift\n                        ;;\n                    -f)\n                        shift\n                        DUMP_FILE=\"$1\"\n                        shift\n                        ;;\n                esac\n            done\n\n            if [[ -z \"$DUMP_FILE\" ]]; then\n                DUMP_FILE=${CURR_BACKUP_DIR}/db.sql\n            fi\n\n            printdbg \"restoring database from $DUMP_FILE\"\n            withRootDBConn mysql <\"$DUMP_FILE\"\n            (( $? != 0 )) && {\n                printerr 'failed restoring database'\n                exit 1\n            }\n            pprint 'database restored from backup'\n            exit 0\n            ;;\n        version|-v|--version)\n            printf '%s\\n' \"$(getConfigAttrib 'VERSION' ${DSIP_CONFIG_FILE})\"\n            exit 1\n            ;;\n        help|-h|--help)\n            usageOptions\n            exit 1\n            ;;\n        *)\n            printerr \"Invalid command [$ARG]\"\n            usageOptions\n            exit 1\n            ;;\n    esac\n\n    # remove duplicate commands, while preserving order\n    RUN_COMMANDS=( $(printf '%s\\n' \"${RUN_COMMANDS[@]}\" | awk '!x[$0]++') )\n\n    # Options are processed... run commands. Processing Notes below.\n    # default priority of install (with rtpengine):\n    # 1. kamailio\n    # 2. dsiprouter\n    # 3. rtpengine\n    # default order of install (without rtpengine):\n    # 1. kamailio\n    # 2. dsiprouter\n    # default order of install (without dsiprouter):\n    # 1. kamailio\n    # 2. rtpengine\n    for RUN_COMMAND in \"${RUN_COMMANDS[@]}\"; do\n        $RUN_COMMAND || {\n            printerr \"[$0:$RUN_CMMAND()] an unhandled fatal error occurred.. halting execution.\"\n            exit $?\n        }\n    done\n    exit 0\n} #end of processCMD\n\nprocessCMD \"$@\"\n"
  },
  {
    "path": "gui/database/__init__.py",
    "content": "# make sure the generated source files are imported instead of the template ones\nimport sys\n\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nimport base64, bson, inspect, os\nfrom collections import OrderedDict\nfrom enum import Enum\nfrom datetime import datetime, timedelta\nfrom sqlalchemy import create_engine, MetaData, Table, Column, String, exc as sql_exceptions, Integer, event\nfrom sqlalchemy.orm import registry, sessionmaker, scoped_session, validates\nfrom sqlalchemy.sql import text\nimport settings\nfrom shared import IO, debugException, dictToStrFields, rowToDict, objToDict\nfrom util.networking import safeUriToHost, safeFormatSipUri, encodeSipUser\nfrom util.security import AES_CTR\n\n# DB specific settings\nUnsignedInt = Integer()\nif settings.KAM_DB_TYPE == \"mysql\":\n    try:\n        import MySQLdb as db_driver\n    except ImportError:\n        try:\n            import _mysql as db_driver\n        except ImportError:\n            try:\n                import pymysql as db_driver\n            except ImportError:\n                raise\n            except Exception as ex:\n                if settings.DEBUG:\n                    debugException(ex)\n                raise\n    from sqlalchemy.dialects.mysql import INTEGER\n\n    UnsignedInt = UnsignedInt.with_variant(INTEGER(unsigned=True), 'mysql', 'mariadb')\n\n# global constants\nDB_ENGINE_NAME = 'global_db_engine'\nSESSION_LOADER_NAME = 'global_session_loader'\n\n\nclass Gateways(object):\n    \"\"\"\n    Schema for dr_gateways table\\n\n    Documentation: `dr_gateways table <https://kamailio.org/docs/db-tables/kamailio-db-5.5.x.html#gen-db-dr-gateways>`_\\n\n    Allowed address types: SIP URI, IP address or DNS domain name\\n\n    The address field can be a full SIP URI, partial URI, or only host; where host portion is an IP or FQDN\n    \"\"\"\n\n    gwid = Column(UnsignedInt, primary_key=True, autoincrement=True, nullable=False)\n\n    def __init__(self, name, address, strip, prefix, type=0, gwgroup=None, addr_id=None,\n                 msteams_domain='', signalling='proxy', media='proxy'):\n        description = {\"name\": name}\n        if gwgroup is not None:\n            description[\"gwgroup\"] = str(gwgroup)\n        if addr_id is not None:\n            description['addr_id'] = str(addr_id)\n\n        self.type = type\n        self.address = address\n        self.strip = strip\n        self.pri_prefix = prefix\n        self.attrs = Gateways.buildAttrs(0, type, msteams_domain, signalling, media)\n        self.description = dictToStrFields(description)\n\n    @staticmethod\n    def buildAttrs(gwid=0, type=0, msteams_domain='', signalling='proxy', media='proxy'):\n        # gwid in dr_attrs is updated via trigger before insert/update\n        return ','.join([str(gwid), str(type), msteams_domain, signalling, media])\n\n    def attrsToDict(self):\n        attrs_dict = {}\n        attrs_list = self.attrs.split(',')\n        try:\n            attrs_dict['gwid'] = int(attrs_list[0])\n        except IndexError:\n            attrs_dict['gwid'] = 0\n        try:\n            attrs_dict['type'] = int(attrs_list[1])\n        except IndexError:\n            attrs_dict['type'] = 0\n        try:\n            attrs_dict['msteams_domain'] = attrs_list[2]\n        except IndexError:\n            attrs_dict['msteams_domain'] = ''\n        try:\n            attrs_dict['signalling'] = attrs_list[3]\n        except IndexError:\n            attrs_dict['signalling'] = 'proxy'\n        try:\n            attrs_dict['media'] = attrs_list[4]\n        except IndexError:\n            attrs_dict['media'] = 'proxy'\n        return attrs_dict\n\n\nclass GatewayGroups(object):\n    \"\"\"\n    Schema for dr_gw_lists table\\n\n    Documentation: `dr_gw_lists table <https://kamailio.org/docs/db-tables/kamailio-db-5.5.x.html#gen-db-dr-gw-lists>`_\n    \"\"\"\n\n    id = Column(UnsignedInt, primary_key=True, autoincrement=True, nullable=False)\n\n    class FILTER(Enum):\n        ENDPOINT = f'type:{settings.FLT_PBX}(,|$)'\n        CARRIER = f'type:{settings.FLT_CARRIER}(,|$)'\n        MSTEAMS = f'type:{settings.FLT_MSTEAMS}(,|$)'\n        ENDPOINT_OR_CARRIER = f'type:({settings.FLT_PBX}|{settings.FLT_CARRIER})(,|$)'\n\n    def __init__(self, name, gwlist=[], type=settings.FLT_CARRIER, dlg_timeout=None):\n        description = {'name': name, 'type': type}\n        if dlg_timeout is not None:\n            description['dlg_timeout'] = dlg_timeout\n\n        self.description = dictToStrFields(description)\n        self.gwlist = \",\".join(str(gw) for gw in gwlist)\n\n\nclass Address(object):\n    \"\"\"\n    Schema for address table\\n\n    Documentation: `address table <https://kamailio.org/docs/db-tables/kamailio-db-5.5.x.html#gen-db-address>`_\\n\n    Allowed address types: exact IP address, subnet IP address or DNS domain name\\n\n    The ip_addr field is either an IP address or DNS domain name; mask field is for subnet\n    \"\"\"\n\n    def __init__(self, name, ip_addr, mask, type, gwgroup=None, port=0):\n        tag = {\"name\": name}\n        if gwgroup is not None:\n            tag[\"gwgroup\"] = str(gwgroup)\n\n        self.grp = type\n        self.ip_addr = ip_addr\n        self.mask = mask\n        self.port = port\n        self.tag = dictToStrFields(tag)\n\n    pass\n\n\nclass InboundMapping(object):\n    \"\"\"\n    Partial Schema for modified version of dr_rules table\\n\n    Documentation: `dr_rules table <https://kamailio.org/docs/db-tables/kamailio-db-5.5.x.html#gen-db-dr-rules>`_\n    \"\"\"\n\n    gwname = Column(String)\n\n    def __init__(self, groupid, prefix, gwlist, description=''):\n        self.groupid = groupid\n        self.prefix = prefix\n        self.gwlist = gwlist\n        self.description = description\n        self.timerec = ''\n        self.routeid = ''\n\n    pass\n\n\nclass OutboundRoutes(object):\n    \"\"\"\n    Schema for dr_rules table\\n\n    Documentation: `dr_rules table <https://kamailio.org/docs/db-tables/kamailio-db-5.5.x.html#gen-db-dr-rules>`_\n    \"\"\"\n\n    def __init__(self, groupid, prefix, timerec, priority, routeid, gwlist, description):\n        self.groupid = groupid\n        self.prefix = prefix\n        self.timerec = timerec\n        self.priority = priority\n        self.routeid = routeid\n        self.gwlist = gwlist\n        self.description = description\n\n    pass\n\n\nclass CustomRouting(object):\n    \"\"\"\n    Schema for dr_custom_rules table\\n\n    \"\"\"\n\n    def __init__(self, locality, ppm, description):\n        self.locality = locality\n        self.ppm = ppm\n        self.description = description\n\n    pass\n\n\nclass dSIPLCR(object):\n    \"\"\"\n    Schema for LCR lookup\\n\n    Mapped to db table: dsip_lcr\n    The pattern field contains a complex key that includes FROM XNPA and TO NPA\\n\n    There the pattern field looks like this XNPA-NPA\\n\n    The dr_groupid field contains a dynamic routing group that maps to a gateway\n    \"\"\"\n\n    def __init__(self, pattern, from_prefix, dr_groupid, cost=0.00):\n        self.pattern = pattern\n        self.from_prefix = from_prefix\n        self.dr_groupid = dr_groupid\n        self.cost = cost\n\n    pass\n\n\nclass dSIPMultiDomainMapping(object):\n    \"\"\"\n    Schema for Multi-Tenant PBX\\n\n    Mapped to db table: dsip_multidomain_mapping\n    \"\"\"\n\n    class FLAGS(Enum):\n        DOMAIN_DISABLED = 0\n        DOMAIN_ENABLED = 1\n        TYPE_UNKNOWN = 0\n        TYPE_FUSIONPBX = 1\n        TYPE_FUSIONPBX_CLUSTER = 2\n        TYPE_FREEPBX = 3\n\n    def __init__(self, pbx_id, db_host, db_username, db_password, domain_list=None, attr_list=None, type=0, enabled=1):\n        self.pbx_id = pbx_id\n        self.db_host = db_host\n        self.db_username = db_username\n        self.db_password = db_password\n        self.domain_list = \",\".join(str(domain_id) for domain_id in domain_list) if domain_list else ''\n        self.attr_list = \",\".join(str(attr_id) for attr_id in attr_list) if attr_list else ''\n        self.type = type\n        self.enabled = enabled\n\n    pass\n\n\nclass dSIPDomainMapping(object):\n    \"\"\"\n    Schema for Single-Tenant PBX domain mapping\\n\n    Mapped to db table: dsip_domain_mapping\n    \"\"\"\n\n    class FLAGS(Enum):\n        DOMAIN_DISABLED = 0\n        DOMAIN_ENABLED = 1\n        TYPE_UNKNOWN = 0\n        TYPE_ASTERISK = 1\n        TYPE_SIPFOUNDRY = 2\n        TYPE_ELASTIX = 3\n        TYPE_FREESWITCH = 4\n        TYPE_OPENPBX = 5\n        TYPE_FREEPBX = 6\n        TYPE_PBXINAFLASH = 7\n        TYPE_3CX = 8\n\n    def __init__(self, pbx_id, domain_id, attr_list, type=0, enabled=1):\n        self.pbx_id = pbx_id\n        self.domain_id = domain_id\n        self.attr_list = \",\".join(str(attr_id) for attr_id in attr_list)\n        self.type = type\n        self.enabled = enabled\n\n    pass\n\n\nclass Subscribers(object):\n    \"\"\"\n    Schema for subscriber table\\n\n    Documentation: `subscriber table <https://kamailio.org/docs/db-tables/kamailio-db-5.5.x.html#gen-db-subscriber>`_\n    \"\"\"\n\n    def __init__(self, username, password, domain, gwid, email_address=None):\n        self.username = username\n        self.password = password\n        self.domain = domain\n        self.ha1 = ''\n        self.ha1b = ''\n        self.rpid = gwid\n        self.email_address = email_address\n\n    pass\n\n\nclass dSIPLeases(object):\n    \"\"\"\n    Schema for dsip_endpoint_leases table\\n\n    maintains a list of active leases based on seconds\n    \"\"\"\n\n    def __init__(self, gwid, sid, ttl):\n        self.gwid = gwid\n        self.sid = sid\n        t = datetime.now() + timedelta(seconds=ttl)\n        self.expiration = t.strftime('%Y-%m-%d %H:%M:%S')\n\n    pass\n\n\nclass dSIPMaintModes(object):\n    \"\"\"\n    Schema for dsip_maintmode table\\n\n    maintains a list of endpoints and carriers that are in maintenance mode\n    \"\"\"\n\n    def __init__(self, ipaddr, gwid, status=1):\n        self.ipaddr = ipaddr\n        self.gwid = gwid\n        self.status = status\n        self.createdate = datetime.now()\n\n    pass\n\n\nclass dSIPCallSettings(object):\n    \"\"\"\n    Schema for dsip_call_settings table\\n\n    \"\"\"\n\n    def __init__(self, gwgroupid, limit=None, timeout=None):\n        self.gwgroupid = gwgroupid\n        self.limit = limit\n        self.timeout = timeout\n\n    pass\n\n\nclass dSIPNotification(object):\n    \"\"\"\n    Schema for dsip_notification table\\n\n    maintains the list of notifications\n    \"\"\"\n\n    class FLAGS(Enum):\n        METHOD_EMAIL = 0\n        METHOD_SLACK = 1\n        TYPE_OVERLIMIT = 0\n        TYPE_GWFAILURE = 1\n\n    def __init__(self, gwgroupid, type, method, value):\n        self.gwgroupid = gwgroupid\n        self.type = type\n        self.method = method\n        self.value = value\n        self.createdate = datetime.now()\n\n    pass\n\n\nclass dSIPHardFwd(object):\n    \"\"\"\n    Schema for dsip_hardfwd table\\n\n    \"\"\"\n\n    def __init__(self, dr_ruleid, did, dr_groupid):\n        self.dr_ruleid = dr_ruleid\n        self.did = did\n        self.dr_groupid = dr_groupid\n\n    pass\n\n\nclass dSIPCDRInfo(object):\n    \"\"\"\n    Schema for dsip_cdrinfo table\\n\n    \"\"\"\n\n    def __init__(self, gwgroupid, email, send_interval):\n        self.gwgroupid = gwgroupid\n        self.email = email\n        self.send_interval = send_interval\n        self.last_sent = datetime.now()\n\n    pass\n\n\nclass dSIPFailFwd(object):\n    \"\"\"\n    Schema for dsip_failfwd table\\n\n    \"\"\"\n\n    def __init__(self, dr_ruleid, did, dr_groupid):\n        self.dr_ruleid = dr_ruleid\n        self.did = did\n        self.dr_groupid = dr_groupid\n\n    pass\n\n\nclass dSIPCertificates(object):\n    \"\"\"\n    Schema for dsip_certificates table\\n\n    \"\"\"\n\n    def __init__(self, domain, type, email, cert, key):\n        self.domain = domain\n        self.type = type\n        self.email = email\n        self.cert = cert\n        self.key = key\n\n    pass\n\n\nclass dSIPDNIDEnrichment(object):\n    \"\"\"\n    Schema for dsip_dnid_enrich_lnp table\\n\n    \"\"\"\n\n    def __init__(self, dnid, country_code='', routing_number='', rule_name=''):\n        description = {'name': rule_name}\n\n        self.dnid = dnid\n        self.country_code = country_code\n        self.routing_number = routing_number\n        self.description = dictToStrFields(description)\n\n    pass\n\n\nclass UAC(object):\n    \"\"\"\n    Schema for uacreg table\\n\n    Documentation: `uacreg table <https://kamailio.org/docs/db-tables/kamailio-db-5.5.x.html#gen-db-uacreg>`_\n    \"\"\"\n\n    class FLAGS(Enum):\n        REG_ENABLED = 0\n        REG_DISABLED = 1\n        REG_IN_PROGRESS = 2\n        REG_SUCCEEDED = 4\n        REG_IN_PROGRESS_AUTH = 8\n        REG_INITIALIZED = 16\n\n    def __init__(self, uuid, username=\"\", password=\"\", realm=\"\", auth_username=\"\", auth_proxy=\"\", local_domain=\"\", remote_domain=\"\", flags=0):\n        self.l_uuid = uuid\n        self.l_username = username\n        self.l_domain = local_domain\n        self.r_username = encodeSipUser(username)\n        self.auth_username = auth_username\n        self.r_domain = remote_domain\n        self.realm = realm\n        self.auth_password = password\n        self.auth_ha1 = \"\"\n        self.auth_proxy = auth_proxy\n        self.expires = 60\n        self.flags = flags\n        self.reg_delay = 0\n        self.socket = ''\n\ndef uacMapperEventHandler(mapper, connection, target):\n    target.r_username = encodeSipUser(target.r_username)\n\nevent.listen(UAC, 'before_insert', uacMapperEventHandler)\nevent.listen(UAC, 'before_update', uacMapperEventHandler)\n\n\nclass Domain(object):\n    \"\"\"\n    Schema for domain table\\n\n    Documentation: `domain table <https://kamailio.org/docs/db-tables/kamailio-db-5.5.x.html#gen-db-domain>`_\n    \"\"\"\n\n    def __init__(self, domain, did=None, last_modified=datetime.utcnow()):\n        self.domain = domain\n        if did is None:\n            did = domain\n        self.did = did\n        self.last_modified = last_modified\n\n    @validates(\"domain\")\n    def __validateDomain(self, key, domain):\n        if domain == '':\n            raise ValueError('empty string is an invalid domain')\n        return domain\n\n\nclass DomainAttrs(object):\n    \"\"\"\n    Schema for domain_attrs table\\n\n    Documentation: `domain_attrs table <https://kamailio.org/docs/db-tables/kamailio-db-5.5.x.html#gen-db-domain-attrs>`_\n    \"\"\"\n\n    class FLAGS(Enum):\n        TYPE_INTEGER = 0\n        TYPE_STRING = 2\n\n    def __init__(self, did, name=\"pbx_ip\", type=2, value=None, last_modified=datetime.utcnow()):\n        self.did = did\n        self.name = name\n        self.type = type\n        self.last_modified = last_modified\n        temp_value = value if value is not None else safeUriToHost(did)\n        self.value = temp_value if temp_value is not None else did\n\n    pass\n\n\nclass Dispatcher(object):\n    \"\"\"\n    Schema for dispatcher table\\n\n    Documentation: `dispatcher table <https://kamailio.org/docs/db-tables/kamailio-db-5.5.x.html#gen-db-dispatcher>`_\n    \"\"\"\n\n    DST_ALG = {\n        'ROUND_ROBIN': 4,\n        'PRIORITY_BASED': 8,\n        'WEIGHT_BASED': 9,\n        'LOAD_DISTRIBUTION': 10,\n        'RELATIVE_WEIGHT': 11,\n        'PARALLEL_FORKING': 12\n    }\n    FLAGS = {\n        'INACTIVE_DST': 1,\n        'TRYING_DST': 2,\n        'DISABLED_DST': 4,\n        'KEEP_ALIVE': 8,\n        'SKIP_DNS': 16\n    }\n\n    # TODO: setting attrs directly will be removed in the future and each possible attribute identified\n    def __init__(self, setid, destination, flags=None, priority=None, attrs=None, rweight=0, signalling='proxy', media='proxy',\n                 name=None, gwid=None):\n        self.setid = setid\n        self.destination = safeFormatSipUri(destination)\n        self.flags = flags\n        self.priority = priority\n        if attrs is not None:\n            self.attrs = attrs\n        else:\n            self.attrs = Dispatcher.buildAttrs(rweight, signalling, media)\n        self.description = Dispatcher.buildDescription(name, gwid)\n\n    @staticmethod\n    def buildAttrs(rweight=0, signalling='proxy', media='proxy'):\n        attrs = {'signalling': signalling, 'media': media, 'rweight': str(rweight)}\n        return dictToStrFields(attrs, delims=(';', '='))\n\n    @staticmethod\n    def buildDescription(name=None, gwid=None):\n        description = {}\n        if name is not None:\n            description['name'] = name\n        if gwid is not None:\n            description['gwid'] = str(gwid)\n        return dictToStrFields(description, delims=(';', '='))\n\n    def attrsToDict(self):\n        attrs = {}\n        for attr in self.attrs.split(';'):\n            attr = attr.split('=', maxsplit=1)\n            if len(attr) != 2 or attr[1] == '':\n                continue\n            if attr[1].isnumeric():\n                attrs[attr[0]] = int(attr[1])\n            else:\n                attrs[attr[0]] = attr[1]\n        return attrs\n\n\n# TODO: create class for dsip_settings table\n\nclass dSIPUser(object):\n    \"\"\"\n    Schema for the dSIPROuter User table\n    \"\"\"\n\n    def __init__(self, firstname, lastname, username, password, roles, domains, token, token_expiration):\n        self.firstname = firstname\n        self.lastname = lastname\n        self.username = username\n        self.password = password\n        self.roles = roles\n        self.domains = domains\n        self.token = token\n        self.token_expiration = token_expiration\n\n    pass\n\n\n# TODO: this is temporary and will be refactored\nclass DsipGwgroup2LB(object):\n    pass\n\nclass DsipSettings(OrderedDict):\n    \"\"\"\n    Identifies the contained data as already formatted for the table\n    \"\"\"\n    pass\n\n\n# TODO: switch to sqlalchemy.engine.URL API\ndef createDBURI(db_driver=None, db_type=None, db_user=None, db_pass=None, db_host=None, db_port=None, db_name=None, db_charset='utf8mb4'):\n    \"\"\"\n    Get any and all DB Connection URI's\n    Facilitates HA DB Server connections through multiple host's defined in settings\n    :return:    list of DB URI connection strings\n    \"\"\"\n    uri_list = []\n\n    # URI is built from the following by order of precedence:\n    # 1: function arguments\n    # 2: environment variables (debug mode only)\n    # 3: settings file\n    if db_driver is None:\n        db_driver = os.getenv('KAM_DB_DRIVER', settings.KAM_DB_DRIVER) if settings.DEBUG else settings.KAM_DB_DRIVER\n    if db_type is None:\n        db_type = os.getenv('KAM_DB_TYPE', settings.KAM_DB_TYPE) if settings.DEBUG else settings.KAM_DB_TYPE\n    if db_user is None:\n        db_user = os.getenv('KAM_DB_USER', settings.KAM_DB_USER) if settings.DEBUG else settings.KAM_DB_USER\n    if db_pass is None:\n        db_pass = os.getenv('KAM_DB_PASS', settings.KAM_DB_PASS) if settings.DEBUG else settings.KAM_DB_PASS\n    if db_host is None:\n        db_host = os.getenv('KAM_DB_HOST', settings.KAM_DB_HOST) if settings.DEBUG else settings.KAM_DB_HOST\n    if db_port is None:\n        db_port = os.getenv('KAM_DB_PORT', settings.KAM_DB_PORT) if settings.DEBUG else settings.KAM_DB_PORT\n    if db_name is None:\n        db_name = os.getenv('KAM_DB_NAME', settings.KAM_DB_NAME) if settings.DEBUG else settings.KAM_DB_NAME\n\n    # need to decrypt password\n    if isinstance(db_pass, bytes):\n        db_pass = AES_CTR.decrypt(db_pass)\n    # formatting for driver\n    if len(db_driver) > 0:\n        db_driver = '+{}'.format(db_driver)\n    # string template\n    db_uri_str = db_type + db_driver + \"://\" + db_user + \":\" + db_pass + \"@\" + \"{host}\" + \":\" + db_port + \"/\" + db_name + \"?charset=\" + db_charset\n    # for cluster of DB add all hosts\n    if isinstance(db_host, list):\n        for host in db_host:\n            uri_list.append(db_uri_str.format(host=host))\n    else:\n        uri_list.append(db_uri_str.format(host=db_host))\n\n    if settings.DEBUG:\n        IO.printdbg('createDBURI() returned: [{}]'.format(','.join('\"{0}\"'.format(uri) for uri in uri_list)))\n\n    return uri_list\n\n\ndef createValidEngine(uri_list):\n    \"\"\"\n    Create DB engine if connection is valid\n    Attempts each uri in the list until a valid connection is made\n    This method uses a singleton pattern and returns db_engine if created\n\n    :param uri_list:    list of connection uri's\n    :return:            DB engine object\n    :raise:             SQLAlchemyError if all connections fail\n    \"\"\"\n\n    # globals from the top-level module\n    caller_globals = dict(inspect.getmembers(inspect.stack()[-1][0]))[\"f_globals\"]\n\n    if DB_ENGINE_NAME in caller_globals:\n        return caller_globals[DB_ENGINE_NAME]\n\n    errors = []\n\n    for conn_uri in uri_list:\n        try:\n            db_engine = create_engine(conn_uri,\n                echo=settings.DEBUG,\n                echo_pool=settings.DEBUG,\n                pool_recycle=300,\n                pool_size=10,\n                isolation_level=\"READ UNCOMMITTED\",\n                connect_args={\"connect_timeout\": 5})\n            # test connection\n            _ = db_engine.connect()\n            # conn good return it\n            return db_engine\n        except Exception as ex:\n            errors.append(ex)\n\n    # we failed to return good connection raise exceptions\n    if settings.DEBUG:\n        for ex in errors:\n            debugException(ex)\n\n    try:\n        raise sql_exceptions.SQLAlchemyError(errors)\n    except:\n        raise Exception(errors)\n\n\ndef startSession():\n    \"\"\"\n    This method uses a singleton pattern to grab the global session loader and start a session\n    \"\"\"\n\n    # globals from the top-level module\n    caller_globals = dict(inspect.getmembers(inspect.stack()[-1][0]))[\"f_globals\"]\n\n    if SESSION_LOADER_NAME in caller_globals:\n        return caller_globals[SESSION_LOADER_NAME]()\n\n    db_engine, session_loader = createSessionObjects()\n    return session_loader()\n\n\ndef createSessionObjects():\n    \"\"\"\n    Create the DB engine and session factory\n\n    :return:    Session factory and DB Engine\n    :rtype:     (:class:`sqlalchemy.orm.Session`,:class:`sqlalchemy.engine.Engine`)\n    \"\"\"\n\n    db_engine = createValidEngine(createDBURI())\n\n    mapper = registry(metadata=MetaData(schema=db_engine.url.database))\n\n    dr_gateways = Table('dr_gateways', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    address = Table('address', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    outboundroutes = Table('dr_rules', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    inboundmapping = Table('dr_rules', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    subscriber = Table('subscriber', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    dsip_domain_mapping = Table('dsip_domain_mapping', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    dsip_multidomain_mapping = Table('dsip_multidomain_mapping', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    # fusionpbx_mappings = Table('dsip_fusionpbx_mappings', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    dsip_lcr = Table('dsip_lcr', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    uacreg = Table('uacreg', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    dr_gw_lists = Table('dr_gw_lists', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    # dr_groups = Table('dr_groups', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    domain = Table('domain', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    domain_attrs = Table('domain_attrs', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    dispatcher = Table('dispatcher', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    dsip_endpoint_lease = Table('dsip_endpoint_lease', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    dsip_maintmode = Table('dsip_maintmode', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    dsip_call_settings = Table('dsip_call_settings', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    dsip_notification = Table('dsip_notification', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    dsip_hardfwd = Table('dsip_hardfwd', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    dsip_failfwd = Table('dsip_failfwd', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    dsip_cdrinfo = Table('dsip_cdrinfo', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    dsip_certificates = Table('dsip_certificates', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    dsip_dnid_enrichment = Table('dsip_dnid_enrich_lnp', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    dsip_user = Table('dsip_user', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n    # TODO: this is temporary and will be refactored\n    dsip_gwgroup2lb = Table('dsip_gwgroup2lb', mapper.metadata, autoload_replace=True, autoload_with=db_engine)\n\n    # dr_gw_lists_alias = select([\n    #     dr_gw_lists.c.id.label(\"drlist_id\"),\n    #     dr_gw_lists.c.gwlist,\n    #     dr_gw_lists.c.description.label(\"drlist_description\"),\n    # ]).correlate(None).alias()\n    # gw_join = join(dr_gw_lists_alias, dr_groups,\n    #                dr_gw_lists_alias.c.drlist_id == dr_groups.c.id,\n    #                dr_gw_lists_alias.c.drlist_description == dr_groups.c.description)\n\n    mapper.map_imperatively(Gateways, dr_gateways)\n    mapper.map_imperatively(Address, address)\n    mapper.map_imperatively(InboundMapping, inboundmapping)\n    mapper.map_imperatively(OutboundRoutes, outboundroutes)\n    mapper.map_imperatively(dSIPDomainMapping, dsip_domain_mapping)\n    mapper.map_imperatively(dSIPMultiDomainMapping, dsip_multidomain_mapping)\n    mapper.map_imperatively(Subscribers, subscriber)\n    # mapper.map_imperatively(CustomRouting, customrouting)\n    mapper.map_imperatively(dSIPLCR, dsip_lcr)\n    mapper.map_imperatively(UAC, uacreg)\n    mapper.map_imperatively(GatewayGroups, dr_gw_lists)\n    mapper.map_imperatively(Domain, domain)\n    mapper.map_imperatively(DomainAttrs, domain_attrs)\n    mapper.map_imperatively(Dispatcher, dispatcher)\n    mapper.map_imperatively(dSIPLeases, dsip_endpoint_lease)\n    mapper.map_imperatively(dSIPMaintModes, dsip_maintmode)\n    mapper.map_imperatively(dSIPCallSettings, dsip_call_settings)\n    mapper.map_imperatively(dSIPNotification, dsip_notification)\n    mapper.map_imperatively(dSIPHardFwd, dsip_hardfwd)\n    mapper.map_imperatively(dSIPFailFwd, dsip_failfwd)\n    mapper.map_imperatively(dSIPCDRInfo, dsip_cdrinfo)\n    mapper.map_imperatively(dSIPCertificates, dsip_certificates)\n    mapper.map_imperatively(dSIPDNIDEnrichment, dsip_dnid_enrichment)\n    mapper.map_imperatively(dSIPUser, dsip_user)\n    # TODO: this is temporary and will be refactored\n    mapper.map_imperatively(DsipGwgroup2LB, dsip_gwgroup2lb)\n\n    # mapper.map_imperatively(GatewayGroups, gw_join, properties={\n    #     'id': [dr_groups.c.id, dr_gw_lists_alias.c.drlist_id],\n    #     'description': [dr_groups.c.description, dr_gw_lists_alias.c.drlist_description],\n    # })\n\n    session_loader = scoped_session(sessionmaker(bind=db_engine))\n\n    # load them into the top level module global namespace\n    caller_globals = dict(inspect.getmembers(inspect.stack()[-1][0]))[\"f_globals\"]\n    caller_globals[DB_ENGINE_NAME] = db_engine\n    caller_globals[SESSION_LOADER_NAME] = session_loader\n\n    # return references for the calling function\n    return caller_globals[DB_ENGINE_NAME], caller_globals[SESSION_LOADER_NAME]\n\n\n# TODO: change to the global define pattern instead of instantiating dummy objects\nclass DummySession():\n    \"\"\"\n    Sole purpose is to avoid exceptions when startSession fails\n    This allows us to handle exceptions later in the try blocks\n    We also avoid exceptions in the except blocks by using dummy sesh\n    \"\"\"\n\n    @staticmethod\n    def __contains__(self, *args, **kwargs):\n        pass\n\n    def __iter__(self, *args, **kwargs):\n        pass\n\n    def add(self, *args, **kwargs):\n        pass\n\n    def add_all(self, *args, **kwargs):\n        pass\n\n    def begin(self, *args, **kwargs):\n        pass\n\n    def begin_nested(self, *args, **kwargs):\n        pass\n\n    def close(self, *args, **kwargs):\n        pass\n\n    def commit(self, *args, **kwargs):\n        pass\n\n    def connection(self, *args, **kwargs):\n        pass\n\n    def delete(self, *args, **kwargs):\n        pass\n\n    def execute(self, *args, **kwargs):\n        pass\n\n    def expire(self, *args, **kwargs):\n        pass\n\n    def expire_all(self, *args, **kwargs):\n        pass\n\n    def expunge(self, *args, **kwargs):\n        pass\n\n    def expunge_all(self, *args, **kwargs):\n        pass\n\n    def flush(self, *args, **kwargs):\n        pass\n\n    def get_bind(self, *args, **kwargs):\n        pass\n\n    def is_modified(self, *args, **kwargs):\n        pass\n\n    def bulk_save_objects(self, *args, **kwargs):\n        pass\n\n    def bulk_insert_mappings(self, *args, **kwargs):\n        pass\n\n    def bulk_update_mappings(self, *args, **kwargs):\n        pass\n\n    def merge(self, *args, **kwargs):\n        pass\n\n    def query(self, *args, **kwargs):\n        pass\n\n    def refresh(self, *args, **kwargs):\n        pass\n\n    def rollback(self, *args, **kwargs):\n        pass\n\n    def scalar(self, *args, **kwargs):\n        pass\n\n    def remove(self, *args, **kwargs):\n        pass\n\n    def configure(self, *args, **kwargs):\n        pass\n\n    def query_property(self, *args, **kwargs):\n        pass\n\n\ndef settingsToTableFormat(settings, updates=None):\n    data = objToDict(settings)\n    if updates is not None:\n        data.update(updates)\n\n    # translate db specific fields\n    if isinstance(data['KAM_DB_HOST'], (list, tuple)):\n        data['KAM_DB_HOST'] = ','.join(data['KAM_DB_HOST'])\n    data['DSIP_LICENSE_STORE'] = base64.b64encode(bson.dumps(data['DSIP_LICENSE_STORE']))\n\n    # order matters here, as this is used to update table settings as well\n    return DsipSettings([\n        ('DSIP_ID', data['DSIP_ID']),\n        ('DSIP_CLUSTER_ID', data['DSIP_CLUSTER_ID']),\n        ('DSIP_CLUSTER_SYNC', data['DSIP_CLUSTER_SYNC']),\n        ('DSIP_PROTO', data['DSIP_PROTO']),\n        ('DSIP_PORT', data['DSIP_PORT']),\n        ('DSIP_USERNAME', data['DSIP_USERNAME']),\n        ('DSIP_PASSWORD', data['DSIP_PASSWORD']),\n        ('DSIP_IPC_PASS', data['DSIP_IPC_PASS']),\n        ('DSIP_API_PROTO', data['DSIP_API_PROTO']),\n        ('DSIP_API_PORT', data['DSIP_API_PORT']),\n        ('DSIP_PRIV_KEY', data['DSIP_PRIV_KEY']),\n        ('DSIP_PID_FILE', data['DSIP_PID_FILE']),\n        ('DSIP_UNIX_SOCK', data['DSIP_UNIX_SOCK']),\n        ('DSIP_IPC_SOCK', data['DSIP_IPC_SOCK']),\n        ('DSIP_API_TOKEN', data['DSIP_API_TOKEN']),\n        ('DSIP_LOG_LEVEL', data['DSIP_LOG_LEVEL']),\n        ('DSIP_LOG_FACILITY', data['DSIP_LOG_FACILITY']),\n        ('DSIP_SSL_KEY', data['DSIP_SSL_KEY']),\n        ('DSIP_SSL_CERT', data['DSIP_SSL_CERT']),\n        ('DSIP_SSL_CA', data['DSIP_SSL_CA']),\n        ('DSIP_SSL_EMAIL', data['DSIP_SSL_EMAIL']),\n        ('DSIP_CERTS_DIR', data['DSIP_CERTS_DIR']),\n        ('VERSION', data['VERSION']),\n        ('DEBUG', data['DEBUG']),\n        ('ROLE', data['ROLE']),\n        ('GUI_INACTIVE_TIMEOUT', data['GUI_INACTIVE_TIMEOUT']),\n        ('KAM_DB_HOST', data['KAM_DB_HOST']),\n        ('KAM_DB_DRIVER', data['KAM_DB_DRIVER']),\n        ('KAM_DB_TYPE', data['KAM_DB_TYPE']),\n        ('KAM_DB_PORT', data['KAM_DB_PORT']),\n        ('KAM_DB_NAME', data['KAM_DB_NAME']),\n        ('KAM_DB_USER', data['KAM_DB_USER']),\n        ('KAM_DB_PASS', data['KAM_DB_PASS']),\n        ('KAM_KAMCMD_PATH', data['KAM_KAMCMD_PATH']),\n        ('KAM_CFG_PATH', data['KAM_CFG_PATH']),\n        ('KAM_TLSCFG_PATH', data['KAM_TLSCFG_PATH']),\n        ('RTP_CFG_PATH', data['RTP_CFG_PATH']),\n        ('FLT_CARRIER', data['FLT_CARRIER']),\n        ('FLT_PBX', data['FLT_PBX']),\n        ('FLT_MSTEAMS', data['FLT_MSTEAMS']),\n        ('FLT_OUTBOUND', data['FLT_OUTBOUND']),\n        ('FLT_INBOUND', data['FLT_INBOUND']),\n        ('FLT_LCR_MIN', data['FLT_LCR_MIN']),\n        ('FLT_FWD_MIN', data['FLT_FWD_MIN']),\n        ('DEFAULT_AUTH_DOMAIN', data['DEFAULT_AUTH_DOMAIN']),\n        ('TELEBLOCK_GW_ENABLED', data['TELEBLOCK_GW_ENABLED']),\n        ('TELEBLOCK_GW_IP', data['TELEBLOCK_GW_IP']),\n        ('TELEBLOCK_GW_PORT', data['TELEBLOCK_GW_PORT']),\n        ('TELEBLOCK_MEDIA_IP', data['TELEBLOCK_MEDIA_IP']),\n        ('TELEBLOCK_MEDIA_PORT', data['TELEBLOCK_MEDIA_PORT']),\n        ('FLOWROUTE_ACCESS_KEY', data['FLOWROUTE_ACCESS_KEY']),\n        ('FLOWROUTE_SECRET_KEY', data['FLOWROUTE_SECRET_KEY']),\n        ('FLOWROUTE_API_ROOT_URL', data['FLOWROUTE_API_ROOT_URL']),\n        ('HOMER_ID', data['HOMER_ID']),\n        ('HOMER_HEP_HOST', data['HOMER_HEP_HOST']),\n        ('HOMER_HEP_PORT', data['HOMER_HEP_PORT']),\n        ('NETWORK_MODE', data['NETWORK_MODE']),\n        ('IPV6_ENABLED', data['IPV6_ENABLED']),\n        ('INTERNAL_IP_ADDR', data['INTERNAL_IP_ADDR']),\n        ('INTERNAL_IP_NET', data['INTERNAL_IP_NET']),\n        ('INTERNAL_IP6_ADDR', data['INTERNAL_IP6_ADDR']),\n        ('INTERNAL_IP6_NET', data['INTERNAL_IP6_NET']),\n        ('INTERNAL_FQDN', data['INTERNAL_FQDN']),\n        ('EXTERNAL_IP_ADDR', data['EXTERNAL_IP_ADDR']),\n        ('EXTERNAL_IP6_ADDR', data['EXTERNAL_IP6_ADDR']),\n        ('EXTERNAL_FQDN', data['EXTERNAL_FQDN']),\n        ('PUBLIC_IFACE', data['PUBLIC_IFACE']),\n        ('PRIVATE_IFACE', data['PRIVATE_IFACE']),\n        ('UPLOAD_FOLDER', data['UPLOAD_FOLDER']),\n        ('MAIL_SERVER', data['MAIL_SERVER']),\n        ('MAIL_PORT', data['MAIL_PORT']),\n        ('MAIL_USE_TLS', data['MAIL_USE_TLS']),\n        ('MAIL_USERNAME', data['MAIL_USERNAME']),\n        ('MAIL_PASSWORD', data['MAIL_PASSWORD']),\n        ('MAIL_ASCII_ATTACHMENTS', data['MAIL_ASCII_ATTACHMENTS']),\n        ('MAIL_DEFAULT_SENDER', data['MAIL_DEFAULT_SENDER']),\n        ('MAIL_DEFAULT_SUBJECT', data['MAIL_DEFAULT_SUBJECT']),\n        ('DSIP_LICENSE_STORE', data['DSIP_LICENSE_STORE']),\n        ('RTPENGINE_URI', data['RTPENGINE_URI']),\n    ])\n\n\ndef settingsTableToDict(table_values, updates=None):\n    if updates is not None:\n        table_values.update(updates)\n\n    if ',' in table_values['KAM_DB_HOST']:\n        table_values['KAM_DB_HOST'] = table_values['KAM_DB_HOST'].split(',')\n    table_values['DSIP_LICENSE_STORE'] = bson.loads(base64.b64decode(table_values['DSIP_LICENSE_STORE']))\n    return table_values\n\n\ndef updateDsipSettingsTable(fields):\n    \"\"\"\n    Update the dsip_settings table using our stored procedure\n\n    :param fields:  columns/values to update\n    :type fields:   dict\n    :return:        None\n    :rtype:         None\n    :raises:        sql_exceptions.SQLAlchemyError\n    \"\"\"\n\n    db = DummySession()\n    try:\n        if isinstance(fields, DsipSettings):\n            db_fields = fields\n        else:\n            db_fields = settingsToTableFormat(settings, updates=fields)\n        field_mapping = ', '.join([':{}'.format(x, x) for x in db_fields.keys()])\n\n        db = startSession()\n        db.execute(\n            text('CALL update_dsip_settings({})'.format(field_mapping)),\n            params=db_fields\n        )\n        db.commit()\n    except sql_exceptions.SQLAlchemyError:\n        db.rollback()\n        db.flush()\n        raise\n    finally:\n        db.close()\n\n\ndef getDsipSettingsTableAsDict(dsip_id, updates=None):\n    db = DummySession()\n    try:\n        db = startSession()\n        data = rowToDict(\n            db.execute(\n                text('SELECT * FROM dsip_settings WHERE DSIP_ID=:dsip_id'),\n                params={'dsip_id': dsip_id}\n            ).first()\n        )\n        # translate db specific fields\n        return settingsTableToDict(data, updates=updates)\n    except sql_exceptions.SQLAlchemyError:\n        raise\n    finally:\n        db.close()\n"
  },
  {
    "path": "gui/dsiprouter.py",
    "content": "#!/opt/dsiprouter/venv/bin/python\n\n# make sure the generated source files are imported instead of the template ones\nimport sys\n\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\n# all of our standard and project file imports\nimport os, json, urllib.parse, glob, datetime, csv, logging, signal, bjoern, secrets, subprocess, time\nimport importlib.util\nfrom ansi2html import Ansi2HTMLConverter\nfrom copy import copy\nfrom importlib import reload\nfrom flask import Flask, render_template, request, redirect, flash, session, url_for, send_from_directory, Blueprint, Response\nfrom flask_wtf.csrf import CSRFProtect\nfrom itsdangerous import URLSafeTimedSerializer\nfrom pygtail import Pygtail\nfrom sqlalchemy import exc as sql_exceptions, Integer\nfrom sqlalchemy.orm import load_only\nfrom sqlalchemy.sql import text, func, select\nfrom sqlalchemy.orm.session import close_all_sessions\nfrom werkzeug import exceptions as http_exceptions\nfrom werkzeug.utils import secure_filename\nfrom werkzeug.middleware.proxy_fix import ProxyFix\nfrom sysloginit import initSyslogLogger\nfrom shared import updateConfig, getCustomRoutes, debugException, debugEndpoint, \\\n    stripDictVals, strFieldsToDict, dictToStrFields, allowed_file, showError, IO, objToDict, StatusCodes\nfrom util.networking import safeUriToHost, safeFormatSipUri, safeStripPort\nfrom database import DummySession, createSessionObjects, startSession, settingsTableToDict, \\\n    DB_ENGINE_NAME, SESSION_LOADER_NAME, settingsToTableFormat, getDsipSettingsTableAsDict, \\\n    Gateways, Address, InboundMapping, OutboundRoutes, Subscribers, dSIPLCR, UAC, GatewayGroups, \\\n    Domain, DomainAttrs, dSIPMultiDomainMapping, dSIPHardFwd, dSIPFailFwd, updateDsipSettingsTable, \\\n    Dispatcher, DsipGwgroup2LB\nfrom modules import flowroute\nfrom modules.domain.domain_routes import domains\nfrom modules.api.api_routes import api\nfrom modules.api.mediaserver.routes import mediaserver\nfrom modules.api.carriergroups.routes import carriergroups, addCarrierGroups\nfrom modules.api.carriergroups.functions import addUpdateCarriers, displayCarrierGroups, displayCarriers\nfrom modules.api.kamailio.functions import reloadKamailio\nfrom modules.api.licensemanager.classes import WoocommerceError\nfrom modules.api.licensemanager.functions import licenseDictToStateDict, getLicenseStatusFromStateDict, \\\n    getLicenseStatus\nfrom modules.api.licensemanager.routes import license_manager\nfrom modules.api.auth.routes import user\nfrom util.security import Credentials, urandomChars, AES_CTR\nfrom util.ipc import SETTINGS_SHMEM_NAME, STATE_SHMEM_NAME, createSharedMemoryDict, getSharedMemoryDict\nfrom util.parse_json import CreateEncoder\nfrom util.persistence import updatePersistentState, setPersistentState\nfrom util.pyasync import process\nfrom modules.upgrade import UpdateUtils\nimport settings\n\n# TODO: unit testing per component\n# TODO: many of these routes could use some updating...\n#       possibly look into this as well when reworking the architecture for API\n# TODO: do license checks on login and store in session variables\n#       this alleviates the extra requests and can be updated easily\n#       marked for implementation in v0.80\n# TODO: move settings to read/write from shared memory instead of using module replacement\n#       marked for implementation in v0.80\n# TODO: go through templates/routes and replace redundant references to settings/globals\n#       they are both injected on every request via the context processor\n# TODO: remove references to \"ipc.sock\" / DSIP_IPC_SOCK\n#       we moved to shared memory instead of domain sockets for sharing state across processes\n\n# module constants\n# TODO: create /var/log/dsiprouter/ and move there\nUPGRADE_LOG = f'{settings.BACKUP_FOLDER}/upgrade.log'\nUPGRADE_OFFSET = f'{UPGRADE_LOG}.offset'\n\n# module variables\napp = Flask(__name__, static_folder=\"./static\", static_url_path=\"/static\")\napp.register_blueprint(domains)\napp.register_blueprint(api)\napp.register_blueprint(mediaserver)\napp.register_blueprint(carriergroups)\napp.register_blueprint(user)\napp.register_blueprint(license_manager)\napp.register_blueprint(Blueprint('docs', 'docs', static_url_path='/docs', static_folder=settings.DSIP_DOCS_DIR))\ncsrf = CSRFProtect(app)\ncsrf.exempt(api)\ncsrf.exempt(mediaserver)\ncsrf.exempt(carriergroups)\ncsrf.exempt(user)\ncsrf.exempt(license_manager)\nnumbers_api = flowroute.Numbers()\nansi_converter = Ansi2HTMLConverter(inline=True)\nauth_modules = []\n\n\n@app.before_first_request\ndef before_first_request():\n    # replace werkzeug and sqlalchemy loggers\n    log_handler = initSyslogLogger()\n    replaceAppLoggers(log_handler)\n\n\n@app.before_request\ndef before_request():\n    # set the session lifetime to gui inactive timeout\n    # if we are in debug mode force it to last for entire day\n    session.permanent = True\n    if settings.DEBUG:\n        app.permanent_session_lifetime = datetime.timedelta(days=1)\n    else:\n        app.permanent_session_lifetime = datetime.timedelta(minutes=settings.GUI_INACTIVE_TIMEOUT)\n    session.modified = True\n\n\n# DEPRECATED: overridden by csrf.exempt(api), need to revist this, marked for review in v0.80\n@api.before_request\ndef api_before_request():\n    # for ua to api w/ api token disable csrf\n    # we only want csrf checks on service to service requests\n    if 'Authorization' not in request.headers:\n        csrf.protect()\n\n\n@app.route('/')\ndef index():\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        if not session.get('logged_in'):\n            return render_template('index.html', version=settings.VERSION)\n        else:\n            action = request.args.get('action')\n            return render_template('dashboard.html', show_add_onload=action, version=settings.VERSION)\n\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        return showError(type=error)\n\n\n@app.route('/error')\ndef displayError():\n    type = request.args.get(\"type\", \"\")\n    code = int(request.args.get(\"code\", StatusCodes.HTTP_INTERNAL_SERVER_ERROR))\n    msg = request.args.get(\"msg\", None)\n    return showError(type, code, msg)\n\n\n@app.route('/backupandrestore')\ndef backupandrestore():\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        if not session.get('logged_in'):\n            return render_template('index.html', version=settings.VERSION)\n        else:\n            action = request.args.get('action')\n            return render_template('backupandrestore.html', show_add_onload=action, version=settings.VERSION)\n\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex, log_ex=False, print_ex=True, showstack=False)\n        error = \"server\"\n        return showError(type=error)\n\n\n@app.route('/certificates')\ndef certificates():\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        if not session.get('logged_in'):\n            return render_template('index.html', version=settings.VERSION)\n        else:\n            action = request.args.get('action')\n            return render_template('certificates.html', show_add_onload=action, version=settings.VERSION)\n\n\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex, log_ex=False, print_ex=True, showstack=False)\n        error = \"server\"\n        return showError(type=error)\n\n\n@app.route('/favicon.ico')\ndef favicon():\n    return send_from_directory(os.path.join(app.root_path, 'static'),\n        'favicon.ico', mimetype='image/vnd.microsoft.icon')\n\n\n@app.route('/login', methods=['GET', 'POST'])\ndef login():\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        # redirect GET requests to index\n        if request.method == 'GET':\n            return redirect(url_for('index'))\n\n        form = stripDictVals(request.form.to_dict())\n        if 'username' not in form or 'password' not in form:\n            raise http_exceptions.BadRequest('Username and Password are required')\n        elif not isinstance(form['username'], str) or not isinstance(form['password'], str):\n            raise http_exceptions.BadRequest('Username or Password is malformed')\n\n        # Get Environment Variables if in debug mode\n        # This is the only case we allow plain text password comparison\n        if settings.DEBUG:\n            settings.DSIP_USERNAME = os.getenv('DSIP_USERNAME', settings.DSIP_USERNAME)\n            settings.DSIP_PASSWORD = os.getenv('DSIP_PASSWORD', settings.DSIP_PASSWORD)\n\n        # if username valid, hash password and compare with stored password\n        if form['username'] == settings.DSIP_USERNAME:\n            if isinstance(settings.DSIP_PASSWORD, bytes):\n                pwcheck = Credentials.hashCreds(form['password'], settings.DSIP_PASSWORD[-(Credentials.SALT_LEN * 2):])\n            else:\n                pwcheck = form['password']\n\n            if secrets.compare_digest(pwcheck, settings.DSIP_PASSWORD):\n                session['logged_in'] = True\n                session['username'] = form['username']\n                return redirect(url_for('index'))\n       \n        # Check for user in other auth modules\n        for auth_mod in auth_modules:\n            if auth_mod.authenticate(form['username'], form['password']):\n                session['logged_in'] = True\n                session['username'] = form['username']\n                return redirect(url_for('index'))\n\n        # if we got here auth failed\n        flash('Wrong Username or Password')\n        return redirect(url_for('index'))\n\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        return showError(type=error)\n\n\n@app.route('/logout')\ndef logout():\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        session.pop('logged_in', None)\n        session.pop('username', None)\n        return redirect(url_for('index'))\n\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        return showError(type=error)\n\n\napp.add_url_rule('/carriergroups', view_func=displayCarrierGroups, methods=['GET'])\napp.add_url_rule('/carriergroups/<int:gwgroup>', view_func=displayCarrierGroups, methods=['GET'])\n\n\n@app.route('/carriergroups', methods=['POST'])\ndef addUpdateCarrierGroups():\n    \"\"\"\n    Add or Update a group of carriers\n    \"\"\"\n\n    # TODO: toss this in a decorator func with error handling and use for gui auth checks\n    if not session.get('logged_in'):\n        return redirect(url_for('index'))\n\n    return addCarrierGroups()\n\n\n@app.route('/carriergroupdelete', methods=['POST'])\ndef deleteCarrierGroups():\n    \"\"\"\n    Delete a group of carriers\n    \"\"\"\n\n    db = DummySession()\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        form = stripDictVals(request.form.to_dict())\n\n        gwgroup = form['gwgroup']\n        gwlist = form['gwlist'] if 'gwlist' in form else ''\n\n        Gwgroup = db.query(GatewayGroups).filter(GatewayGroups.id == gwgroup)\n        gwgroup_row = Gwgroup.first()\n        if gwgroup_row is None:\n            return displayCarrierGroups()\n\n        db.query(Address).filter(\n            Address.tag.regexp_match(f'gwgroup:{gwgroup}(,|$)')\n        ).delete(synchronize_session=False)\n        db.query(UAC).filter(\n            UAC.l_uuid == gwgroup_row.id\n        ).delete(synchronize_session=False)\n        db.query(Dispatcher).filter(\n            Dispatcher.setid == gwgroup\n        ).delete(synchronize_session=False)\n        # validate this group has gateways assigned to it\n        if len(gwlist) > 0:\n            GWs = db.query(Gateways).filter(Gateways.gwid.in_(list(map(int, gwlist.split(\",\")))))\n            GWs.delete(synchronize_session=False)\n        Gwgroup.delete(synchronize_session=False)\n\n        db.commit()\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return displayCarrierGroups()\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        db.rollback()\n        db.flush()\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\n\napp.add_url_rule('/carriers', view_func=displayCarriers, methods=['GET'])\napp.add_url_rule('/carriers/<int:gwid>', view_func=displayCarriers, methods=['GET'])\napp.add_url_rule('/carriers/group/<int:gwgroup>', view_func=displayCarriers, methods=['GET'])\napp.add_url_rule('/carriers', view_func=addUpdateCarriers, methods=['POST'])\n\n\n@app.route('/carrierdelete', methods=['POST'])\ndef deleteCarriers():\n    \"\"\"\n    Delete a carrier\n    \"\"\"\n\n    db = DummySession()\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        form = stripDictVals(request.form.to_dict())\n\n        gwid = form['gwid']\n        hostname = form['ip_addr'] if len(form['ip_addr']) > 0 else ''\n        gwgroup = form['gwgroup'] if 'gwgroup' in form else ''\n        related_rules = json.loads(form['related_rules']) if 'related_rules' in form else ''\n\n        sip_addr = safeUriToHost(hostname, default_port=5060)\n\n        Gateway = db.query(Gateways).filter(Gateways.gwid == gwid)\n        Gateway_Row = Gateway.first()\n        gw_fields = strFieldsToDict(Gateway_Row.description)\n\n        # find associated gwgroup if not provided\n        if len(gwgroup) <= 0:\n            gwgroup = gw_fields['gwgroup'] if 'gwgroup' in gw_fields else ''\n\n        # remove associated address if exists\n        if 'addr_id' in gw_fields:\n            Addr = db.query(Address).filter(Address.id == gw_fields['addr_id'])\n            Addr.delete(synchronize_session=False)\n\n        # grab any related carrier groups\n        Gatewaygroups = db.execute(\n            text('SELECT * FROM dr_gw_lists WHERE FIND_IN_SET(:gwid, dr_gw_lists.gwlist)'),\n            {'gwid': gwid}\n        )\n\n        # remove gateway\n        Gateway.delete(synchronize_session=False)\n\n        # remove from carrier from dispatcher\n        db.query(Dispatcher).filter(\n            Dispatcher.description.regexp_match(f'gwid={gwid}(;|$)')\n        ).delete(synchronize_session=False)\n\n        # remove carrier from gwlist in carrier group\n        for Gatewaygroup in Gatewaygroups:\n            gwlist = list(filter(None, Gatewaygroup[1].split(\",\")))\n            gwlist.remove(gwid)\n            db.query(GatewayGroups).filter(GatewayGroups.id == str(Gatewaygroup[0])).update(\n                {'gwlist': ','.join(gwlist)}, synchronize_session=False)\n\n        # remove carrier from gwlist in carrier rules\n        if len(related_rules) > 0:\n            rule_ids = related_rules.keys()\n            rules = db.query(OutboundRoutes).filter(OutboundRoutes.ruleid.in_(rule_ids)).all()\n            for rule in rules:\n                gwlist = list(filter(None, rule.gwlist.split(\",\")))\n                gwlist.remove(gwid)\n                rule.gwlist = ','.join(gwlist)\n\n        db.commit()\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return displayCarriers(gwgroup=gwgroup)\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        db.rollback()\n        db.flush()\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\n\n@app.route('/endpointgroups')\ndef displayEndpointGroups():\n    \"\"\"\n    Display Endpoint Groups / Endpoints in the view\n    \"\"\"\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        # NOTE: not including IPv6 address here as we only need to connect over IPv4 to fusion DB\n        return render_template('endpointgroups.html', dsiprouter_ip=settings.EXTERNAL_IP_ADDR,\n            DEFAULT_auth_domain=settings.DEFAULT_AUTH_DOMAIN)\n\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        return showError(type=error)\n\n\n@app.route('/numberenrichment')\ndef displayNumberEnrichment():\n    \"\"\"\n    Display Endpoint Groups / Endpoints in the view\n    \"\"\"\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        return render_template('dnid_enrichment.html')\n\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        return showError(type=error)\n\n\n@app.route('/cdrs')\ndef displayCDRS():\n    \"\"\"\n    Display Endpoint Groups / Endpoints in the view\n    \"\"\"\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        license_status = getLicenseStatus(license_tag='DSIP_CORE')\n        if license_status == 0:\n            return render_template('license_required.html', msg='DSIP_CORE license is required to use this feature')\n\n        if license_status == 1:\n            return render_template('license_required.html', msg='license is not valid, ensure your license is still active')\n\n        if license_status == 2:\n            return render_template('license_required.html', msg='license is associated with another machine, re-associate it with this machine first')\n\n\n        return render_template('cdrs.html')\n\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        return showError(type=error)\n\n\n@app.route('/licensing')\ndef displayLicenseManager():\n    \"\"\"\n    Display License Manager page\n    \"\"\"\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        return render_template('license_manager.html')\n\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        return showError(type=error)\n\n\n# TODO: why are we doing this weird api workaround, instead of making the gui and api separate??\n@app.route('/endpointgroups', methods=['POST'])\ndef addUpdateEndpointGroups():\n    \"\"\"\n    Add or Update a PBX / Endpoint\n    \"\"\"\n\n    db = DummySession()\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        # form = stripDictVals(request.form.to_dict())\n        #\n        # TODO: change ip_addr field to hostname or domainname, we already support this\n        # gwid = form['gwid']\n        # gwgroupid = request.form.get(\"gwgroup\", None)\n        name = request.form.get(\"name\", None)\n        # ip_addr = request.form.get(\"ip_addr\", None)\n        # strip = form['strip'] if len(form['strip']) > 0 else \"0\"\n        # prefix = form['prefix'] if len(form['prefix']) > 0 else \"\"\n        # authtype = form['authtype']\n        # fusionpbx_db_server = form['fusionpbx_db_server']\n        # fusionpbx_db_username = form['fusionpbx_db_username']\n        # fusionpbx_db_password = form['fusionpbx_db_password']\n        # auth_username = form['auth_username']\n        # auth_password = form['auth_password']\n        # auth_domain = form['auth_domain'] if len(form['auth_domain']) > 0 else settings.DEFAULT_AUTH_DOMAIN\n        # calllimit = form['calllimit'] if len(form['calllimit']) > 0 else \"\"\n        #\n        # multi_tenant_domain_enabled = False\n        # multi_tenant_domain_type = dSIPMultiDomainMapping.FLAGS.TYPE_UNKNOWN.value\n        # single_tenant_domain_enabled = False\n        # single_tenant_domain_type = dSIPDomainMapping.FLAGS.TYPE_UNKNOWN.value\n\n        if name is not None:\n            Gwgroup = GatewayGroups(name, type=settings.FLT_PBX)\n            db.add(Gwgroup)\n            db.commit()\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return displayEndpointGroups()\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        db.rollback()\n        db.flush()\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\n\n# TODO: is this route still in use?\n@app.route('/pbxdelete', methods=['POST'])\ndef deletePBX():\n    \"\"\"\n    Delete a PBX / Endpoint\n    \"\"\"\n\n    db = DummySession()\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        form = stripDictVals(request.form.to_dict())\n\n        gwid = form['gwid']\n        name = form['name']\n\n        gateway = db.query(Gateways).filter(Gateways.gwid == gwid)\n        gateway.delete(synchronize_session=False)\n        address = db.query(Address).filter(\n            Address.tag.regexp_match(f'name:{name}(,|$)')\n        )\n        address.delete(synchronize_session=False)\n        subscriber = db.query(Subscribers).filter(Subscribers.rpid == gwid)\n        subscriber.delete(synchronize_session=False)\n        address.delete(synchronize_session=False)\n\n        domainmultimapping = db.query(dSIPMultiDomainMapping).filter(dSIPMultiDomainMapping.pbx_id == gwid)\n        res = domainmultimapping.options(load_only(\"domain_list\", \"attr_list\")).first()\n        if res is not None:\n            # make sure mapping has domains\n            if len(res.domain_list) > 0:\n                domains = list(map(int, filter(None, res.domain_list.split(\",\"))))\n                domain = db.query(Domain).filter(Domain.id.in_(domains))\n                domain.delete(synchronize_session=False)\n            # make sure mapping has attributes\n            if len(res.attr_list) > 0:\n                attrs = list(map(int, filter(None, res.attr_list.split(\",\"))))\n                domainattrs = db.query(DomainAttrs).filter(DomainAttrs.id.in_(attrs))\n                domainattrs.delete(synchronize_session=False)\n\n            # delete the entry in the multi domain mapping table\n            domainmultimapping.delete(synchronize_session=False)\n\n        db.commit()\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return displayEndpointGroups()\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        db.rollback()\n        db.flush()\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\n\n@app.route('/inboundmapping')\ndef displayInboundMapping():\n    \"\"\"\n    Displaying of dr_rules table (inbound routes only)\\n\n    Partially Supports rule definitions for Kamailio Drouting module\\n\n    Documentation: `Drouting module <https://kamailio.org/docs/modules/4.4.x/modules/drouting.html>`_\n    \"\"\"\n\n    db = DummySession()\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        res = db.execute(\n            text(\"\"\"\nSELECT * FROM (\n    SELECT r.ruleid, r.groupid, r.prefix, r.gwlist, r.description AS rule_description, g.id AS gwgroupid, g.description AS gwgroup_description FROM dr_rules AS r LEFT JOIN dr_gw_lists AS g ON g.id = REPLACE(r.gwlist, '#', '') WHERE r.groupid = :flt_inbound\n) AS t1 LEFT JOIN (\n    SELECT hf.dr_ruleid AS hf_ruleid, hf.dr_groupid AS hf_groupid, hf.did AS hf_fwddid, g.id AS hf_gwgroupid FROM dsip_hardfwd AS hf LEFT JOIN dr_rules AS r ON hf.dr_groupid = r.groupid LEFT JOIN dr_gw_lists AS g ON g.id = REPLACE(r.gwlist, '#', '') WHERE hf.dr_groupid <> :flt_outbound\n    UNION ALL\n    SELECT hf.dr_ruleid AS hf_ruleid, hf.dr_groupid AS hf_groupid, hf.did AS hf_fwddid, NULL AS hf_gwgroupid FROM dsip_hardfwd AS hf LEFT JOIN dr_rules AS r ON hf.dr_ruleid = r.ruleid WHERE hf.dr_groupid = :flt_outbound\n) AS t2 ON t1.ruleid = t2.hf_ruleid LEFT JOIN (\n    SELECT ff.dr_ruleid AS ff_ruleid, ff.dr_groupid AS ff_groupid, ff.did AS ff_fwddid, g.id AS ff_gwgroupid FROM dsip_failfwd AS ff LEFT JOIN dr_rules AS r ON ff.dr_groupid = r.groupid LEFT JOIN dr_gw_lists AS g ON g.id = REPLACE(r.gwlist, '#', '') WHERE ff.dr_groupid <> :flt_outbound\n    UNION ALL\n    SELECT ff.dr_ruleid AS ff_ruleid, ff.dr_groupid AS ff_groupid, ff.did AS ff_fwddid, NULL AS ff_gwgroupid FROM dsip_failfwd AS ff LEFT JOIN dr_rules AS r ON ff.dr_ruleid = r.ruleid WHERE ff.dr_groupid = :flt_outbound\n) AS t3 ON t1.ruleid = t3.ff_ruleid\"\"\"), {\"flt_inbound\": settings.FLT_INBOUND, \"flt_outbound\": settings.FLT_OUTBOUND})\n\n        epgroups = db.query(GatewayGroups).filter(\n            GatewayGroups.description.regexp_match(GatewayGroups.FILTER.ENDPOINT.value)\n        ).all()\n        gwgroups = db.query(GatewayGroups).filter(\n            GatewayGroups.description.regexp_match(GatewayGroups.FILTER.ENDPOINT_OR_CARRIER.value)\n        ).all()\n\n        gatewayList = db.query(Gateways).all()\n\n        # sort endpoint groups by name\n        epgroups.sort(key=lambda x: strFieldsToDict(x.description)['name'].lower())\n        # sort gateway groups by type then by name\n        gwgroups.sort(key=lambda x: (strFieldsToDict(x.description)['type'], strFieldsToDict(x.description)['name'].lower()))\n\n        dids = []\n        if len(settings.FLOWROUTE_ACCESS_KEY) > 0 and len(settings.FLOWROUTE_SECRET_KEY) > 0:\n            try:\n                dids = numbers_api.getNumbers()\n            except http_exceptions.HTTPException as ex:\n                debugException(ex)\n                return showError(type=\"http\", code=ex.code, msg=\"Flowroute Credentials Not Valid\")\n\n        return render_template('inboundmapping.html', rows=res, gwgroups=gwgroups, epgroups=epgroups, imported_dids=dids, gatewayList=gatewayList)\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        db.rollback()\n        db.flush()\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\n\n@app.route('/inboundmapping', methods=['POST'])\ndef addUpdateInboundMapping():\n    \"\"\"\n    Adding and Updating of dr_rules table (inbound routes only)\\n\n    Partially Supports rule definitions for Kamailio Drouting module\\n\n    Documentation: `Drouting module <https://kamailio.org/docs/modules/4.4.x/modules/drouting.html>`_\n    \"\"\"\n\n    db = DummySession()\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        form = stripDictVals(request.form.to_dict())\n\n        # get form data\n        ruleid = form['ruleid'] if 'ruleid' in form else None\n        gwgroupid = form['gwgroupid'] if 'gwgroupid' in form and form['gwgroupid'] != \"0\" else ''\n        prefix = form['prefix'] if 'prefix' in form else ''\n        desc_dict = {'name': form['rulename']} if 'rulename' in form else {}\n        hardfwd_enabled = int(form['hardfwd_enabled'])\n        hf_gwgroupid = form['hf_gwgroupid'] if 'hf_gwgroupid' in form and form['hf_gwgroupid'] != \"0\" else ''\n        hf_groupid = form['hf_groupid'] if 'hf_groupid' in form else ''\n        hf_fwddid = form['hf_fwddid'] if 'hf_fwddid' in form else ''\n        failfwd_enabled = int(form['failfwd_enabled'])\n        ff_gwgroupid = form['ff_gwgroupid'] if 'ff_gwgroupid' in form and form['ff_gwgroupid'] != \"0\" else ''\n        ff_groupid = form['ff_groupid'] if 'ff_groupid' in form else ''\n        ff_fwddid = form['ff_fwddid'] if 'ff_fwddid' in form else ''\n\n        fwdgroupid = None\n\n        # TODO: seperate redundant code into functions\n        # TODO  need to add support for updating and deleting a LB Rule\n\n        # Adding\n        if not ruleid:\n            inserts = []\n\n            # don't allow duplicate entries\n            if db.query(InboundMapping).filter(InboundMapping.prefix == prefix).filter(InboundMapping.groupid == settings.FLT_INBOUND).scalar():\n                raise http_exceptions.BadRequest(\"Duplicate DID's are not allowed\")\n\n            if \"lb_\" in gwgroupid:\n                x = gwgroupid.split(\"_\")\n                gwgroupid = x[1]\n                desc_dict['lb_enabled'] = '1'\n            else:\n                desc_dict['lb_enabled'] = '0'\n            description = dictToStrFields(desc_dict)\n            # we only support a single gwgroup at this time\n            gwlist = '#{}'.format(gwgroupid) if len(gwgroupid) > 0 else ''\n            #     dispatcher_id = x[2].zfill(4)\n            #\n            #     Gateway = db.query(Gateways).filter(Gateways.description.like(\"%drouting_to_dispatcher%\"), Gateways.address == \"localhost\", Gateways.strip == 0, Gateways.pri_prefix == dispatcher_id, Gateways.type == settings.FLT_PBX).first()\n            #\n            #     if not Gateway:\n            #         # Create a gateway\n            #         Gateway = Gateways(\"drouting_to_dispatcher\", \"localhost\", 0, dispatcher_id, settings.FLT_PBX, gwgroup=gwgroupid)\n            #\n            #         db.add(Gateway)\n            #         db.flush()\n            #\n            #     addresses = [Address(\"myself\", settings.INTERNAL_IP_ADDR, 32, 1, gwgroup=gwgroupid)]\n            #     if settings.IPV6_ENABLED:\n            #         addresses.append(Address(\"myself\", settings.INTERNAL_IP6_ADDR, 32, 1, gwgroup=gwgroupid))\n            #     db.add_all(addresses)\n            #\n            #     # Define an Inbound Mapping that maps to the newly created gateway\n            #     gwlist = Gateway.gwid\n            #     IMap = InboundMapping(settings.FLT_INBOUND, prefix, gwlist, description)\n            #     db.add(IMap)\n            #\n            #     db.commit()\n            #     return displayInboundMapping()\n\n            IMap = InboundMapping(settings.FLT_INBOUND, prefix, gwlist, description)\n            inserts.append(IMap)\n\n            # find last rule in dr_rules\n            res = db.execute(\n                text(\"SELECT AUTO_INCREMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=:db_name AND TABLE_NAME='dr_rules'\"),\n                {\"db_name\": settings.KAM_DB_NAME}\n            ).first()\n            ruleid = int(res[0])\n\n            if hardfwd_enabled:\n                # when gwgroup is set we need to create a dr_rule that maps to the gwlist of that gwgroup\n                if len(hf_gwgroupid) > 0:\n                    gwlist = '#{}'.format(hf_gwgroupid)\n\n                    # find last forwarding rule\n                    lastfwd = db.query(InboundMapping).filter(InboundMapping.groupid >= settings.FLT_FWD_MIN).order_by(\n                        InboundMapping.groupid.desc()).first()\n\n                    # Start forwarding routes with a groupid set in settings (default is 20000)\n                    if lastfwd is None:\n                        fwdgroupid = settings.FLT_FWD_MIN\n                    else:\n                        fwdgroupid = int(lastfwd.groupid) + 1\n\n                    hfwd = InboundMapping(fwdgroupid, '', gwlist, 'name:Hard Forward from {} to DID {}'.format(prefix, hf_fwddid))\n                    inserts.append(hfwd)\n                # if no gwgroup selected we dont need dr_rules we set dr_groupid to FLT_OUTBOUND and we create hardfwd/failfwd rules\n                else:\n                    fwdgroupid = settings.FLT_OUTBOUND\n\n                hfwd_htable = dSIPHardFwd(ruleid, hf_fwddid, fwdgroupid)\n                inserts.append(hfwd_htable)\n\n            if failfwd_enabled:\n                # when gwgroup is set we need to create a dr_rule that maps to the gwlist of that gwgroup\n                if len(ff_gwgroupid) > 0:\n                    gwlist = '#{}'.format(ff_gwgroupid)\n\n                    # skip lookup if we just found the groupid\n                    if fwdgroupid is not None:\n                        fwdgroupid += 1\n                    else:\n                        # find last forwarding rule\n                        lastfwd = db.query(InboundMapping).filter(InboundMapping.groupid >= settings.FLT_FWD_MIN).order_by(\n                            InboundMapping.groupid.desc()).first()\n\n                        # Start forwarding routes with a groupid set in settings (default is 20000)\n                        if lastfwd is None:\n                            fwdgroupid = settings.FLT_FWD_MIN\n                        else:\n                            fwdgroupid = int(lastfwd.groupid) + 1\n\n                    ffwd = InboundMapping(fwdgroupid, '', gwlist, 'name:Failover Forward from {} to DID {}'.format(prefix, ff_fwddid))\n                    inserts.append(ffwd)\n                # if no gwgroup selected we dont need dr_rules we set dr_groupid to FLT_OUTBOUND and we create hardfwd/failfwd rules\n                else:\n                    fwdgroupid = settings.FLT_OUTBOUND\n\n                ffwd_htable = dSIPFailFwd(ruleid, ff_fwddid, fwdgroupid)\n                inserts.append(ffwd_htable)\n\n            db.add_all(inserts)\n\n            # enabling/disabling load balancing on one route must enable them all due to current limitations\n            db.flush()\n            if desc_dict['lb_enabled'] == '1':\n                lb_find = 'lb_enabled:0'\n                lb_repl = 'lb_enabled:1'\n            else:\n                lb_find = 'lb_enabled:1'\n                lb_repl = 'lb_enabled:0'\n            for row in db.query(InboundMapping).filter(\n                InboundMapping.groupid == settings.FLT_INBOUND\n            ).filter(\n                InboundMapping.gwlist == IMap.gwlist\n            ).filter(\n                InboundMapping.ruleid != IMap.ruleid\n            ):\n                row.description = row.description.replace(lb_find, lb_repl)\n\n        # Updating\n        else:\n            inserts = []\n\n            if \"lb_\" in gwgroupid:\n                # logging.info(\"In the load balancing update logic\")\n                x = gwgroupid.split(\"_\")\n                gwgroupid = x[1]\n                desc_dict['lb_enabled'] = '1'\n            else:\n                desc_dict['lb_enabled'] = '0'\n            description = dictToStrFields(desc_dict)\n            # we only support a single gwgroup at this time\n            gwlist = '#{}'.format(gwgroupid) if len(gwgroupid) > 0 else ''\n            # dispatcher_id = x[2].zfill(4)\n            #\n            # # logging.info(\"Searching for the gateway by description\")\n            # # Create a gateway\n            # Gateway = db.query(Gateways).filter(Gateways.description.like(gwgroupid) & Gateways.description.like(\"lb:{}\".format(dispatcher_id))).first()\n            # if Gateway:\n            #     logging.info(\"Gateway found\")\n            #     fields = strFieldsToDict(Gateway.description)\n            #     fields['lb'] = dispatcher_id\n            #     fields['gwgroup'] = gwgroupid\n            #     Gateway.update({'prefix': dispatcher_id, 'description': dictToStrFields(fields)})\n            # else:\n            #     # logging.info(\"Gateway not found\")\n            #     Gateway = db.query(Gateways).filter(Gateways.description.like(\"%drouting_to_dispatcher%\"), Gateways.address == \"localhost\", Gateways.strip == 0, Gateways.pri_prefix == dispatcher_id, Gateways.type == settings.FLT_PBX).first()\n            #     if not Gateway:\n            #         logging.info(\"Gateway not found, creating new gateway\")\n            #         # Create a gateway\n            #         Gateway = Gateways(\"drouting_to_dispatcher\", \"localhost\", 0, dispatcher_id, settings.FLT_PBX, gwgroup=gwgroupid)\n            #         db.add(Gateway)\n            #         db.flush()\n            #\n            #     # Gateway = Gateways(\"drouting_to_dispatcher\", \"localhost\", 0, dispatcher_id, settings.FLT_PBX, gwgroup=gwgroupid)\n            #     # db.add(Gateway)\n            #\n            # db.flush()\n            # # Assign Gateway id to the gateway list\n            # gwlist = Gateway.gwid\n            #\n            # try:\n            #     if not db.query(Address).filter(Address.ip_addr == settings.INTERNAL_IP_ADDR).scalar():\n            #         db.add(Address(\"myself\", settings.INTERNAL_IP_ADDR, 32, 1, gwgroup=gwgroupid))\n            #     if settings.IPV6_ENABLED:\n            #         if not db.query(Address).filter(Address.ip_addr == settings.INTERNAL_IP6_ADDR).scalar():\n            #             db.add(Address(\"myself\", settings.INTERNAL_IP6_ADDR, 32, 1, gwgroup=gwgroupid))\n            # except sql_exceptions.MultipleResultsFound as ex:\n            #     logging.info(\"Multiple Address rows found\")\n\n            IMap = db.query(InboundMapping).filter(InboundMapping.ruleid == ruleid).first()\n            IMap.prefix = prefix\n            IMap.gwlist = gwlist\n            IMap.description = description\n\n            hardfwd_exists = True if db.query(dSIPHardFwd).filter(dSIPHardFwd.dr_ruleid == ruleid).scalar() else False\n            db.flush()\n\n            if hardfwd_enabled:\n\n                # create fwd htable if it does not exist\n                if not hardfwd_exists:\n                    # only create rule if gwgroup has been selected\n                    if len(hf_gwgroupid) > 0:\n                        gwlist = '#{}'.format(hf_gwgroupid)\n\n                        # find last forwarding rule\n                        lastfwd = db.query(InboundMapping).filter(InboundMapping.groupid >= settings.FLT_FWD_MIN).order_by(\n                            InboundMapping.groupid.desc()).first()\n\n                        # Start forwarding routes with a groupid set in settings (default is 20000)\n                        if lastfwd is None:\n                            fwdgroupid = settings.FLT_FWD_MIN\n                        else:\n                            fwdgroupid = int(lastfwd.groupid) + 1\n\n                        hfwd = InboundMapping(fwdgroupid, '', gwlist, 'name:Hard Forward from {} to DID {}'.format(prefix, hf_fwddid))\n                        inserts.append(hfwd)\n                    # if no gwgroup selected we dont need dr_rules we set dr_groupid to FLT_OUTBOUND and we create htable\n                    else:\n                        fwdgroupid = settings.FLT_OUTBOUND\n\n                    hfwd_htable = dSIPHardFwd(ruleid, hf_fwddid, fwdgroupid)\n                    inserts.append(hfwd_htable)\n\n                # update existing fwd htable\n                else:\n                    # intially set fwdgroupid to update (if we create new rule it will change)\n                    fwdgroupid = int(hf_groupid) if len(hf_gwgroupid) > 0 else settings.FLT_OUTBOUND\n\n                    # only update rule if one exists, which we know only happens if groupid != FLT_OUTBOUND\n                    if hf_groupid != str(settings.FLT_OUTBOUND):\n                        # if gwgroup is selected we update the rule, if not selected we delete the rule\n                        if len(hf_gwgroupid) > 0:\n                            gwlist = '#{}'.format(hf_gwgroupid)\n                            db.query(InboundMapping).filter(InboundMapping.groupid == hf_groupid).update(\n                                {'prefix': '', 'gwlist': gwlist, 'description': 'name:Hard Forward from {} to DID {}'.format(prefix, hf_fwddid)},\n                                synchronize_session=False)\n                        else:\n                            hf_rule = db.query(InboundMapping).filter(\n                                ((InboundMapping.groupid == hf_groupid) & (InboundMapping.groupid != settings.FLT_OUTBOUND)) |\n                                ((InboundMapping.ruleid == ruleid) & (InboundMapping.groupid == settings.FLT_OUTBOUND)))\n                            if hf_rule.scalar():\n                                hf_rule.delete(synchronize_session=False)\n                    else:\n                        # if gwgroup is selected we create the rule\n                        if len(hf_gwgroupid) > 0:\n                            gwlist = '#{}'.format(hf_gwgroupid)\n\n                            # find last forwarding rule\n                            lastfwd = db.query(InboundMapping).filter(InboundMapping.groupid >= settings.FLT_FWD_MIN).order_by(\n                                InboundMapping.groupid.desc()).first()\n\n                            # Start forwarding routes with a groupid set in settings (default is 20000)\n                            if lastfwd is None:\n                                fwdgroupid = settings.FLT_FWD_MIN\n                            else:\n                                fwdgroupid = int(lastfwd.groupid) + 1\n\n                            hfwd = InboundMapping(fwdgroupid, '', gwlist, 'name:Hard Forward from {} to DID {}'.format(prefix, hf_fwddid))\n                            inserts.append(hfwd)\n\n                    db.query(dSIPHardFwd).filter(dSIPHardFwd.dr_ruleid == ruleid).update(\n                        {'did': hf_fwddid, 'dr_groupid': fwdgroupid}, synchronize_session=False)\n\n            else:\n                # delete existing fwd htable and possibly fwd rule\n                if hardfwd_exists:\n                    hf_rule = db.query(InboundMapping).filter(\n                        ((InboundMapping.groupid == hf_groupid) & (InboundMapping.groupid != settings.FLT_OUTBOUND)) |\n                        ((InboundMapping.ruleid == ruleid) & (InboundMapping.groupid == settings.FLT_OUTBOUND)))\n                    if hf_rule.scalar():\n                        hf_rule.delete(synchronize_session=False)\n                    hf_htable = db.query(dSIPHardFwd).filter(dSIPHardFwd.dr_ruleid == ruleid)\n                    hf_htable.delete(synchronize_session=False)\n\n            failfwd_exists = True if db.query(dSIPFailFwd).filter(dSIPFailFwd.dr_ruleid == ruleid).scalar() else False\n            db.flush()\n\n            if failfwd_enabled:\n\n                # create fwd htable if it does not exist\n                if not failfwd_exists:\n                    # only create rule if gwgroup has been selected\n                    if len(ff_gwgroupid) > 0:\n                        gwlist = '#{}'.format(ff_gwgroupid)\n\n                        # find last forwarding rule\n                        lastfwd = db.query(InboundMapping).filter(InboundMapping.groupid >= settings.FLT_FWD_MIN).order_by(\n                            InboundMapping.groupid.desc()).first()\n\n                        # Start forwarding routes with a groupid set in settings (default is 20000)\n                        if lastfwd is None:\n                            fwdgroupid = settings.FLT_FWD_MIN\n                        else:\n                            fwdgroupid = int(lastfwd.groupid) + 1\n\n                        ffwd = InboundMapping(fwdgroupid, '', gwlist, 'name:Failover Forward from {} to DID {}'.format(prefix, ff_fwddid))\n                        inserts.append(ffwd)\n                    # if no gwgroup selected we dont need dr_rules we set dr_groupid to FLT_OUTBOUND and we create htable\n                    else:\n                        fwdgroupid = settings.FLT_OUTBOUND\n\n                    ffwd_htable = dSIPFailFwd(ruleid, ff_fwddid, fwdgroupid)\n                    inserts.append(ffwd_htable)\n\n                # update existing fwd htable\n                else:\n                    # intially set fwdgroupid to update (if we create new rule it will change)\n                    fwdgroupid = int(ff_groupid) if len(ff_gwgroupid) > 0 else settings.FLT_OUTBOUND\n\n                    # only update rule if one exists, which we know only happens if groupid != FLT_OUTBOUND\n                    if ff_groupid != str(settings.FLT_OUTBOUND):\n                        # if gwgroup is selected we update the rule, if not selected we delete the rule\n                        if len(ff_gwgroupid) > 0:\n                            gwlist = '#{}'.format(ff_gwgroupid)\n                            db.query(InboundMapping).filter(InboundMapping.groupid == ff_groupid).update(\n                                {'prefix': '', 'gwlist': gwlist, 'description': 'name:Failover Forward from {} to DID {}'.format(prefix, ff_fwddid)},\n                                synchronize_session=False)\n                        else:\n                            ff_rule = db.query(InboundMapping).filter(\n                                ((InboundMapping.groupid == ff_groupid) & (InboundMapping.groupid != settings.FLT_OUTBOUND)) |\n                                ((InboundMapping.ruleid == ruleid) & (InboundMapping.groupid == settings.FLT_OUTBOUND)))\n                            if ff_rule.scalar():\n                                ff_rule.delete(synchronize_session=False)\n                    else:\n                        # if gwgroup is selected we create the rule\n                        if len(ff_gwgroupid) > 0:\n                            gwlist = '#{}'.format(ff_gwgroupid)\n\n                            # find last forwarding rule\n                            lastfwd = db.query(InboundMapping).filter(InboundMapping.groupid >= settings.FLT_FWD_MIN).order_by(\n                                InboundMapping.groupid.desc()).first()\n\n                            # Start forwarding routes with a groupid set in settings (default is 20000)\n                            if lastfwd is None:\n                                fwdgroupid = settings.FLT_FWD_MIN\n                            else:\n                                fwdgroupid = int(lastfwd.groupid) + 1\n\n                            ffwd = InboundMapping(fwdgroupid, '', gwlist, 'name:Failover Forward from {} to DID {}'.format(prefix, ff_fwddid))\n                            inserts.append(ffwd)\n\n                    db.query(dSIPFailFwd).filter(dSIPFailFwd.dr_ruleid == ruleid).update(\n                        {'did': ff_fwddid, 'dr_groupid': fwdgroupid}, synchronize_session=False)\n\n            else:\n                # delete existing fwd htable and possibly fwd rule\n                if failfwd_exists:\n                    ff_rule = db.query(InboundMapping).filter(\n                        ((InboundMapping.groupid == ff_groupid) & (InboundMapping.groupid != settings.FLT_OUTBOUND)) |\n                        ((InboundMapping.ruleid == ruleid) & (InboundMapping.groupid == settings.FLT_OUTBOUND)))\n                    if ff_rule.scalar():\n                        ff_rule.delete(synchronize_session=False)\n                    ff_htable = db.query(dSIPFailFwd).filter(dSIPFailFwd.dr_ruleid == ruleid)\n                    ff_htable.delete(synchronize_session=False)\n\n            if len(inserts) > 0:\n                db.add_all(inserts)\n\n            # enabling/disabling load balancing on one route must enable them all due to current limitations\n            db.flush()\n            if desc_dict['lb_enabled'] == '1':\n                lb_find = 'lb_enabled:0'\n                lb_repl = 'lb_enabled:1'\n            else:\n                lb_find = 'lb_enabled:1'\n                lb_repl = 'lb_enabled:0'\n            for row in db.query(InboundMapping).filter(\n                InboundMapping.groupid == settings.FLT_INBOUND\n            ).filter(\n                InboundMapping.gwlist == IMap.gwlist\n            ).filter(\n                InboundMapping.ruleid != IMap.ruleid\n            ):\n                row.description = row.description.replace(lb_find, lb_repl)\n\n        db.commit()\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return displayInboundMapping()\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        db.rollback()\n        db.flush()\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\n\n@app.route('/inboundmappingdelete', methods=['POST'])\ndef deleteInboundMapping():\n    \"\"\"\n    Deleting of dr_rules table (inbound routes only)\\n\n    Partially Supports rule definitions for Kamailio Drouting module\\n\n    Documentation: `Drouting module <https://kamailio.org/docs/modules/4.4.x/modules/drouting.html>`_\n    \"\"\"\n\n    db = DummySession()\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        form = stripDictVals(request.form.to_dict())\n\n        ruleid = form['ruleid']\n        hf_ruleid = form['hf_ruleid']\n        hf_groupid = form['hf_groupid']\n        ff_ruleid = form['ff_ruleid']\n        ff_groupid = form['ff_groupid']\n\n        im_rule = db.query(InboundMapping).filter(InboundMapping.ruleid == ruleid).first()\n        # Delete the gateway if it's being used for Load Balancing\n        if '#' not in im_rule.gwlist:\n            dispatcher_gateway = db.query(Gateways).filter(Gateways.gwid == im_rule.gwlist)\n            dispatcher_gateway.delete(synchronize_session=False)\n        # Delete the rule now\n        db.delete(im_rule)\n\n        if len(hf_ruleid) > 0:\n            # no dr_rules created for fwding without a gwgroup selected\n            hf_rule = db.query(InboundMapping).filter(\n                ((InboundMapping.groupid == hf_groupid) & (InboundMapping.groupid != settings.FLT_OUTBOUND)) |\n                ((InboundMapping.ruleid == ruleid) & (InboundMapping.groupid == settings.FLT_OUTBOUND)))\n            if hf_rule.scalar():\n                hf_rule.delete(synchronize_session=False)\n            hf_htable = db.query(dSIPHardFwd).filter(dSIPHardFwd.dr_ruleid == ruleid)\n            hf_htable.delete(synchronize_session=False)\n\n        if len(ff_ruleid) > 0:\n            # no dr_rules created for fwding without a gwgroup selected\n            ff_rule = db.query(InboundMapping).filter(\n                ((InboundMapping.groupid == ff_groupid) & (InboundMapping.groupid != settings.FLT_OUTBOUND)) |\n                ((InboundMapping.ruleid == ruleid) & (InboundMapping.groupid == settings.FLT_OUTBOUND)))\n            if ff_rule.scalar():\n                ff_rule.delete(synchronize_session=False)\n            ff_htable = db.query(dSIPFailFwd).filter(dSIPFailFwd.dr_ruleid == ruleid)\n            ff_htable.delete(synchronize_session=False)\n\n        db.commit()\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return displayInboundMapping()\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        db.rollback()\n        db.flush()\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\n# TODO: add support for hard/fail forwarding\ndef processInboundMappingImport(filename, override_gwgroupid, db):\n    try:\n        # sql = insert(\n        #     InboundMapping\n        # ).values(\n        #     groupid=str(settings.FLT_INBOUND),\n        #     prefix=bindparam(\"prefix\"),\n        #     gwlist=bindparam(\"gwlist\"),\n        #     description=f'name:{bindparam(\"name\")}',\n        # ).on_duplicate_key_update(\n        #     prefix=bindparam(\"prefix\"),\n        #     gwlist=bindparam(\"gwlist\"),\n        #     # description=bindparam('name', callable_=lambda name: text(f\"REGEXP_REPLACE(description, '(.*?)name:[^,|$]*(.*?)', '\\\\1name:{name}\\\\2')\"),\n        # )\n        insert_sql = text(\"\"\"\nINSERT INTO dr_rules (groupid, prefix, timerec, routeid, gwlist, description)\nVALUES (:groupid, :prefix, '', '', :gwlist, CONCAT('name:', :name))\n        \"\"\")\n        update_sql = text(\"\"\"\nUPDATE dr_rules SET\nprefix = :prefix, gwlist = :gwlist, description = REGEXP_REPLACE(description, '(.*?)name:[^,|$]*(.*?)', CONCAT('\\\\\\\\1name:', :name, '\\\\\\\\2'))\nWHERE prefix = :prefix\n        \"\"\")\n        insert_sql_args = []\n        update_sql_args = []\n        # prefix: sql_args\n        dids_uploaded = {}\n\n        with open(os.path.join(settings.UPLOAD_FOLDER, filename), 'r', newline='') as csv_file:\n            csv_file = csv.DictReader(csv_file)\n\n            for row in csv_file:\n                if len(row) == 0:\n                    continue\n\n                prefix = row['DID']\n                if override_gwgroupid is not None:\n                    gwlist = override_gwgroupid\n                else:\n                    gwlist = f'#{row[\"EndpointGroupID\"]}'\n                name = row[\"Name\"]\n\n                dids_uploaded[prefix] = {'groupid': str(settings.FLT_INBOUND), 'prefix': prefix, 'gwlist': gwlist, 'name': name}\n\n        # bulk SELECT\n        select_sql = select(InboundMapping.prefix).filter(\n            (InboundMapping.groupid == settings.FLT_INBOUND) &\n            (InboundMapping.prefix.in_(dids_uploaded.keys()))\n        )\n        existing_dids = db.execute(select_sql).scalars().all()\n        for did in dids_uploaded:\n            if did in existing_dids:\n                update_sql_args.append(dids_uploaded[did])\n            else:\n                insert_sql_args.append(dids_uploaded[did])\n\n        db.execute(insert_sql, insert_sql_args)\n        db.execute(update_sql, update_sql_args)\n        db.commit()\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        db.rollback()\n        db.flush()\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n\n\n@app.route('/inboundmappingimport', methods=['POST'])\ndef importInboundMapping():\n    db = DummySession()\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        form = stripDictVals(request.form.to_dict())\n\n        # get form data\n        gwgroupid = form['gwgroupid'] if 'gwgroupid' in form else None\n\n        if 'file' not in request.files:\n            flash('No file part')\n            return redirect(url_for('displayInboundMapping'))\n        file = request.files['file']\n        # if user does not select file, browser also\n        # submit a empty part without filename\n        if file.filename == '':\n            flash('No selected file')\n            return redirect(request.url)\n        if file and allowed_file(file.filename, ALLOWED_EXTENSIONS={'csv'}):\n            filename = secure_filename(file.filename)\n            file.save(os.path.join(settings.UPLOAD_FOLDER, filename))\n            processInboundMappingImport(filename, gwgroupid, db)\n            flash('DIDs were successfully imported')\n            getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n            return redirect(url_for('displayInboundMapping', filename=filename))\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        db.rollback()\n        db.flush()\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\n\n@app.route('/teleblock')\ndef displayTeleBlock():\n    \"\"\"\n    Display teleblock settings in view\n    \"\"\"\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        teleblock = {}\n        teleblock[\"gw_enabled\"] = settings.TELEBLOCK_GW_ENABLED\n        teleblock[\"gw_ip\"] = settings.TELEBLOCK_GW_IP\n        teleblock[\"gw_port\"] = settings.TELEBLOCK_GW_PORT\n        teleblock[\"media_ip\"] = settings.TELEBLOCK_MEDIA_IP\n        teleblock[\"media_port\"] = settings.TELEBLOCK_MEDIA_PORT\n\n        return render_template('teleblock.html', teleblock=teleblock)\n\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        return showError(type=error)\n\n\n@app.route('/teleblock', methods=['POST'])\ndef addUpdateTeleBlock():\n    \"\"\"\n    Update teleblock config file settings\n    \"\"\"\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        form = stripDictVals(request.form.to_dict())\n\n        # Update the teleblock settings\n        teleblock = {}\n        teleblock['TELEBLOCK_GW_ENABLED'] = int(form.get('gw_enabled', 0))\n        teleblock['TELEBLOCK_GW_IP'] = form['gw_ip']\n        teleblock['TELEBLOCK_GW_PORT'] = form['gw_port']\n        teleblock['TELEBLOCK_MEDIA_IP'] = form['media_ip']\n        teleblock['TELEBLOCK_MEDIA_PORT'] = form['media_port']\n\n        updateConfig(settings, teleblock, hot_reload=True)\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return displayTeleBlock()\n\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        return showError(type=error)\n\n\n@app.route('/transnexus')\ndef displayTransNexus(msg=None):\n    \"\"\"\n    Display TransNexus settings in view\n    \"\"\"\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        license_status = getLicenseStatus(license_tag='DSIP_TRANSNEXUS')\n        if license_status == 0:\n            return render_template('license_required.html', msg=None)\n\n        if license_status == 1:\n            return render_template('license_required.html', msg='license is not valid, ensure your license is still active')\n\n        if license_status == 2:\n            return render_template('license_required.html', msg='license is associated with another machine, re-associate it with this machine first')\n\n        return render_template(\n            'transnexus.html',\n            msg=msg\n        )\n\n    except WoocommerceError as ex:\n        return render_template('license_required.html', msg=str(ex))\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        return showError(type=error)\n\n\n@app.route('/transnexus', methods=['POST'])\ndef addUpdateTransNexus():\n    \"\"\"\n    Update TransNexus config file settings\n    \"\"\"\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        license_status = getLicenseStatus(license_tag='DSIP_TRANSNEXUS')\n        if license_status == 0:\n            return render_template('license_required.html', msg=None)\n\n        if license_status == 1:\n            return render_template('license_required.html', msg='license is not valid, ensure your license is still active')\n\n        if license_status == 2:\n            return render_template('license_required.html', msg='license is associated with another machine, re-associate it with this machine first')\n\n        form = stripDictVals(request.form.to_dict())\n\n        # Update the TransNexus settings\n        tn_settings = {}\n        if 'authservice_enabled' in form:\n            tn_settings['TRANSNEXUS_AUTHSERVICE_ENABLED'] = int(form['authservice_enabled'])\n            tn_settings[\"TRANSNEXUS_AUTHSERVICE_HOST\"] = form['authservice_host']\n        else:\n            # It was disabled so the form field was not sent over\n            tn_settings['TRANSNEXUS_AUTHSERVICE_ENABLED'] = 0\n        if 'verifyservice_enabled' in form:\n            tn_settings['TRANSNEXUS_VERIFYSERVICE_ENABLED'] = int(form['verifyservice_enabled'])\n            tn_settings[\"TRANSNEXUS_VERIFYSERVICE_HOST\"] = form['verifyservice_host']\n        else:\n            # It was disabled so the form field was not sent over\n            tn_settings['TRANSNEXUS_VERIFYSERVICE_ENABLED'] = 0\n\n        if len(tn_settings) != 0:\n            updateConfig(settings, tn_settings, hot_reload=True)\n            getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n\n        return displayTransNexus()\n\n    except WoocommerceError as ex:\n        return render_template('license_required.html', msg=str(ex))\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        return showError(type=error)\n\n\n@app.route('/outboundroutes')\ndef displayOutboundRoutes():\n    \"\"\"\n    Displaying of dr_rules table (outbound routes only)\\n\n    Supports rule definitions for Kamailio Drouting module\\n\n    Documentation: `Drouting module <https://kamailio.org/docs/modules/4.4.x/modules/drouting.html>`_\n    \"\"\"\n\n    db = DummySession()\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        rows = db.query(OutboundRoutes).filter(\n            (OutboundRoutes.groupid == settings.FLT_OUTBOUND) |\n            ((OutboundRoutes.groupid >= settings.FLT_LCR_MIN) &\n             (OutboundRoutes.groupid < settings.FLT_FWD_MIN))).outerjoin(\n            dSIPLCR, dSIPLCR.dr_groupid == OutboundRoutes.groupid).outerjoin(\n            GatewayGroups, func.REPLACE(OutboundRoutes.gwlist, '#', '') == GatewayGroups.id).add_columns(\n            dSIPLCR.from_prefix, dSIPLCR.cost, dSIPLCR.dr_groupid, OutboundRoutes.ruleid,\n            OutboundRoutes.prefix, OutboundRoutes.routeid, func.REPLACE(OutboundRoutes.gwlist, '#', '').label('gwgroupid'),\n            OutboundRoutes.timerec, OutboundRoutes.priority, OutboundRoutes.description,\n            GatewayGroups.description.label('gwgroup_description'), GatewayGroups.gwlist)\n\n        cgroups = db.query(GatewayGroups).filter(\n            GatewayGroups.description.regexp_match(GatewayGroups.FILTER.CARRIER.value)\n        ).all()\n\n        # sort carrier groups by name\n        cgroups.sort(key=lambda x: strFieldsToDict(x.description)['name'].lower())\n\n        teleblock = {}\n        teleblock[\"gw_enabled\"] = settings.TELEBLOCK_GW_ENABLED\n        teleblock[\"gw_ip\"] = settings.TELEBLOCK_GW_IP\n        teleblock[\"gw_port\"] = settings.TELEBLOCK_GW_PORT\n        teleblock[\"media_ip\"] = settings.TELEBLOCK_MEDIA_IP\n        teleblock[\"media_port\"] = settings.TELEBLOCK_MEDIA_PORT\n\n        return render_template('outboundroutes.html', rows=rows, cgroups=cgroups, teleblock=teleblock,\n            custom_routes=getCustomRoutes())\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        db.rollback()\n        db.flush()\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\n\n@app.route('/outboundroutes', methods=['POST'])\ndef addUpateOutboundRoutes():\n    \"\"\"\n    Adding and Updating dr_rules table (outbound routes only)\\n\n    Supports rule definitions for Kamailio Drouting module\\n\n    Documentation: `Drouting module <https://kamailio.org/docs/modules/4.4.x/modules/drouting.html>`_\n    \"\"\"\n\n    db = DummySession()\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        form = stripDictVals(request.form.to_dict())\n\n        # get form data\n        ruleid = form['ruleid'] if 'ruleid' in form and len(form['ruleid']) > 0 else None\n        groupid = form['groupid'] if 'groupid' in form and len(form['groupid']) > 0 \\\n                                     and form['groupid'] != \"None\" else settings.FLT_OUTBOUND\n        from_prefix = form['from_prefix'] if 'from_prefix' in form and len(form['from_prefix']) > 0 else None\n        prefix = form['prefix'] if 'prefix' in form else ''\n        timerec = form['timerec'] if 'timerec' in form else ''\n        priority = int(form['priority']) if 'priority' in form and len(form['priority']) > 0 else 0\n        routeid = form['routeid'] if 'routeid' in form else ''\n        gwgroupid = form['gwgroupid'] if 'gwgroupid' in form and form['gwgroupid'] != \"0\" else ''\n        description = 'name:{}'.format(form['name']) if 'name' in form else ''\n\n        pattern = None\n        gwlist = '#{}'.format(gwgroupid) if len(gwgroupid) > 0 else ''\n\n        # Adding\n        if not ruleid:\n            # if len(from_prefix) > 0 and len(prefix) == 0 :\n            #    return displayOutboundRoutes()\n            if from_prefix is not None:\n                print(\"from_prefix: {}\".format(from_prefix))\n\n                # Grab the lastest groupid and increment\n                mlcr = db.query(dSIPLCR).filter((dSIPLCR.dr_groupid >= settings.FLT_LCR_MIN) &\n                                                (dSIPLCR.dr_groupid < settings.FLT_FWD_MIN)).order_by(dSIPLCR.dr_groupid.desc()).first()\n                db.commit()\n\n                # Start LCR routes with a groupid in settings (default is 10000)\n                if mlcr is None:\n                    groupid = settings.FLT_LCR_MIN\n                else:\n                    groupid = int(mlcr.dr_groupid) + 1\n\n                pattern = from_prefix + \"-\" + prefix\n\n            OMap = OutboundRoutes(groupid=groupid, prefix=prefix, timerec=timerec, priority=priority,\n                routeid=routeid, gwlist=gwlist, description=description)\n            db.add(OMap)\n\n            # Add the lcr map\n            if pattern != None:\n                OLCRMap = dSIPLCR(pattern, from_prefix, groupid)\n                db.add(OLCRMap)\n\n        # Updating\n        else:\n            # no from_prefix we can update outbound route and remove any LCR entries\n            if from_prefix is None and prefix is not None:\n                oldgroupid = groupid\n                groupid = settings.FLT_OUTBOUND\n                #if (groupid is None) or (groupid == \"None\"):\n                #    groupid = settings.FLT_OUTBOUND\n\n                # Convert the dr_rule back to a default carrier rule\n                db.query(OutboundRoutes).filter(OutboundRoutes.ruleid == ruleid).update({\n                    'prefix': prefix, 'groupid': groupid, 'timerec': timerec, 'priority': priority,\n                    'routeid': routeid, 'gwlist': gwlist, 'description': description\n                }, synchronize_session=False)\n\n                # Delete from LCR table using the old group id\n                d1 = db.query(dSIPLCR).filter(dSIPLCR.dr_groupid == oldgroupid)\n                d1.delete(synchronize_session=False)\n\n            # from_prefix given, we update outbound route and create/update LCR entry\n            elif from_prefix is not None:\n\n                # Setup pattern\n                pattern = from_prefix + \"-\" + prefix\n\n                # Check if pattern already exists\n                exists = db.query(dSIPLCR).filter(dSIPLCR.pattern == pattern).scalar()\n                if exists:\n                    db.query(OutboundRoutes).filter(OutboundRoutes.ruleid == ruleid).update({\n                        'prefix': prefix, 'groupid': groupid, 'timerec': timerec, 'priority': priority,\n                        'routeid': routeid, 'gwlist': gwlist, 'description': description\n                    }, synchronize_session=False)\n\n                # Adding a From prefix to an existing To\n                elif prefix is not None and groupid == settings.FLT_OUTBOUND:\n                    # Create a new groupid\n                    mlcr = db.query(dSIPLCR).filter((dSIPLCR.dr_groupid >= settings.FLT_LCR_MIN) &\n                                                    (dSIPLCR.dr_groupid < settings.FLT_FWD_MIN)).order_by(dSIPLCR.dr_groupid.desc()).first()\n\n                    # Start LCR routes with a groupid set in settings (default is 10000)\n                    if mlcr is None:\n                        groupid = settings.FLT_LCR_MIN\n                    else:\n                        groupid = int(mlcr.dr_groupid) + 1\n\n                    # Setup new pattern\n                    pattern = from_prefix + \"-\" + prefix\n\n                    # Add the lcr map\n                    if pattern != None:\n                        OLCRMap = dSIPLCR(pattern, from_prefix, groupid)\n                        db.add(OLCRMap)\n\n                    db.query(OutboundRoutes).filter(OutboundRoutes.ruleid == ruleid).update({\n                        'groupid': groupid, 'prefix': prefix, 'timerec': timerec, 'priority': priority,\n                        'routeid': routeid, 'gwlist': gwlist, 'description': description\n                    }, synchronize_session=False)\n\n                # Update existing pattern\n                elif (int(groupid) >= settings.FLT_LCR_MIN) and (int(groupid) < settings.FLT_FWD_MIN):\n                    db.query(dSIPLCR).filter(dSIPLCR.dr_groupid == groupid).update({\n                        'pattern': pattern, 'from_prefix': from_prefix})\n\n                    # Update the dr_rules table\n                    db.query(OutboundRoutes).filter(OutboundRoutes.ruleid == ruleid).update({\n                        'prefix': prefix, 'groupid': groupid, 'timerec': timerec, 'priority': priority,\n                        'routeid': routeid, 'gwlist': gwlist, 'description': description\n                    }, synchronize_session=False)\n\n            # no to/from_prefix remove any LCR entries and update outbound route\n            # TODO: not sure how to correlate stale LCR entries without old from-prefix (ideally we match by id)\n            else:\n                # Update the dr_rules table\n                db.query(OutboundRoutes).filter(OutboundRoutes.ruleid == ruleid).update({\n                    'prefix': prefix, 'groupid': groupid, 'timerec': timerec, 'priority': priority,\n                    'routeid': routeid, 'gwlist': gwlist, 'description': description\n                }, synchronize_session=False)\n\n        db.commit()\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return displayOutboundRoutes()\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        db.rollback()\n        db.flush()\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\n\n@app.route('/outboundroutesdelete', methods=['POST'])\ndef deleteOutboundRoute():\n    \"\"\"\n    Deleting of dr_rules table (outbound routes only)\\n\n    Supports rule definitions for Kamailio Drouting module\\n\n    Documentation: `Drouting module <https://kamailio.org/docs/modules/4.4.x/modules/drouting.html>`_\n    \"\"\"\n\n    db = DummySession()\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        form = stripDictVals(request.form.to_dict())\n\n        ruleid = form['ruleid']\n\n        OMap = db.query(OutboundRoutes).filter(OutboundRoutes.ruleid == ruleid).first()\n        if OMap:\n            d1 = db.query(dSIPLCR).filter(dSIPLCR.dr_groupid == OMap.groupid)\n            d1.delete(synchronize_session=False)\n\n        d = db.query(OutboundRoutes).filter(OutboundRoutes.ruleid == ruleid)\n        d.delete(synchronize_session=False)\n\n        db.commit()\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return displayOutboundRoutes()\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        db.rollback()\n        db.flush()\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\n\n@app.route('/stirshaken')\ndef displayStirShaken(msg=None):\n    \"\"\"\n    Display TransNexus settings in view\n    \"\"\"\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        license_status = getLicenseStatus(license_tag='DSIP_STIRSHAKEN')\n        if license_status == 0:\n            return render_template('license_required.html', msg=None)\n\n        if license_status == 1:\n            return render_template('license_required.html', msg='license is not valid, ensure your license is still active')\n\n        if license_status == 2:\n            return render_template('license_required.html', msg='license is associated with another machine, re-associate it with this machine first')\n\n        if settings.STIR_SHAKEN_ENABLED == 1:\n            toggle_checked = 'checked'\n            options_hidden = 'hidden'\n        else:\n            toggle_checked = ''\n            options_hidden = ''\n\n        return render_template(\n            'stirshaken.html',\n            msg=msg,\n            toggle_checked=toggle_checked,\n            options_hidden=options_hidden\n        )\n\n    except WoocommerceError as ex:\n        return render_template('license_required.html', msg=str(ex))\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        return showError(type=error)\n\n\n@app.route('/stirshaken', methods=['POST'])\ndef addUpdateStirShaken():\n    \"\"\"\n    Update STIR/SHAKEN config file settings\n    \"\"\"\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        license_status = getLicenseStatus(license_tag='DSIP_STIRSHAKEN')\n        if license_status == 0:\n            return render_template('license_required.html', msg=None)\n\n        if license_status == 1:\n            return render_template('license_required.html', msg='license is not valid, ensure your license is still active')\n\n        if license_status == 2:\n            return render_template('license_required.html', msg='license is associated with another machine, re-associate it with this machine first')\n\n        form = stripDictVals(request.form.to_dict())\n\n        # Update the STIR/SHAKEN settings\n        ss_settings = {}\n        if 'stir_shaken_enabled' in form:\n            tmp = form.get('stir_shaken_enabled', 0)\n            ss_settings[\"STIR_SHAKEN_ENABLED\"] = 1 if tmp == \"1\" else 0\n        else:\n            ss_settings[\"STIR_SHAKEN_ENABLED\"] = 0\n\n        if 'stir_shaken_prefix_a' in form:\n            ss_settings[\"STIR_SHAKEN_PREFIX_A\"] = form.get('stir_shaken_prefix_a', '')\n        if 'stir_shaken_prefix_b' in form:\n            ss_settings[\"STIR_SHAKEN_PREFIX_B\"] = form.get('stir_shaken_prefix_b', '')\n        if 'stir_shaken_prefix_c' in form:\n            ss_settings[\"STIR_SHAKEN_PREFIX_C\"] = form.get('stir_shaken_prefix_c', '')\n        if 'stir_shaken_prefix_invalid' in form:\n            ss_settings[\"STIR_SHAKEN_PREFIX_INVALID\"] = form.get('stir_shaken_prefix_invalid', '')\n        if 'stir_shaken_block_invalid' in form:\n            tmp = form.get('stir_shaken_block_invalid', 0)\n            ss_settings[\"STIR_SHAKEN_BLOCK_INVALID\"] = 1 if tmp == \"on\" else 0\n        if 'stir_shaken_cert_url' in form:\n            ss_settings[\"STIR_SHAKEN_CERT_URL\"] = form.get('stir_shaken_cert_url', '')\n        if 'stir_shaken_key_path' in form:\n            ss_settings[\"STIR_SHAKEN_KEY_PATH\"] = form.get('stir_shaken_key_path', '')\n\n        if len(ss_settings) != 0:\n            updateConfig(settings, ss_settings, hot_reload=True)\n            getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n\n        return displayStirShaken()\n\n    except WoocommerceError as ex:\n        return render_template('license_required.html', msg=str(ex))\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        return showError(type=error)\n\n\n@app.route('/upgrade')\ndef displayUpgrade(msg=None):\n    \"\"\"\n    Display Upgrade settings in view\n    \"\"\"\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        license_status = getLicenseStatus(license_tag='DSIP_CORE')\n        if license_status == 0:\n            return render_template('license_required.html', msg=None)\n        if license_status == 1:\n            return render_template('license_required.html', msg='license is not valid, ensure your license is still active')\n        if license_status == 2:\n            return render_template('license_required.html', msg='license is associated with another machine, re-associate it with this machine first')\n\n        # make sure we remove offset file when navigating to top level page\n        if os.path.exists(UPGRADE_OFFSET):\n            os.remove(UPGRADE_OFFSET)\n\n        latest = UpdateUtils.get_latest_version()\n\n        upgrade_settings = {\n            \"current_version\": settings.VERSION,\n            \"latest_version\": latest['ver_num'],\n            \"upgrade_available\": 1 if latest['ver_num'] > float(settings.VERSION) else 0\n        }\n        return render_template('upgrade.html', upgrade_settings=upgrade_settings, msg=msg)\n\n    except WoocommerceError as ex:\n        return render_template('license_required.html', msg=str(ex))\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        return showError(type=error)\n\n\n@process\ndef runUpgrade(cmd, env):\n    try:\n        getSharedMemoryDict(STATE_SHMEM_NAME)['dsip_upgrade_ongoing'] = True\n\n        with open(UPGRADE_LOG, 'wb', buffering=0) as f:\n            run_info = subprocess.run(\n                cmd,\n                stdout=f,\n                stderr=subprocess.STDOUT,\n                universal_newlines=True,\n                env=env,\n                restore_signals=False,\n                close_fds=True\n            )\n            run_info.check_returncode()\n\n            # getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n            # kamailio will be reloaded when dsiprouter is reloaded\n            getSharedMemoryDict(STATE_SHMEM_NAME)['dsip_reload_required'] = True\n    finally:\n        getSharedMemoryDict(STATE_SHMEM_NAME)['dsip_upgrade_ongoing'] = False\n\n@app.route('/upgrade/start', methods=['POST'])\ndef startUpgrade():\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        if getLicenseStatus(license_tag='DSIP_CORE') != 3:\n            raise WoocommerceError('invalid license')\n\n        IO.loginfo(\"starting upgrade\")\n\n        form = stripDictVals(request.form.to_dict())\n        cmd = ['sudo', '-E', 'dsiprouter', 'upgrade', '-rel', f\"{form['latest_version']}\", '-url', settings.GIT_REPO_URL]\n        runUpgrade(cmd, None)\n\n        IO.loginfo(\"upgrade started\")\n\n        return Response(), StatusCodes.HTTP_OK\n\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        return showError(type=error)\n\n\n# format a message as a server sent event\n# idea from: https://maxhalford.github.io/blog/flask-sse-no-deps/\n# TODO: separate into SSE specific file\ndef formatSSE(data: str, event: str = None) -> str:\n    msg = f'data: {data}\\n\\n'\n    if event is not None:\n        msg = f'event: {event}\\n{msg}'\n    return msg\n\n\n# inspired by: https://gist.github.com/kapb14/87255efffa173bb76cf5c1ed9db1d047\n# TODO: move to file handling\n# TODO: figure out why the SSE are not being sent every 1 second as expected\ndef readLogChunk(log_file):\n    state = getSharedMemoryDict(STATE_SHMEM_NAME)\n    while state['dsip_upgrade_ongoing'] == True:\n        try:\n            lines = Pygtail(log_file, offset_file=f'{log_file}.offset').read()\n            yield formatSSE(\n                ansi_converter.convert(\n                    lines,\n                    full=False\n                ).replace('\\n', '<br>')\n            )\n        except:\n            pass\n        time.sleep(1)\n    # process the reset of the lines\n    try:\n        lines = Pygtail(log_file, offset_file=f'{log_file}.offset').read()\n        yield formatSSE(\n            ansi_converter.convert(\n                lines,\n                full=False\n            ).replace('\\n', '<br>')\n        )\n    except:\n        pass\n\ndef checkUpgradeStatus():\n    state = getSharedMemoryDict(STATE_SHMEM_NAME)\n    while state['dsip_upgrade_ongoing'] == True:\n        yield formatSSE('1')\n        time.sleep(30)\n    yield formatSSE('0')\n\n\n@app.route('/upgrade/status')\ndef getUpgradeStatus():\n    try:\n        if not session.get('logged_in'):\n            return 'Unauthorized', 401\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        return Response(\n            checkUpgradeStatus(),\n            mimetype=\"text/event-stream\",\n            headers={\n                'X-Accel-Buffering': 'no',\n                'Cache-Control': 'no-cache',\n                'Connection': 'keep-alive'\n            },\n        )\n\n    except Exception as ex:\n        debugException(ex)\n        return 'Failed Checking Status', StatusCodes.HTTP_INTERNAL_SERVER_ERROR\n\n@app.route('/upgrade/log')\ndef getUpgradeLog():\n    try:\n        if not session.get('logged_in'):\n            return 'Unauthorized', 401\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        accept = request.headers.get('Accept', '').lower()\n        if accept == 'text/event-stream':\n            return Response(\n                readLogChunk(UPGRADE_LOG),\n                mimetype=\"text/event-stream\",\n                headers={\n                    'X-Accel-Buffering': 'no',\n                    'Cache-Control': 'no-cache',\n                    'Connection': 'keep-alive'\n                },\n            )\n        else:\n            with open(UPGRADE_LOG, 'r') as f:\n                return Response(\n                    ansi_converter.convert(f.read(), full=False).replace('\\n', '<br>'),\n                    mimetype=\"text/html\",\n                )\n    except FileNotFoundError:\n        return 'File not found', 404\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        return showError(type=error)\n\n\n# custom jinja filters\ndef yesOrNoFilter(list, field):\n    if list == 1:\n        return \"Yes\"\n    else:\n        return \"No\"\n\n\ndef noneFilter(list):\n    if list is None:\n        return \"\"\n    else:\n        return list\n\n\ndef attrFilter(s, attr, delims=(',', ':')):\n    if s is None:\n        return ''\n    try:\n        return dict(\n            item.split(delims[1], maxsplit=1) for item in s.split(delims[0])\n        )[attr]\n    except:\n        return ''\n\n\ndef domainTypeFilter(list):\n    if list is None:\n        return \"Unknown\"\n    elif list == \"0\":\n        return \"Static\"\n    elif list == \"1\":\n        return \"Dynamic\"\n    elif list == \"2\":\n        return \"Static using Dispatcher\"\n    elif list == \"3\":\n        return \"MSTeams\"\n    else:\n        return \"Other\"\n\n\ndef imgFilter(name):\n    images_url = urllib.parse.urljoin(app.static_url_path, 'images')\n    search_path = os.path.join(app.static_folder, 'images', name)\n    filenames = glob.glob(search_path + '.*')\n    if len(filenames) > 0:\n        return urllib.parse.urljoin(images_url, filenames[0])\n    else:\n        return \"\"\n\n# custom jinja context processors\n@app.context_processor\ndef injectGlobals():\n    state = getSharedMemoryDict(STATE_SHMEM_NAME)\n    return {\n        'settings': settings,\n        'state': state,\n        'licenseValid': lambda tag: getLicenseStatusFromStateDict(state['dsip_license_store'], tag)\n    }\n\n\n# DEPRECATED: now done by dsiprouter.sh (CLI commands run by dsip-init.service), marked for removal in v0.80\n# def getDynamicNetworkSettings():\n#     \"\"\"\n#     Get the network settings depending on the state of NETWORK_MODE when called\n#\n#     :return:    network settings or empty dict\n#     :rtype:     dict\n#     \"\"\"\n#\n#     if settings.NETWORK_MODE == 1:\n#         return {}\n#     elif settings.NETWORK_MODE == 2:\n#         raise NotImplementedError()\n#\n#     int_ip = getInternalIP('4')\n#     int_ip6 = getInternalIP('6')\n#     int_net = getInternalCIDR('4')\n#     int_net6 = getInternalCIDR('6')\n#     int_fqdn = socket.getfqdn()\n#     ext_ip = getExternalIP('4')\n#     ext_ip6 = getExternalIP('6')\n#     ext_fqdn = ipToHost(ext_ip)\n#     if os.path.exists('/proc/net/if_inet6'):\n#         try:\n#             with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) as s:\n#                 s.connect((int_ip6, 0))\n#             ipv6_enabled = True\n#         except:\n#             ipv6_enabled = False\n#     else:\n#         ipv6_enabled = False\n#\n#     return {\n#         'IPV6_ENABLED': ipv6_enabled,\n#         'INTERNAL_IP_ADDR': int_ip if int_ip is not None else '',\n#         'INTERNAL_IP_NET': int_net if int_net is not None else '',\n#         'INTERNAL_IP6_ADDR': int_ip6 if int_ip6 is not None else '',\n#         'INTERNAL_IP6_NET': int_net6 if int_net6 is not None else '',\n#         'EXTERNAL_IP_ADDR': ext_ip if ext_ip is not None else int_ip if int_ip is not None else '',\n#         'EXTERNAL_IP6_ADDR': ext_ip6 if ext_ip6 is not None else int_ip6 if int_ip6 is not None else '',\n#         'EXTERNAL_FQDN': ext_fqdn if ext_fqdn is not None else int_fqdn if int_fqdn is not None else ''\n#     }\n\n# DEPRECATED: updating network settings portion of this function has moved to the CLI\n#             marked for refactoring in v0.80 as shown below\n#               def syncSettings(new_fields={}):\ndef syncSettings(new_fields={}, update_net=False):\n    \"\"\"\n    Synchronize settings.py with shared mem / db\n\n    :param new_fields:      fields to override when syncing\n    :type new_fields:       dict\n    :return:                None\n    :rtype:                 None\n    \"\"\"\n\n    try:\n        # sync settings from settings.py\n        if settings.LOAD_SETTINGS_FROM == 'file' or settings.DSIP_ID is None:\n            # need to grab any changes on disk b4 merging\n            reload(settings)\n\n        # if DSIP_ID is not set, generate it\n        if settings.DSIP_ID is None:\n            with open('/etc/machine-id', 'r') as f:\n                settings.DSIP_ID = Credentials.hashCreds(f.read().rstrip())\n\n        # if HOMER_ID is not set, generate it\n        if settings.HOMER_ID is None:\n            with open('/etc/machine-id', 'r') as f:\n                settings.HOMER_ID = int(Credentials.hashCreds(f.read().rstrip(), dklen=4)[0:8], 16)\n\n        # sync settings from settings.py\n        if settings.LOAD_SETTINGS_FROM == 'file':\n\n            # format fields for DB\n            fields = settingsToTableFormat(settings, updates=new_fields)\n\n            # update the table\n            updateDsipSettingsTable(fields)\n\n            # revert db formatting\n            fields = settingsTableToDict(fields)\n\n        # sync settings from dsip_settings table\n        elif settings.LOAD_SETTINGS_FROM == 'db':\n            fields = getDsipSettingsTableAsDict(settings.DSIP_ID, updates=new_fields)\n\n        # no configured storage device to sync settings to/from\n        else:\n            raise ValueError('invalid value for LOAD_SETTINGS_FROM, acceptable values: \"file\" or \"db\"')\n\n        # update and reload settings file\n        if len(fields) > 0:\n            updateConfig(settings, fields, hot_reload=True)\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        IO.printerr('Could Not Update dsip_settings Database Table')\n        raise\n\n\ndef sigHandler(signum=None, frame=None):\n    \"\"\" Logic for trapped signals \"\"\"\n    # ignore SIGHUP\n    if signum == signal.SIGHUP.value:\n        IO.printwarn(\"Received SIGHUP.. ignoring signal\")\n        IO.logwarn(\"Received SIGHUP.. ignoring signal\")\n    # sync settings from db or file\n    elif signum == signal.SIGUSR1.value:\n        IO.printdbg(\"Received SIGUSR1 syncing settings from {}\".format(settings.LOAD_SETTINGS_FROM))\n        IO.logdbg(\"Received SIGUSR1 syncing settings from {}\".format(settings.LOAD_SETTINGS_FROM))\n        syncSettings()\n    # sync settings from shared memory\n    elif signum == signal.SIGUSR2.value:\n        IO.printdbg(\"Received SIGUSR2 syncing settings from shared memory\")\n        IO.logdbg(\"Received SIGUSR2 syncing settings from shared memory\")\n        shared_settings = dict(getSharedMemoryDict(SETTINGS_SHMEM_NAME).items())\n        syncSettings(shared_settings)\n    # run teardown logic before exiting\n    elif signum == signal.SIGINT.value:\n        IO.printdbg(\"Received SIGINT tearing down services\")\n        IO.logdbg(\"Received SIGINT tearing down services\")\n        teardown()\n        sys.exit(0)\n    # run teardown logic before exiting\n    elif signum == signal.SIGTERM.value:\n        IO.printdbg(\"Received SIGTERM tearing down services\")\n        IO.logdbg(\"Received SIGTERM tearing down services\")\n        teardown()\n        sys.exit(0)\n\n\ndef replaceAppLoggers(log_handler):\n    \"\"\" Handle configuration of web server loggers \"\"\"\n\n    # close current log handlers\n    for handler in copy(logging.getLogger('werkzeug').handlers):\n        logging.getLogger('werkzeug').removeHandler(handler)\n        handler.close()\n    for handler in copy(logging.getLogger('sqlalchemy').handlers):\n        logging.getLogger('sqlalchemy').removeHandler(handler)\n        handler.close()\n\n    # replace vanilla werkzeug and sqlalchemy log handler\n    logging.getLogger('werkzeug').addHandler(log_handler)\n    logging.getLogger('werkzeug').setLevel(settings.DSIP_LOG_LEVEL)\n    logging.getLogger('sqlalchemy.engine').addHandler(log_handler)\n    logging.getLogger('sqlalchemy.engine').setLevel(settings.DSIP_LOG_LEVEL)\n    logging.getLogger('sqlalchemy.dialects').addHandler(log_handler)\n    logging.getLogger('sqlalchemy.dialects').setLevel(settings.DSIP_LOG_LEVEL)\n    logging.getLogger('sqlalchemy.pool').addHandler(log_handler)\n    logging.getLogger('sqlalchemy.pool').setLevel(settings.DSIP_LOG_LEVEL)\n    logging.getLogger('sqlalchemy.orm').addHandler(log_handler)\n    logging.getLogger('sqlalchemy.orm').setLevel(settings.DSIP_LOG_LEVEL)\n\n\ndef intializeGlobalSettings():\n    \"\"\"\n    Initialize global settings and make it accessible in shared memory\n\n    :return:    None\n    :rtype:     None\n    \"\"\"\n\n    syncSettings()\n\n    updated_settings = objToDict(settings)\n    createSharedMemoryDict(updated_settings, name=SETTINGS_SHMEM_NAME)\n\n    if settings.DEBUG:\n        IO.printinfo(f'global settings initialized: {updated_settings}')\n\n\ndef intializeGlobalState():\n    \"\"\"\n    Initialize global state and make it accessible in shared memory\n\n    :return:    None\n    :rtype:     None\n    \"\"\"\n\n    # create/update the shared state file\n    # if the state got corrupted throw it away and start fresh\n    # NOTE: upgrade process does not daemonize, it is always gone on startup\n    try:\n        state = updatePersistentState({\n            'dsip_reload_ongoing': False,\n            'dsip_reload_required': False,\n            'dsip_upgrade_ongoing': False,\n        })\n    except json.JSONDecodeError:\n        state = {}\n\n    # license checks are always performed on startup, not loaded from state file\n    state['dsip_license_store'] = licenseDictToStateDict(settings.DSIP_LICENSE_STORE)\n    state['kam_reload_required'] = state.get('kam_reload_required', False)\n    state['dsip_reload_required'] = state.get('dsip_reload_required', False)\n    state['dsip_reload_ongoing'] = state.get('dsip_reload_ongoing', False)\n    state['dsip_upgrade_ongoing'] = state.get('dsip_upgrade_ongoing', False)\n\n    createSharedMemoryDict(state, name=STATE_SHMEM_NAME)\n\n    if settings.DEBUG:\n        IO.printinfo(f'global state initialized: {state}')\n\ndef intializeAuthModules():\n    global auth_modules\n\n    for modname in settings.AUTH_MODULES.keys():\n        # Use the Base Dir to specify the location of the plugin required for this domain\n        spec = importlib.util.spec_from_file_location(\n            f'auth.{modname}',\n            f'{settings.DSIP_PROJECT_DIR}/gui/modules/api/auth/{modname}/interface.py'\n        )\n        auth_mod = importlib.util.module_from_spec(spec)\n        spec.loader.exec_module(auth_mod)\n        auth_mod.initialize()\n        auth_modules.append(auth_mod)\n\ndef guiLicenseCheck(tag):\n    global state\n    return getLicenseStatusFromStateDict(state['dsip_license_store'], tag)\n\n\ndef initApp(flask_app):\n    # trap signals we handle as soon as possible.\n    # we do not want any allocated shared memory / DB connections / socket files hanging around on startup failure\n    signal.signal(signal.SIGHUP, sigHandler)\n    signal.signal(signal.SIGUSR1, sigHandler)\n    signal.signal(signal.SIGUSR2, sigHandler)\n    signal.signal(signal.SIGINT, sigHandler)\n    signal.signal(signal.SIGTERM, sigHandler)\n\n    # load the DB objects into memory\n    global global_db_engine, global_session_loader\n    global_db_engine, global_session_loader = createSessionObjects()\n    # make sure we did not break the global naming contract with the database module\n    assert DB_ENGINE_NAME in globals() and SESSION_LOADER_NAME in globals()\n\n    # Setup the Flask session manager with a random secret key\n    if settings.DSIP_SESSION_KEY is None:\n        flask_app.secret_key = os.urandom(32)\n    else:\n        flask_app.secret_key = AES_CTR.decrypt(settings.DSIP_SESSION_KEY, decode=False)\n\n    # Setup Flask timed url serializer\n    flask_app.config['EMAIL_SALT'] = urandomChars()\n    flask_app.config['TIMED_SERIALIZER'] = URLSafeTimedSerializer(flask_app.config[\"SECRET_KEY\"])\n\n    # Add jinja2 filters\n    flask_app.jinja_env.filters[\"attrFilter\"] = attrFilter\n    flask_app.jinja_env.filters[\"yesOrNoFilter\"] = yesOrNoFilter\n    flask_app.jinja_env.filters[\"noneFilter\"] = noneFilter\n    flask_app.jinja_env.filters[\"imgFilter\"] = imgFilter\n    flask_app.jinja_env.filters[\"domainTypeFilter\"] = domainTypeFilter\n\n    # Add jinja2 functions (these must be independent from the context processor)\n    flask_app.jinja_env.globals.update(zip=zip)\n    flask_app.jinja_env.globals.update(jsonLoads=json.loads)\n\n    # Dynamically update settings\n    intializeGlobalSettings()\n\n    # Reload Kamailio with the settings from dSIPRouter settings config\n    reloadKamailio()\n\n    # configs depending on updated settings go here\n    # DEPRECATED: flask_app.env is deprecated and only flask_app.debug will be used in the future, marked for removal in v0.80\n    flask_app.env = \"development\" if settings.DEBUG else \"production\"\n    flask_app.debug = settings.DEBUG\n\n    # DEPRECATED: class interface changed and will be removed in Flask 2.3, marked for review in v0.80\n    #             customize 'app.json_provider_class' or 'app.json' instead\n    # Set flask JSON encoder\n    flask_app.json_encoder = CreateEncoder()\n\n    # Set session / cookie options\n    flask_app.config.update(\n        SESSION_PROTECTION='strong',\n        SESSION_COOKIE_SECURE=True,\n        SESSION_COOKIE_HTTPONLY=True,\n        SESSION_COOKIE_SAMESITE='Lax',\n        REMEMBER_COOKIE_SECURE=True,\n        REMEMBER_COOKIE_HTTPONLY=True,\n        REMEMBER_COOKIE_DURATION=datetime.timedelta(minutes=settings.GUI_INACTIVE_TIMEOUT),\n        PERMANENT_SESSION_LIFETIME=datetime.timedelta(minutes=settings.GUI_INACTIVE_TIMEOUT),\n    )\n\n    # Setup ProxyFix middleware\n    flask_app = ProxyFix(flask_app, 1, 1, 1, 1, 1)\n\n    # change umask to 660 before creating sockets\n    # this allows members of the dsiprouter group access\n    os.umask(~0o660 & 0o777)\n\n    # remove sockets / PID file from previous runs (only possible on SIGKILL or system halt)\n    if os.path.exists(settings.DSIP_UNIX_SOCK):\n        os.remove(settings.DSIP_UNIX_SOCK)\n\n    # Initialize global variables based on persistent state\n    intializeGlobalState()\n\n    # Initialize authentication modules\n    intializeAuthModules()\n\n    # write out the main proc's PID\n    with open(settings.DSIP_PID_FILE, 'w') as pidfd:\n        pidfd.write(str(os.getpid()))\n\n    # start the Flask App server\n    bjoern.run(flask_app, 'unix:{}'.format(settings.DSIP_UNIX_SOCK), reuse_port=True)\n\n\ndef teardown():\n    global global_db_engine\n\n    try:\n        setPersistentState(objToDict(globals))\n    except:\n        pass\n    try:\n        for auth_mod in auth_modules:\n            auth_mod.teardown()\n    except:\n        pass\n    try:\n        getSharedMemoryDict(SETTINGS_SHMEM_NAME).unlink()\n    except:\n        pass\n    try:\n        getSharedMemoryDict(STATE_SHMEM_NAME).unlink()\n    except:\n        pass\n    try:\n        close_all_sessions()\n    except:\n        pass\n    try:\n        global_db_engine.dispose(close=True)\n    except:\n        pass\n    try:\n        os.remove(settings.DSIP_PID_FILE)\n    except:\n        pass\n\n\ndef main():\n    try:\n        # start the main proc here\n        # from this point on shutdown is handled by signalling to the main proc\n        initApp(app)\n        # should not be reachable, we always exit with an exception or signal handler\n        IO.printerr('a fatal logic error occurred, bypassing the signal handler')\n        IO.logerr('a fatal logic error occurred, bypassing the signal handler')\n        sys.exit(1)\n    except SystemExit as ex:\n        # normal exit, call underlying C-API with exit code\n        if ex.code == 0:\n            IO.printinfo('exited successfully')\n            IO.loginfo('exited successfully')\n            os._exit(0)\n        # an exception bubbled up, allow caller to handler it\n        raise\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "gui/dsiprouter_cron.py",
    "content": "#!/opt/dsiprouter/venv/bin/python\n#\n# summary:\n#   dsiprouter cronjob interface script\n# usage:\n#   /opt/dsiprouter/gui/dsiprouter_cron.py <module> <command> [<args...>]\n# supported modules:\n#   api\n#   cdr\n#   fusionpbx\n# supported commands:\n#   api         -   cleanleases\n#               -   synclicenses\n#   cdr         -   sendreport <gwgroupid>\n#   fusionpbx   -   sync\n#\n\nimport sys\n\n\nif __name__ == '__main__':\n    args = sys.argv[1:]\n    if len(args) < 2:\n        sys.exit(1)\n    mod = args.pop(0)\n    cmd = args.pop(0)\n\n    if mod == 'api':\n        if cmd == 'cleanleases':\n            from modules.api.cron_functions import cleanupLeases\n            cleanupLeases()\n            sys.exit(0)\n        elif cmd == 'synclicenses':\n            from modules.api.licensemanager.functions import syncLicensesToGlobalState\n            syncLicensesToGlobalState()\n            sys.exit(0)\n\n    elif mod == 'cdr':\n        if cmd == 'sendreport' and len(args) > 0:\n            from modules.cdr.cron_functions import sendCdrReport\n            gwgroupid = args[0]\n            sendCdrReport(gwgroupid)\n            sys.exit(0)\n\n    elif mod == 'fusionpbx':\n        if cmd == 'sync':\n            from modules.fusionpbx.fusionpbx_sync_functions import run_sync\n            import settings\n            run_sync(settings)\n            sys.exit(0)\n\n    sys.exit(1)\n"
  },
  {
    "path": "gui/dsiprouter_ut.py",
    "content": "import unittest\nfrom modules.domain.domain_service import *\n\nclass TestUnit(unittest.TestCase):\n\n    def setUp(self):\n        pass\n    \n    def test_add_static_domain(self):\n        self.assertTrue(addDomain('xyc.com'))\n\n    def test_getDomains(self):\n        res = getDomains()\n        assert res is not None \n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "gui/modules/api/api.sql",
    "content": "DROP TABLE IF EXISTS `dsip_endpoint_lease`;\nCREATE TABLE `dsip_endpoint_lease` (\n\t  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n\t  `gwid` int(10) unsigned NOT NULL,\n\t  `sid` int(10) unsigned NOT NULL,\n\t  `expiration` datetime NOT NULL,\n\t  PRIMARY KEY (`id`)\n);\n\n\n\nDROP TABLE IF EXISTS `dsip_user`;\nCREATE TABLE `dsip_user` (\n  `id` INT NOT NULL auto_increment unique,\n  `firstname` VARCHAR(255) NOT NULL,\n  `lastname` VARCHAR(255) NULL,\n  `username` VARCHAR(255) NOT NULL unique,\n  `password` VARCHAR(255) NOT NULL,\n  `roles` VARCHAR(255) NULL,\n  `domains` VARCHAR(255) NULL,\n  `token` VARCHAR(255) NULL,\n  `token_expiration` DATETIME NULL,\n  PRIMARY KEY (`id`));\n"
  },
  {
    "path": "gui/modules/api/api_functions.py",
    "content": "# make sure the generated source files are imported instead of the template ones\nimport sys\n\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nimport re\nfrom functools import wraps\nfrom flask import jsonify, render_template, request, session\nfrom sqlalchemy import exc as sql_exceptions\nfrom werkzeug import exceptions as http_exceptions\nfrom shared import debugException, StatusCodes\nfrom util.ipc import STATE_SHMEM_NAME, getSharedMemoryDict\nfrom util.security import APIToken\nfrom modules.api.licensemanager.functions import getLicenseStatus\nimport settings\n\n\ndef createApiResponse(error=None, msg=None, kamreload=None, dsipreload=None, data=None,\n                     status_code=StatusCodes.HTTP_OK, **kwargs):\n    \"\"\"\n    Standardize a response from the API\n\n    :param error:       Type of error if one occurred (db|http|server|other)\n    :type error:        str\n    :param msg:         response message string\n    :type msg:          str\n    :param kamreload:   if kamailio requires a reload for latest changes to be live\n    :type kamreload:    bool\n    :param dsipreload:  if the entire dsiprouter platform requires a reload\n    :type dsipreload:   bool\n    :param data:        requested data to be returned, if any\n    :type data:         list\n    :param status_code: HTTP status code\n    :type status_code:  int\n    :param kwargs:      eats any extra parameters\n    :type kwargs:       None\n    :return:            a response flask can serve to the user\n    :rtype:             (:class:`~flask.Response`, int)\n    \"\"\"\n\n    if error is None:\n        error = ''\n    if msg is None:\n        msg = ''\n    if kamreload is None:\n        kamreload = getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required']\n    if dsipreload is None:\n        dsipreload = getSharedMemoryDict(STATE_SHMEM_NAME)['dsip_reload_required']\n    if data is None:\n        data = []\n    return jsonify({\n        'error': error,\n        'msg': msg,\n        'kamreload': kamreload,\n        'dsipreload': dsipreload,\n        'data': data,\n    }), status_code\n\ndef showApiError(ex, payload=None):\n    debugException(ex)\n\n    if payload is None:\n        payload = {}\n\n    if isinstance(ex, sql_exceptions.SQLAlchemyError):\n        payload['error'] = \"db\"\n        if len(str(ex)) > 0:\n            payload['msg'] = str(ex)\n        else:\n            payload['msg'] = \"Unknown DB Error Occurred\"\n        status_code = StatusCodes.HTTP_INTERNAL_SERVER_ERROR\n    elif isinstance(ex, http_exceptions.HTTPException):\n        payload['error'] = \"http\"\n        if len(ex.description) > 0:\n            payload['msg'] = ex.description\n        else:\n            payload['msg'] = \"Unknown HTTP Error Occurred\"\n        status_code = ex.code or StatusCodes.HTTP_INTERNAL_SERVER_ERROR\n    else:\n        payload['error'] = \"server\"\n        if len(str(ex)) > 0:\n            payload['msg'] = str(ex)\n        else:\n            payload['msg'] = \"Unknown Error Occurred\"\n        status_code = StatusCodes.HTTP_INTERNAL_SERVER_ERROR\n\n    return createApiResponse(**payload, status_code=status_code)\n\ndef api_security(func):\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        apiToken = APIToken(request)\n        accept_header = request.headers.get('Accept', '')\n\n        # If user is logged into a session return right away\n        if session.get('logged_in'):\n            return func(*args, **kwargs)\n        else:\n            if 'text/html' in accept_header:\n                return render_template('index.html', version=settings.VERSION), StatusCodes.HTTP_UNAUTHORIZED\n\n        # If API Request check for license\n        if not re.match('text/html|text/css', accept_header, flags=re.IGNORECASE):\n            flask_rule = request.url_rule.rule if request.url_rule is not None else ''\n            # Check if they have a Core Subscription\n            if flask_rule[:17] != '/api/v1/licensing' and getLicenseStatus(license_tag='DSIP_CORE') != 3:\n                return createApiResponse(\n                    error='http',\n                    msg='Unauthorized - Core Subscription Required. Purchase from https://dopensource.com/product/dsiprouter-core/',\n                    status_code=StatusCodes.HTTP_UNAUTHORIZED\n                )\n            # Check if token is valid\n            if not apiToken.isValid():\n                return createApiResponse(\n                    error='http',\n                    msg='Unauthorized',\n                    status_code=StatusCodes.HTTP_UNAUTHORIZED\n                )\n            # checks succeeded allow the request\n            return func(*args, **kwargs)\n\n    return wrapper\n"
  },
  {
    "path": "gui/modules/api/api_routes.py",
    "content": "# make sure the generated source files are imported instead of the template ones\nimport sys\n\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nimport os, time, random, subprocess, requests, csv, base64, codecs, re, socket, json\nfrom contextlib import closing\nfrom datetime import datetime\nfrom flask import Blueprint, jsonify, request, send_file, g\nfrom sqlalchemy import exc as sql_exceptions, and_, or_\nfrom sqlalchemy.sql import text\nfrom werkzeug import exceptions as http_exceptions\nfrom werkzeug.utils import secure_filename\nfrom database import startSession, DummySession, Address, dSIPNotification, dSIPMultiDomainMapping, Gateways, \\\n    GatewayGroups, Subscribers, dSIPLeases, dSIPMaintModes, dSIPCallSettings, InboundMapping, dSIPCDRInfo, \\\n    dSIPCertificates, Dispatcher, dSIPDNIDEnrichment\nfrom shared import allowed_file, dictToStrFields, isCertValid, rowToDict, debugEndpoint, StatusCodes, \\\n    strFieldsToDict, getRequestData, IO\nfrom util.pyasync import daemonize\nfrom util.ipc import STATE_SHMEM_NAME, getSharedMemoryDict\nfrom modules.api.api_functions import createApiResponse, showApiError, api_security\nfrom modules.api.kamailio.functions import reloadKamailio\nfrom util.networking import getExternalIP, hostToIP, safeUriToHost, safeStripPort\nfrom util.notifications import sendEmail\nfrom util.security import AES_CTR, urandomChars, KeyCertPair\nfrom util.file_handling import change_owner\nfrom util import kamtls, letsencrypt\nfrom util.cron import addTaggedCronjob, updateTaggedCronjob, deleteTaggedCronjob\nfrom sysloginit import initSyslogLogger\nimport settings\n\napi = Blueprint('api', __name__)\n\n\n# TODO: we need to abstract out common code between gui and api\n\n\n@api.route(\"/api/v1/kamailio/stats\", methods=['GET'])\n@api_security\ndef getKamailioStats():\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        jsonrpc_payload = {\"jsonrpc\": \"2.0\", \"method\": \"tm.stats\", \"id\": 1}\n        r = requests.get('http://127.0.0.1:5060/api/kamailio', json=jsonrpc_payload)\n        if r.status_code >= 400:\n            ex = http_exceptions.HTTPException(r.reason)\n            ex.code = r.status_code\n            raise ex\n\n        return createApiResponse(\n            msg='Successfully retrieved kamailio stats',\n            data=[r.json()['result']],\n        )\n\n    except Exception as ex:\n        return showApiError(ex)\n\n\n@api.route(\"/api/v1/reload/kamailio\", methods=['POST'])\n@api_security\ndef handleReloadKamailio():\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        reloadKamailio()\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = False\n        return createApiResponse(\n            msg='Kamailio reload succeeded',\n            kamreload=False,\n        )\n\n    except Exception as ex:\n        return showApiError(ex)\n\n# TODO: state file is not thread/process safe, move to shared memory manager\n@api.route(\"/api/v1/reload/dsiprouter\", methods=['GET', 'POST'])\n@api_security\ndef handleReloadDsiprouter():\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        # check on a current reload\n        if request.method == 'GET':\n            if getSharedMemoryDict(STATE_SHMEM_NAME)['dsip_reload_ongoing']:\n                return createApiResponse(\n                    msg='dSIPRouter reload in progress',\n                    data=[False],\n                    status_code=StatusCodes.HTTP_ACCEPTED,\n                )\n\n            return createApiResponse(\n                msg='dSIPRouter reload complete',\n                data=[True],\n            )\n        # try the reload\n        elif request.method == 'POST':\n            if getSharedMemoryDict(STATE_SHMEM_NAME)['dsip_reload_ongoing']:\n                return createApiResponse(\n                    msg='dSIPRouter reload in progress',\n                    status_code=StatusCodes.HTTP_ACCEPTED,\n                )\n\n            # update globals before we reload so server stores them on teardown\n            getSharedMemoryDict(STATE_SHMEM_NAME)['dsip_reload_ongoing'] = True\n            getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = False\n            getSharedMemoryDict(STATE_SHMEM_NAME)['dsip_reload_required'] = False\n\n            daemonize(['sudo', 'dsiprouter', 'restart', '-all', '-daemonize'])\n\n            return createApiResponse(\n                msg='dSIPRouter reload started',\n            )\n        # method not allowed\n        else:\n            return createApiResponse(\n                msg='Invalid HTTP method for this route',\n                status_code=StatusCodes.HTTP_METHOD_NOT_ALLOWED,\n            )\n\n    except Exception as ex:\n        return showApiError(ex)\n\n\n# TODO: this func actually adds an endpoint lease\n#       it should be renamed and changed to use POST method\n# TODO: the last lease id/username generated must be tracked (just query DB)\n#       and used to determine next lease id, otherwise conflicts may occur\n@api.route(\"/api/v1/lease/endpoint\", methods=['GET'])\n@api_security\ndef getEndpointLease():\n    db = DummySession()\n\n    DEF_PASSWORD_LEN = 32\n\n    lease_data = {}\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        email = request.args.get('email')\n        if not email:\n            raise Exception(\"email parameter is missing\")\n\n        ttl = request.args.get('ttl', None)\n        if ttl is None:\n            raise http_exceptions.BadRequest(\"time to live (ttl) parameter is missing\")\n\n        # Convert TTL to Seconds\n        r = re.compile('\\d*m|M')\n        if r.match(ttl):\n            ttl = 60 * int(ttl[0:(len(ttl) - 1)])\n\n        # Generate some values\n        rand_num = random.randint(1, 200)\n        name = \"lease\" + str(rand_num)\n        auth_username = name\n        auth_password = urandomChars(DEF_PASSWORD_LEN)\n        auth_domain = settings.DEFAULT_AUTH_DOMAIN\n\n        # Set some defaults\n        host_addr = ''\n        strip = 0\n        prefix = ''\n\n        # Add the Gateways table\n        Gateway = Gateways(name, host_addr, strip, prefix, settings.FLT_PBX)\n        db.add(Gateway)\n        db.flush()\n\n        # Add the Subscribers table\n        Subscriber = Subscribers(auth_username, auth_password, auth_domain, Gateway.gwid, email)\n        db.add(Subscriber)\n        db.flush()\n\n        # Add to the Leases table\n        Lease = dSIPLeases(Gateway.gwid, Subscriber.id, int(ttl))\n        db.add(Lease)\n        db.flush()\n\n        lease_data['leaseid'] = Lease.id\n        lease_data['username'] = auth_username\n        lease_data['password'] = auth_password\n        lease_data['domain'] = auth_domain\n        lease_data['ttl'] = ttl\n\n        db.commit()\n\n        # Install Cron job to clean up the leases\n        #cron_cmd = '{} cleanleases'.format(settings.DSIP_PROJECT_DIR + '/gui/dsiprouter_cron.py')\n        #if not addTaggedCronjob(\"lease_management\", \"* * * * *\", cron_cmd):\n        #    raise Exception('Crontab entry could not be created')\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return createApiResponse(\n            msg='Lease created',\n            data=[lease_data],\n            kamreload=True,\n        )\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@api.route(\"/api/v1/lease/endpoint/<int:leaseid>/revoke\", methods=['DELETE'])\n@api_security\ndef revokeEndpointLease(leaseid):\n    db = DummySession()\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        # Query the Lease ID\n        Lease = db.query(dSIPLeases).filter(dSIPLeases.id == leaseid).first()\n\n        # Remove the entry in the Subscribers table\n        Subscriber = db.query(Subscribers).filter(Subscribers.id == Lease.sid).first()\n        db.delete(Subscriber)\n\n        # Remove the entry in the Gateway table\n        Gateway = db.query(Gateways).filter(Gateways.gwid == Lease.gwid).first()\n        db.delete(Gateway)\n\n        # Remove the entry in the Lease table\n        db.delete(Lease)\n\n        db.commit()\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return createApiResponse(\n            msg='Lease revoked',\n            kamreload=True,\n        )\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n# TODO: is this endpoint still used?\n@api.route(\"/api/v1/endpoint/<int:id>\", methods=['POST'])\n@api_security\ndef updateEndpoint(id):\n    db = DummySession()\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        # Covert JSON message to Dictionary Object\n        request_payload = getRequestData()\n\n        # Only handling the maintmode attribute on this release\n        if 'maintmode' not in request_payload:\n            raise http_exceptions.BadRequest('maintmode attribute missing')\n\n        if request_payload['maintmode'] == 0:\n            MaintMode = db.query(dSIPMaintModes).filter(dSIPMaintModes.gwid == id).first()\n            if MaintMode:\n                db.delete(MaintMode)\n        elif request_payload['maintmode'] == 1:\n            # Lookup Gateway ip adddess\n            Gateway = db.query(Gateways).filter(Gateways.gwid == id).first()\n            if Gateway != None:\n                MaintMode = dSIPMaintModes(Gateway.address, id)\n                db.add(MaintMode)\n\n        db.commit()\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return createApiResponse(\n            msg='Endpoint updated',\n            kamreload=True,\n        )\n\n    except sql_exceptions.IntegrityError as ex:\n        db.rollback()\n        db.flush()\n        payload = {'msg': \"endpoint {} is already in maintmode\".format(id)}\n        return showApiError(ex, payload)\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n# TODO: we should optimize this and cleanup reused code\n@api.route(\"/api/v1/inboundmapping\", methods=['GET', 'POST', 'PUT', 'DELETE'])\n@api_security\ndef handleInboundMapping():\n    \"\"\"\n    Endpoint for Inbound DID Rule Mapping\n    \"\"\"\n\n    db = DummySession()\n\n    # use a whitelist to avoid possible SQL Injection vulns\n    VALID_REQUEST_ARGS = {'ruleid', 'did'}\n\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    VALID_REQUEST_DATA_ARGS = {'did', 'servers', 'name'}\n\n    payload = {'data': []}\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        # =========================================\n        # get rule for DID mapping or list of rules\n        # =========================================\n        if request.method == \"GET\":\n            # sanity check\n            for arg in request.args:\n                if arg not in VALID_REQUEST_ARGS:\n                    raise http_exceptions.BadRequest(\"Request argument not recognized\")\n\n            # get single rule by ruleid\n            rule_id = request.args.get('ruleid')\n            if rule_id is not None:\n                res = db.query(InboundMapping).filter(InboundMapping.groupid == settings.FLT_INBOUND).filter(\n                    InboundMapping.ruleid == rule_id).first()\n                if res is not None:\n                    data = rowToDict(res)\n                    data = {'ruleid': data['ruleid'], 'did': data['prefix'],\n                            'name': strFieldsToDict(data['description'])['name'] if 'name' in strFieldsToDict(\n                                data['description']) else '',\n                            'servers': data['gwlist'].split(',')}\n                    payload['data'].append(data)\n                    payload['msg'] = 'Rule Found'\n                else:\n                    payload['msg'] = 'No Matching Rule Found'\n                    payload['status_code'] = StatusCodes.HTTP_NOT_FOUND\n\n            # get single rule by did\n            else:\n                did_pattern = request.args.get('did')\n                if did_pattern is not None:\n                    res = db.query(InboundMapping).filter(InboundMapping.groupid == settings.FLT_INBOUND).filter(\n                        InboundMapping.prefix == did_pattern).first()\n                    if res is not None:\n                        data = rowToDict(res)\n                        data = {'ruleid': data['ruleid'], 'did': data['prefix'],\n                                'name': strFieldsToDict(data['description'])['name'] if 'name' in strFieldsToDict(\n                                    data['description']) else '',\n                                'servers': data['gwlist'].split(',')}\n                        payload['data'].append(data)\n                        payload['msg'] = 'DID Found'\n                    else:\n                        payload['msg'] = 'No Matching DID Found'\n                        payload['status_code'] = StatusCodes.HTTP_NOT_FOUND\n\n                # get list of rules\n                else:\n                    res = db.query(InboundMapping).filter(InboundMapping.groupid == settings.FLT_INBOUND).all()\n                    if len(res) > 0:\n                        for row in res:\n                            data = rowToDict(row)\n                            data = {'ruleid': data['ruleid'], 'did': data['prefix'],\n                                    'name': strFieldsToDict(data['description'])['name'] if 'name' in strFieldsToDict(\n                                        data['description']) else '',\n                                    'servers': data['gwlist'].split(',')}\n                            payload['data'].append(data)\n                        payload['msg'] = 'Rules Found'\n                    else:\n                        payload['msg'] = 'No Rules Found'\n\n            return createApiResponse(**payload)\n\n        # ===========================\n        # create rule for DID mapping\n        # ===========================\n        elif request.method == \"POST\":\n            data = getRequestData()\n\n            # sanity checks\n            for arg in data:\n                if arg not in VALID_REQUEST_DATA_ARGS:\n                    raise http_exceptions.BadRequest(\"Request data argument not recognized\")\n\n            if 'servers' not in data:\n                raise http_exceptions.BadRequest('Servers to map DID to are required')\n            elif len(data['servers']) < 1 or len(data['servers']) > 2:\n                raise http_exceptions.BadRequest('Primary Server missing or More than 2 Servers Provided')\n            elif 'did' not in data:\n                raise http_exceptions.BadRequest('DID is required')\n\n            # TODO: we should be checking dr_gateways table to make sure the servers exist\n            for i in range(0, len(data['servers'])):\n                try:\n                    data['servers'][i] = str(data['servers'][i])\n                    # _ = int(data['servers'][i])\n                except:\n                    raise http_exceptions.BadRequest('Invalid Server ID')\n            for c in data['did']:\n                if c not in settings.DID_PREFIX_ALLOWED_CHARS:\n                    raise http_exceptions.BadRequest(\n                        'DID improperly formatted. Allowed characters: {}'.format(\n                            ','.join(settings.DID_PREFIX_ALLOWED_CHARS)))\n\n            gwlist = ','.join(data['servers'])\n            prefix = data['did']\n            description = 'name:{}'.format(data['name']) if 'name' in data else ''\n\n            # don't allow duplicate entries\n            if db.query(InboundMapping).filter(InboundMapping.prefix == prefix).filter(\n                InboundMapping.groupid == settings.FLT_INBOUND).scalar():\n                raise http_exceptions.BadRequest(\"Duplicate DID's are not allowed\")\n            IMap = InboundMapping(settings.FLT_INBOUND, prefix, gwlist, description)\n            db.add(IMap)\n\n            db.commit()\n            getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n            payload['kamreload'] = getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required']\n            payload['msg'] = 'Rule Created'\n            return createApiResponse(**payload)\n\n        # ===========================\n        # update rule for DID mapping\n        # ===========================\n        elif request.method == \"PUT\":\n            # sanity check\n            for arg in request.args:\n                if arg not in VALID_REQUEST_ARGS:\n                    raise http_exceptions.BadRequest(\"Request argument not recognized\")\n\n            data = getRequestData()\n            updates = {}\n\n            # sanity checks\n            for arg in data:\n                if arg not in VALID_REQUEST_DATA_ARGS:\n                    raise http_exceptions.BadRequest(\"Request data argument not recognized\")\n\n            if 'did' not in data and 'servers' not in data and 'name' not in data:\n                raise http_exceptions.BadRequest(\"No data args supplied, {did, and servers} is required\")\n            # TODO: we should be checking dr_gateways table to make sure the servers exist\n            if 'servers' in data:\n                if len(data['servers']) < 1 or len(data['servers']) > 2:\n                    raise http_exceptions.BadRequest('Primary Server missing or More than 2 Servers Provided')\n                else:\n                    for i in range(0, len(data['servers'])):\n                        try:\n                            data['servers'][i] = str(data['servers'][i])\n                            # _ = int(data['servers'][i])\n                        except:\n                            raise http_exceptions.BadRequest('Invalid Server ID')\n                updates['gwlist'] = ','.join(data['servers'])\n            if 'did' in data:\n                for c in data['did']:\n                    if c not in settings.DID_PREFIX_ALLOWED_CHARS:\n                        raise http_exceptions.BadRequest(\n                            'DID improperly formatted. Allowed characters: {}'.format(\n                                ','.join(settings.DID_PREFIX_ALLOWED_CHARS)))\n                updates['prefix'] = data['did']\n            if 'name' in data:\n                updates['description'] = 'name:{}'.format(data['name'])\n\n            # update single rule by ruleid\n            rule_id = request.args.get('ruleid')\n            if rule_id is not None:\n                res = db.query(InboundMapping).filter(InboundMapping.groupid == settings.FLT_INBOUND).filter(\n                    InboundMapping.ruleid == rule_id).update(\n                    updates, synchronize_session=False)\n                if res > 0:\n                    payload['msg'] = 'Rule Updated'\n                else:\n                    payload['msg'] = 'No Matching Rule Found'\n\n            # update single rule by did\n            else:\n                did_pattern = request.args.get('did')\n                if did_pattern is not None:\n                    res = db.query(InboundMapping).filter(InboundMapping.groupid == settings.FLT_INBOUND).filter(\n                        InboundMapping.prefix == did_pattern).update(\n                        updates, synchronize_session=False)\n                    if res > 0:\n                        payload['msg'] = 'Rule Updated'\n                    else:\n                        payload['msg'] = 'No Matching Rule Found'\n\n                # no other options\n                else:\n                    raise http_exceptions.BadRequest('One of the following is required: {ruleid, or did}')\n\n            db.commit()\n            getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n            payload['kamreload'] = getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required']\n            return createApiResponse(**payload)\n\n        # ===========================\n        # delete rule for DID mapping\n        # ===========================\n        elif request.method == \"DELETE\":\n            # sanity check\n            for arg in request.args:\n                if arg not in VALID_REQUEST_ARGS:\n                    raise http_exceptions.BadRequest(\"Request argument not recognized\")\n\n            # delete single rule by ruleid\n            rule_id = request.args.get('ruleid')\n            if rule_id is not None:\n                rule = db.query(InboundMapping).filter(InboundMapping.groupid == settings.FLT_INBOUND).filter(\n                    InboundMapping.ruleid == rule_id)\n                if rule.delete(synchronize_session=False) == 0:\n                    payload['msg'] = 'No Rules Found'\n                    payload['status_code'] = StatusCodes.HTTP_NOT_FOUND\n\n            # delete single rule by did\n            else:\n                did_pattern = request.args.get('did')\n                if did_pattern is not None:\n                    rule = db.query(InboundMapping).filter(InboundMapping.groupid == settings.FLT_INBOUND).filter(\n                        InboundMapping.prefix == did_pattern)\n                    if rule.delete(synchronize_session=False) == 0:\n                        payload['msg'] = 'No Rules Found'\n                        payload['status_code'] = StatusCodes.HTTP_NOT_FOUND\n\n                # no other options\n                else:\n                    raise http_exceptions.BadRequest('One of the following is required: {ruleid, or did}')\n\n            db.commit()\n            getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n            payload['kamreload'] = True\n            payload['msg'] = 'Rule Deleted'\n            return createApiResponse(**payload)\n\n        # not possible\n        else:\n            payload['msg'] = 'Invalid HTTP method for this route'\n            return createApiResponse(\n                **payload,\n                status_code=StatusCodes.HTTP_METHOD_NOT_ALLOWED\n            )\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@api.route(\"/api/v1/notification/gwgroup\", methods=['POST'])\n@api_security\ndef handleNotificationRequest():\n    \"\"\"\n    Endpoint for Sending Notifications\n    \"\"\"\n\n    db = DummySession()\n\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    VALID_REQUEST_DATA_ARGS = {'gwgroupid': int, 'type': int, 'text_body': str,\n                               'gwid': int, 'subject': str, 'sender': str}\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        # ============================\n        # create and send notification\n        # ============================\n\n        data = getRequestData()\n\n        # sanity checks\n        for k, v in data.items():\n            if k not in VALID_REQUEST_DATA_ARGS.keys():\n                raise http_exceptions.BadRequest(\"Request data argument '{}' not recognized\".format(k))\n            if not type(v) == VALID_REQUEST_DATA_ARGS[k]:\n                raise http_exceptions.BadRequest(\"Request data argument '{}' not valid\".format(k))\n        if 'gwgroupid' not in data:\n            raise http_exceptions.BadRequest('Gateway Group ID is required')\n        elif 'type' not in data:\n            raise http_exceptions.BadRequest('Notification Type is required')\n        elif 'text_body' not in data:\n            raise http_exceptions.BadRequest('Text Body is required')\n\n        # lookup recipients\n        gwgroupid = data.pop('gwgroupid')\n        notif_type = data.pop('type')\n        notification_row = db.query(dSIPNotification).filter(dSIPNotification.gwgroupid == gwgroupid).filter(\n            dSIPNotification.type == notif_type).first()\n        if notification_row is None:\n            raise sql_exceptions.SQLAlchemyError('DB Entry Missing for {}'.format(str(gwgroupid)))\n\n        # customize message based on type\n        gwid = data.pop('gwid', None)\n        gw_row = db.query(Gateways).filter(Gateways.gwid == gwid).first() if gwid is not None else None\n        gw_name = strFieldsToDict(gw_row.description)['name'] if gw_row is not None else ''\n        gwgroup_row = db.query(GatewayGroups).filter(GatewayGroups.id == gwgroupid).first()\n        gwgroup_name = strFieldsToDict(gwgroup_row.description)['name'] if gwgroup_row is not None else ''\n        if notif_type == dSIPNotification.FLAGS.TYPE_OVERLIMIT.value:\n            data['html_body'] = (\n                '<html><head><style>.error{{border: 1px solid; margin: 10px 0px; padding: 15px 10px 15px 50px; background-color: #FF5555;}}</style></head>'\n                '<body><div class=\"error\"><strong>Call Limit Exceeded in Endpoint Group [{}] on Endpoint [{}]</strong></div></body>').format(\n                gwgroup_name, gw_name)\n        elif notif_type == dSIPNotification.FLAGS.TYPE_GWFAILURE.value:\n            data['html_body'] = (\n                '<html><head><style>.error{{border: 1px solid; margin: 10px 0px; padding: 15px 10px 15px 50px; background-color: #FF5555;}}</style></head>'\n                '<body><div class=\"error\"><strong>Failure Detected in Endpoint Group [{}] on Endpoint [{}]</strong></div></body>').format(\n                gwgroup_name, gw_name)\n\n        # # get attachments if any uploaded\n        # data['attachments'] = []\n        # if len(request.files) > 0:\n        #     for upload in request.files:\n        #         if upload.filename != '' and isValidFile(upload.filename):\n        #             data['attachments'].append(upload)\n\n        # TODO: we only support email at this time, add support for slack\n        if notification_row.method == dSIPNotification.FLAGS.METHOD_EMAIL.value:\n            data['recipients'] = [notification_row.value]\n            sendEmail(**data)\n        elif notification_row.method == dSIPNotification.FLAGS.METHOD_SLACK.value:\n            pass\n\n        return createApiResponse(msg='Email Sent')\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@api.route(\"/api/v1/endpointgroups/<int:gwgroupid>\", methods=['DELETE'])\n@api_security\ndef deleteEndpointGroup(gwgroupid):\n    db = DummySession()\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        db = startSession()\n\n        endpointgroup = db.query(GatewayGroups).filter(GatewayGroups.id == gwgroupid)\n        if endpointgroup is not None:\n            endpointgroup.delete(synchronize_session=False)\n        else:\n            raise http_exceptions.NotFound(\"The endpoint group doesn't exist\")\n\n        call_settings = db.query(dSIPCallSettings).filter(dSIPCallSettings.gwgroupid == gwgroupid)\n        if call_settings is not None:\n            call_settings.delete(synchronize_session=False)\n\n        subscriber = db.query(Subscribers).filter(Subscribers.rpid == gwgroupid)\n        if subscriber is not None:\n            subscriber.delete(synchronize_session=False)\n\n        endpoints = db.query(Gateways).filter(\n            Gateways.description.regexp_match(f'gwgroup:{gwgroupid}(,|$)')\n        )\n        if endpoints is not None:\n            address_ids = []\n            for endpoint in endpoints:\n                description_dict = strFieldsToDict(endpoint.description)\n                if 'addr_id' in description_dict:\n                    address_ids.append(description_dict['addr_id'])\n            db.query(Address).filter(Address.id.in_(address_ids)).delete(synchronize_session=False)\n            endpoints.delete(synchronize_session=False)\n\n        notifications = db.query(dSIPNotification).filter(dSIPNotification.gwgroupid == gwgroupid)\n        if notifications is not None:\n            notifications.delete(synchronize_session=False)\n\n        cdrinfo = db.query(dSIPCDRInfo).filter(dSIPCDRInfo.gwgroupid == gwgroupid)\n        if cdrinfo is not None:\n            cdrinfo.delete(synchronize_session=False)\n            deleteTaggedCronjob(gwgroupid)\n            # if not deleteTaggedCronjob(gwgroupid):\n            #    raise Exception('Crontab entry could not be deleted')\n\n        domainmapping = db.query(dSIPMultiDomainMapping).filter(dSIPMultiDomainMapping.pbx_id == gwgroupid)\n        if domainmapping.count() > 0:\n            # Get list of all domains managed by the endpoint group\n            did_list = db.execute(\n                text(\"SELECT DISTINCT did FROM domain_attrs WHERE name = 'created_by' AND value=:gwgroupid\"),\n                {'gwgroupid': gwgroupid}\n            ).all()\n\n            # Delete all domains\n            db.execute(\n                text(\"DELETE FROM domain WHERE did IN (SELECT did FROM domain_attrs WHERE name='created_by' AND value=:gwgroupid)\"),\n                {'gwgroupid': gwgroupid}\n            )\n\n            # Delete all domains_attrs\n            if len(did_list) > 0:\n                for did in did_list:\n                    db.execute(\n                        text(\"DELETE FROM domain_attrs WHERE did IN (:dids)\"),\n                        {'dids': str(did[0])}\n                    )\n\n            # Delete domain mapping, which will stop the fusionpbx sync\n            domainmapping.delete(synchronize_session=False)\n\n        dispatcher = db.query(Dispatcher).filter(\n            (Dispatcher.setid == gwgroupid) |\n            (Dispatcher.setid == int(gwgroupid) + 1000)\n        )\n\n        if dispatcher is not None:\n            dispatcher.delete(synchronize_session=False)\n\n        db.commit()\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return createApiResponse(msg='EndpointGroup deleted', kamreload=True)\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@api.route(\"/api/v1/endpointgroups/<int:gwgroupid>\", methods=['GET'])\n@api_security\ndef getEndpointGroup(gwgroupid):\n    db = DummySession()\n\n    gwgroup_data = {}\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        db = startSession()\n\n        gwgroupid = int(gwgroupid)\n\n        endpointgroup = db.query(GatewayGroups).filter(GatewayGroups.id == gwgroupid).first()\n        if endpointgroup is not None:\n            gwgroup_data['name'] = strFieldsToDict(endpointgroup.description)['name']\n        else:\n            raise http_exceptions.NotFound(\"Endpoint Group Does Not Exist\")\n\n        # Send back the gateway groupid that was requested\n        gwgroup_data['gwgroupid'] = gwgroupid\n\n        call_settings = db.query(dSIPCallSettings).filter(dSIPCallSettings.gwgroupid == gwgroupid).first()\n        if call_settings is not None:\n            gwgroup_data['call_settings'] = {\n                'limit': call_settings.limit,\n                'timeout': call_settings.timeout\n            }\n        else:\n            gwgroup_data['call_settings'] = {}\n\n            # Check to see if a subscriber record exists.  If so, auth is userpwd\n        auth = {}\n        subscriber = db.query(Subscribers).filter(Subscribers.rpid == gwgroupid).first()\n        if subscriber is not None:\n            auth['type'] = \"userpwd\"\n            auth['user'] = subscriber.username\n            auth['pass'] = subscriber.password\n            auth['domain'] = subscriber.domain\n        else:\n            auth['type'] = \"ip\"\n        gwgroup_data['auth'] = auth\n\n        gwgroup_data['endpoints'] = []\n        endpoint_weights = {}\n        endpoint_keepalives = {}\n        endpoints = db.query(Gateways).filter(\n            Gateways.gwid.in_(endpointgroup.gwlist.split(','))\n        ).all()\n        dispatcher_rows = db.query(Dispatcher).filter(\n            (Dispatcher.setid == gwgroupid) |\n            (Dispatcher.setid == gwgroupid + 1000)\n        ).all()\n        if dispatcher_rows is not None:\n            for row in dispatcher_rows:\n                dst_host = row.destination.split(':', maxsplit=1)[1]\n                attrs = row.attrsToDict()\n                if attrs.get('rweight', None) is not None:\n                    endpoint_weights[dst_host] = attrs['rweight']\n                # TODO: make this check part of the DB object methods\n                if Dispatcher.FLAGS['KEEP_ALIVE'] & row.flags:\n                    endpoint_keepalives[dst_host] = 1\n                else:\n                    endpoint_keepalives[dst_host] = 0\n        for endpoint in endpoints:\n            host_split = endpoint.address.split(':')\n            attrs_dict = endpoint.attrsToDict()\n            gwgroup_data['endpoints'].append({\n                'gwid': endpoint.gwid,\n                'host': host_split[0],\n                'port': int(host_split[1]) if len(host_split) > 1 else 5060,\n                'signalling': attrs_dict['signalling'],\n                'media': attrs_dict['media'],\n                'description': strFieldsToDict(endpoint.description)['name'],\n                'rweight': endpoint_weights[endpoint.address] if endpoint.address in endpoint_weights else 0,\n                'keepalive': endpoint_keepalives[endpoint.address] if endpoint.address in endpoint_keepalives else 0,\n                'maintmode': ''\n            })\n            gwgroup_data['strip'] = endpoint.strip\n            gwgroup_data['prefix'] = endpoint.pri_prefix\n\n        # Notifications\n        notifications = {}\n        n = db.query(dSIPNotification).filter(dSIPNotification.gwgroupid == gwgroupid)\n        for notification in n:\n            # if notification.type == dSIPNotification.FLAGS.TYPE_MAXCALLLIMIT.value:\n            if notification.type == 0:\n                notifications['overmaxcalllimit'] = notification.value\n            # if notification.type == dSIPNotification.FLAGS.TYPE_ENDPOINTFAILURE.value:\n            if notification.type == 1:\n                notifications['endpointfailure'] = notification.value\n\n        gwgroup_data['notifications'] = notifications\n\n        gwgroup_data['fusionpbx'] = {}\n        domainmapping = db.query(dSIPMultiDomainMapping).filter(dSIPMultiDomainMapping.pbx_id == gwgroupid).first()\n        if domainmapping is not None:\n            gwgroup_data['fusionpbx']['enabled'] = \"1\"\n            gwgroup_data['fusionpbx']['dbhost'] = domainmapping.db_host\n            gwgroup_data['fusionpbx']['dbuser'] = domainmapping.db_username\n            gwgroup_data['fusionpbx']['dbpass'] = domainmapping.db_password\n\n        # CDR info\n        gwgroup_data['cdr'] = {}\n        cdrinfo = db.query(dSIPCDRInfo).filter(dSIPCDRInfo.gwgroupid == gwgroupid).first()\n        if cdrinfo is not None:\n            gwgroup_data['cdr']['cdr_email'] = cdrinfo.email\n            gwgroup_data['cdr']['cdr_send_interval'] = cdrinfo.send_interval\n\n        return createApiResponse(\n            msg='Endpoint group found',\n            data=[gwgroup_data],\n        )\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n# TODO: should reuse getEndpointGroup() function and return all settings for each gwgroup\n@api.route(\"/api/v1/endpointgroups\", methods=['GET'])\n@api_security\ndef listEndpointGroups():\n    db = DummySession()\n\n    response_data = []\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        db = startSession()\n\n        endpointgroups = db.query(GatewayGroups).filter(\n            GatewayGroups.description.regexp_match(GatewayGroups.FILTER.ENDPOINT.value)\n        ).all()\n\n        for endpointgroup in endpointgroups:\n            # Grap the description field, which is comma seperated key/value pair\n            fields = strFieldsToDict(endpointgroup.description)\n\n            # append summary of endpoint group data\n            response_data.append({\n                'gwgroupid': endpointgroup.id,\n                'name': fields['name'],\n                'gwlist': endpointgroup.gwlist\n            })\n\n        return createApiResponse(\n            msg='Endpoint groups found',\n            data=response_data,\n        )\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@api.route(\"/api/v1/endpointgroups\", methods=['PUT'])\n@api.route(\"/api/v1/endpointgroups/<int:gwgroupid>\", methods=['PUT'])\n@api_security\ndef updateEndpointGroups(gwgroupid=None):\n    \"\"\"\n    Update a single Endpoint Group\\n\n    The gwgroupid can be provided in url path or payload\n\n    ===============\n    Request Payload\n    ===============\n\n    .. code-block:: json\n\n        {\n            name: <string>,\n            call_settings: {\n                limit: <int>,\n                timeout: <int>\n            },\n            auth: {\n                type: \"ip\"|\"userpwd\",\n                user: <string>\n                pass: <string>\n                domain: <string>\n            },\n            endpoints [\n                {\n                    gwid:<int>,\n                    host:<string>,\n                    port:<int>,\n                    signalling:<string>,\n                    media:<string>,\n                    description:<string>,\n                    rweight:<int>,\n                    keepalive:<int>\n                },\n                ...\n            ],\n            strip: <int>,\n            prefix: <string>,\n            notifications: {\n                overmaxcalllimit: <string>,\n                endpointfailure: <string>\n            },\n            cdr: {\n                cdr_email: <string>,\n                cdr_send_interval: <string>\n            }\n            fusionpbx: {\n                enabled: <bool>,\n                dbhost: <string>,\n                dbuser: <string>,\n                dbpass: <string>\n            }\n        }\n\n    ================\n    Response Payload\n    ================\n\n    .. code-block:: json\n\n        {\n            error: <string>,\n            msg: <string>,\n            kamreload: <bool>,\n            data: [\n                {\n                    gwgroupid: <int>,\n                    endpoints: [\n                        <int>,\n                        ...\n                    ]\n                }\n            ]\n        }\n    \"\"\"\n\n    db = DummySession()\n\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    VALID_REQUEST_DATA_ARGS = {\"gwgroupid\": int, \"name\": str, \"call_settings\": dict, \"auth\": dict,\n                               \"strip\": int, \"prefix\": str, \"notifications\": dict, \"cdr\": dict,\n                               \"fusionpbx\": dict, \"endpoints\": list}\n\n    # ensure requred args are provided\n    REQUIRED_ARGS = {'gwgroupid'}\n\n    gwgroup_data = {}\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        # get request data\n        request_payload = getRequestData()\n\n        if gwgroupid is not None:\n            gwgroupid = int(gwgroupid)\n            gwgroupid_str = str(gwgroupid)\n            request_payload['gwgroupid'] = gwgroupid\n        else:\n            if 'gwgroupid' in request_payload:\n                gwgroupid = int(request_payload['gwgroupid'])\n                request_payload['gwgroupid'] = gwgroupid\n            gwgroupid_str = str(gwgroupid)\n\n        # sanity checks\n        for k, v in request_payload.items():\n            if k not in VALID_REQUEST_DATA_ARGS.keys():\n                raise http_exceptions.BadRequest(\"Request argument '{}' not recognized\".format(k))\n            if not type(v) == VALID_REQUEST_DATA_ARGS[k]:\n                try:\n                    request_payload[k] = VALID_REQUEST_DATA_ARGS[k](v)\n                    continue\n                except:\n                    raise http_exceptions.BadRequest(\"Request argument '{}' not valid\".format(k))\n        for k in REQUIRED_ARGS:\n            if k not in REQUIRED_ARGS:\n                raise http_exceptions.BadRequest(\"Request argument '{}' is required\".format(k))\n        if 'prefix' in request_payload:\n            for c in request_payload['prefix']:\n                if c not in settings.DID_PREFIX_ALLOWED_CHARS:\n                    raise http_exceptions.BadRequest(\n                        \"Request argument 'prefix' not valid. Allowed characters: {}\".format(\n                            ','.join(settings.DID_PREFIX_ALLOWED_CHARS)))\n\n        # Update gateway group name\n        Gwgroup = db.query(GatewayGroups).filter(GatewayGroups.id == gwgroupid).first()\n        if Gwgroup is None:\n            raise http_exceptions.NotFound('gwgroup does not exist')\n        gwgroup_data['gwgroupid'] = gwgroupid\n        gwgroup_desc_dict = strFieldsToDict(Gwgroup.description)\n        gwgroup_desc_dict['name'] = request_payload['name'] if 'name' in request_payload else ''\n        Gwgroup.description = dictToStrFields(gwgroup_desc_dict)\n        db.flush()\n\n        # Update the gateway group call settings\n        call_settings_data = request_payload['call_settings'] if 'call_settings' in request_payload else {}\n        if db.query(dSIPCallSettings).filter(dSIPCallSettings.gwgroupid == gwgroupid).update({\n            'limit': call_settings_data['limit'],\n            'timeout': call_settings_data['timeout']\n        }, synchronize_session=False):\n            pass\n        else:\n            call_settings = dSIPCallSettings(gwgroupid, **call_settings_data)\n            db.add(call_settings)\n\n        # runtime defaults for this route\n        strip = request_payload['strip'] if 'strip' in request_payload else 0\n        prefix = request_payload['prefix'] if 'prefix' in request_payload else \"\"\n        authtype = request_payload['auth']['type'] \\\n            if 'auth' in request_payload and 'type' in request_payload['auth'] else \"\"\n\n        if 'fusionpbx' in request_payload:\n            fusionpbxenabled = int(request_payload['fusionpbx']['enabled']) \\\n                if 'enabled' in request_payload['fusionpbx'] else 0\n            fusionpbxdbhost = request_payload['fusionpbx']['dbhost'] \\\n                if 'dbhost' in request_payload['fusionpbx'] else None\n            fusionpbxdbuser = request_payload['fusionpbx']['dbuser'] \\\n                if 'dbuser' in request_payload['fusionpbx'] else None\n            fusionpbxdbpass = request_payload['fusionpbx']['dbpass'] \\\n                if 'dbpass' in request_payload['fusionpbx'] else None\n        else:\n            fusionpbxenabled = 0\n\n        # Update the AuthType userpwd settings\n        if authtype == \"userpwd\":\n            authuser = request_payload['auth']['user'] \\\n                if 'user' in request_payload['auth'] and len(request_payload['auth']['user']) > 0 else None\n            authpass = request_payload['auth']['pass'] \\\n                if 'pass' in request_payload['auth'] and len(request_payload['auth']['pass']) > 0 else None\n            authdomain = request_payload['auth']['domain'] \\\n                if 'domain' in request_payload['auth'] and len(\n                request_payload['auth']['domain']) > 0 else settings.DEFAULT_AUTH_DOMAIN\n            authdomain = safeUriToHost(authdomain)\n\n            if authuser is None or authpass is None:\n                raise http_exceptions.BadRequest(\"Auth username or password invalid\")\n            if authdomain is None:\n                raise http_exceptions.BadRequest(\"Auth domain is malformed\")\n\n            # Get the existing username and domain_name if it exists\n            currentSubscriberInfo = db.query(Subscribers).filter(Subscribers.rpid == gwgroupid).first()\n            if currentSubscriberInfo is not None:\n                # Check if the new username and domain is unique\n                if db.query(Subscribers).filter(\n                    Subscribers.username == authuser,\n                    Subscribers.domain == authdomain,\n                    Subscribers.id != currentSubscriberInfo.id\n                ).scalar():\n                    raise http_exceptions.BadRequest(\"Subscriber username already taken\")\n                # Update the Subscriber Info\n                else:\n                    currentSubscriberInfo.username = authuser\n                    currentSubscriberInfo.password = authpass\n                    currentSubscriberInfo.domain = authdomain\n            # Create a new Suscriber entry\n            else:\n                Subscriber = Subscribers(authuser, authpass, authdomain, gwgroupid_str)\n                db.add(Subscriber)\n\n        # Delete the Subscriber info if IP is selected\n        elif authtype == \"ip\":\n            subscriber = db.query(Subscribers).filter(Subscribers.rpid == gwgroupid)\n            if subscriber is not None:\n                subscriber.delete(synchronize_session=False)\n\n        # Update endpoints\n        # Get List of existing endpoints\n        # If endpoint sent over is not in the existing endpoint list then remove it\n        gwgroup_filter = f'gwgroup:{gwgroupid_str}(,|$)'\n        current_endpoints_lut = {\n            x.gwid: {\n                'address': x.address,\n                'type': x.type,\n                'description_dict': strFieldsToDict(x.description),\n                'attrs_dict': x.attrsToDict()\n            } \\\n            for x in db.query(Gateways).filter(\n                Gateways.description.regexp_match(gwgroup_filter)\n            ).all()\n        }\n        updated_endpoints = request_payload['endpoints'] if \"endpoints\" in request_payload else []\n        unprocessed_endpoints_lut = {}\n        gwlist = []\n\n        # endpoints to add unconditionally\n        for endpoint in updated_endpoints:\n            gwid = endpoint['gwid'] if 'gwid' in endpoint else None\n\n            # If gwid is empty then this is a new endpoint\n            if gwid is None:\n                if 'host' not in endpoint:\n                    raise http_exceptions.BadRequest(\"Endpoint hostname/address is required\")\n\n                host = endpoint['host']\n                port = int(endpoint['port']) if endpoint.get('port', None) is not None else 5060\n                signalling = endpoint['signalling'] if 'signalling' in endpoint else 'proxy'\n                media = endpoint['media'] if 'media' in endpoint else 'proxy'\n                name = endpoint['description'] if 'description' in endpoint else ''\n                rweight = int(endpoint['rweight']) if endpoint.get('rweight', '') != '' else 1\n                keepalive = int(endpoint['keepalive']) if endpoint.get('keepalive', '') != '' else 0\n\n                flags = 0\n                if keepalive > 0:\n                    flags += Dispatcher.FLAGS['KEEP_ALIVE']\n                if rweight == 0:\n                    flags += Dispatcher.FLAGS['DISABLED_DST']\n\n                sip_addr = safeUriToHost(host, default_port=port)\n                if sip_addr is None:\n                    raise http_exceptions.BadRequest(\"Endpoint hostname/address is malformed\")\n\n                if authtype == \"ip\":\n                    # for ip auth we must create address records for the endpoint\n                    host_ip = hostToIP(host)\n\n                    # TODO: address entries should include port user specified\n                    Addr = Address(name, host_ip, 32, settings.FLT_PBX, gwgroup=gwgroupid)\n                    db.add(Addr)\n                    db.flush()\n                    Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_PBX, gwgroup=gwgroupid, addr_id=Addr.id,\n                        signalling=signalling, media=media)\n                else:\n                    # we know this a new endpoint so we don't have to check for any address records here\n                    Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_PBX, gwgroup=gwgroupid,\n                        signalling=signalling, media=media)\n\n                db.add(Gateway)\n                db.flush()\n\n                # Create dispatcher group with the set id being the gateway group id\n                sip_uri = f'sip:{sip_addr}'\n                dispatcher = Dispatcher(setid=gwgroupid, destination=sip_uri, flags=flags, rweight=rweight,\n                    signalling=signalling, media=media, name=name, gwid=gwid)\n                db.add(dispatcher)\n\n                # Create dispatcher for FusionPBX external interface if FusionPBX feature is enabled\n                if fusionpbxenabled:\n                    sip_uri_ext = f'sip:{safeStripPort(sip_addr)}:5080'\n                    setid_ext = gwgroupid + 1000\n                    # Add 1000 to the gwgroupid so that the setid for the FusionPBX external interface is 1000 apart\n                    dispatcher = Dispatcher(setid=setid_ext, destination=sip_uri_ext, flags=flags, rweight=rweight,\n                        signalling=signalling, media=media, name=name, gwid=gwid)\n                    db.add(dispatcher)\n\n                gwlist.append(Gateway.gwid)\n\n            # Process separately\n            else:\n                unprocessed_endpoints_lut[gwid] = endpoint\n\n        # conditionally adding/updating/deleting endpoints (using set theory)\n        # generally endpoints won't be added with gwid specified but its possible\n        current_gwids = set(current_endpoints_lut.keys())\n        updated_gwids = set(unprocessed_endpoints_lut.keys())\n        add_gwids = updated_gwids - current_gwids\n        upd_gwids = current_gwids & updated_gwids\n        del_gwids = current_gwids - updated_gwids\n\n        # conditional endpoints to add\n        for gwid in add_gwids:\n            endpoint = unprocessed_endpoints_lut[gwid]\n\n            if 'host' not in endpoint:\n                raise http_exceptions.BadRequest(\"Endpoint hostname/address is required\")\n\n            host = endpoint['host']\n            port = int(endpoint['port']) if endpoint.get('port', None) is not None else 5060\n            signalling = endpoint['signalling'] if 'signalling' in endpoint else 'proxy'\n            media = endpoint['media'] if 'media' in endpoint else 'proxy'\n            name = endpoint['description'] if 'description' in endpoint else ''\n            rweight = int(endpoint['rweight']) if endpoint.get('rweight', '') != '' else 1\n            keepalive = int(endpoint['keepalive']) if endpoint.get('keepalive', '') != '' else 0\n\n            flags = 0\n            if keepalive > 0:\n                flags += Dispatcher.FLAGS['KEEP_ALIVE']\n            if rweight == 0:\n                flags += Dispatcher.FLAGS['DISABLED_DST']\n\n            sip_addr = safeUriToHost(host, default_port=port)\n            if sip_addr is None:\n                raise http_exceptions.BadRequest(\"Endpoint hostname/address is malformed\")\n\n            if authtype == \"ip\":\n                # for ip auth we must create address records for the endpoint\n                host_ip = hostToIP(host)\n\n                # TODO: address entries should include port user specified\n                Addr = Address(name, host_ip, 32, settings.FLT_PBX, gwgroup=gwgroupid)\n                db.add(Addr)\n                db.flush()\n                Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_PBX, gwgroup=gwgroupid, addr_id=Addr.id,\n                    signalling=signalling, media=media)\n            else:\n                # we know this a new endpoint so we don't have to check for any address records here\n                Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_PBX, gwgroup=gwgroupid,\n                    signalling=signalling, media=media)\n\n            db.add(Gateway)\n            db.flush()\n\n            # Create dispatcher group with the set id being the gateway group id\n            sip_uri = f'sip:{sip_addr}'\n            dispatcher = Dispatcher(setid=gwgroupid, destination=sip_uri, flags=flags, rweight=rweight,\n                signalling=signalling, media=media, name=name, gwid=gwid)\n            db.add(dispatcher)\n\n            # Create dispatcher for FusionPBX external interface if FusionPBX feature is enabled\n            if fusionpbxenabled:\n                sip_uri_ext = f'sip:{safeStripPort(sip_addr)}:5080'\n                setid_ext = gwgroupid + 1000\n                # Add 1000 to the gwgroupid so that the setid for the FusionPBX external interface is 1000 apart\n                dispatcher = Dispatcher(setid=setid_ext, destination=sip_uri_ext, flags=flags, rweight=rweight,\n                    signalling=signalling, media=media, name=name, gwid=gwid)\n                db.add(dispatcher)\n\n            # we ignore the given gwid and allow DB to assign one instead\n            gwlist.append(Gateway.gwid)\n\n        # conditional endpoints to update\n        for gwid in upd_gwids:\n            endpoint = unprocessed_endpoints_lut[gwid]\n            current_endpoint = current_endpoints_lut[gwid]\n            endpoint_fields = current_endpoint['description_dict']\n            endpoint_attrs = current_endpoint['attrs_dict']\n\n            # allow updating single fields (if not provided set to current value)\n            host = endpoint['host'] if 'host' in endpoint else current_endpoint['address'].split(':')[0]\n            if 'port' in endpoint and endpoint['port'] is not None:\n                port = int(endpoint['port'])\n            else:\n                tmp = current_endpoint['address'].split(':')\n                port = int(tmp[1]) if len(tmp) > 1 else 5060\n            signalling = endpoint['signalling'] if 'signalling' in endpoint else endpoint_attrs['signalling']\n            media = endpoint['media'] if 'media' in endpoint else endpoint_attrs['media']\n            name = endpoint['description'] if 'description' in endpoint else endpoint_fields['name']\n            endpoint_fields['name'] = name\n\n            # fields not in dr_gateways attributes we set to None if not updated and check it after querying dispatcher\n            rweight = int(endpoint['rweight']) if endpoint.get('rweight', None) is not None else None\n            keepalive = int(endpoint['keepalive']) if endpoint.get('keepalive', None) is not None else None\n\n            if len(host) == 0:\n                raise http_exceptions.BadRequest(\"Endpoint hostname/address is required\")\n\n            sip_addr = safeUriToHost(host, default_port=port)\n            if sip_addr is None:\n                raise http_exceptions.BadRequest(\"Endpoint hostname/address is malformed\")\n\n            if authtype == \"ip\":\n                # for ip auth we must create address records for the endpoint\n                host_ip = hostToIP(host)\n\n                # if address exists update, otherwise create it\n                address_exists = False\n                if 'addr_id' in endpoint_fields and len(endpoint_fields['addr_id']) > 0:\n                    Addr = db.query(Address).filter(Address.id == endpoint_fields['addr_id']).first()\n\n                    if Addr is not None:\n                        address_exists = True\n\n                        Addr.ip_addr = host_ip\n                        addr_fields = strFieldsToDict(Addr.tag)\n                        addr_fields['name'] = name\n                        addr_fields['gwgroup'] = gwgroupid_str\n                        Addr.tag = dictToStrFields(addr_fields)\n\n                if not address_exists:\n                    Addr = Address(name, host_ip, 32, settings.FLT_PBX, gwgroup=gwgroupid)\n\n                    db.add(Addr)\n                    db.flush()\n                    endpoint_fields['addr_id'] = str(Addr.id)\n            else:\n                # if not using ip auth make sure we delete any old address records for the endpoint\n                if 'addr_id' in endpoint_fields and len(endpoint_fields['addr_id']) > 0:\n                    Addr = db.query(Address).filter(Address.id == endpoint_fields['addr_id'])\n                    if Addr is not None:\n                        Addr.delete(synchronize_session=False)\n                    # remove addr_id field from endpoint description\n                    endpoint_fields.pop(\"addr_id\", None)\n\n            # find the current entry in dr_gateways\n            ep_gateway = db.query(Gateways).filter(Gateways.gwid == gwid).first()\n\n            # find the current dispatcher entry (load balancing one is always there and has same params as other \"feature\" sets)\n            ep_dispatcher = db.query(Dispatcher).filter(\n                (Dispatcher.setid == gwgroupid) &\n                Dispatcher.description.regexp_match(f'gwid={gwid}(;|$)')\n            ).first()\n            if ep_dispatcher is not None:\n                if rweight is None:\n                    rweight = ep_dispatcher.attrsToDict()['rweight']\n                if keepalive is None:\n                    keepalive = 8 & ep_dispatcher.flags\n            else:\n                if rweight is None:\n                    rweight = 0\n                if keepalive is None:\n                    keepalive = 0\n\n            flags = 0\n            if keepalive > 0:\n                flags += Dispatcher.FLAGS['KEEP_ALIVE']\n            if rweight == 0:\n                flags += Dispatcher.FLAGS['DISABLED_DST']\n\n            # update the endpoint in dispatcher\n            sip_uri = f'sip:{sip_addr}'\n            if ep_dispatcher is not None:\n                ep_dispatcher.destination = sip_uri\n                ep_dispatcher.flags = flags\n                ep_dispatcher.attrs = Dispatcher.buildAttrs(rweight, signalling, media)\n                ep_dispatcher.description = Dispatcher.buildDescription(name, gwid)\n            else:\n                ep_dispatcher = Dispatcher(setid=gwgroupid, destination=sip_uri, flags=flags,\n                    rweight=rweight, signalling=signalling, media=media, name=name, gwid=gwid)\n                db.add(ep_dispatcher)\n\n            # update the fusionpbx dispatcher entries for the endpoint\n            sip_uri_ext = f'sip:{safeStripPort(sip_addr)}:5080'\n            setid_ext = int(gwgroupid) + 1000\n            if fusionpbxenabled:\n                ep_dispatcher_ext = db.query(Dispatcher).filter(\n                    (Dispatcher.setid == setid_ext) &\n                    Dispatcher.description.regexp_match(f'gwid={gwid}(;|$)')\n                ).first()\n                if ep_dispatcher_ext is not None:\n                    ep_dispatcher_ext.destination = sip_uri_ext\n                    ep_dispatcher_ext.flags = flags\n                    ep_dispatcher_ext.attrs = Dispatcher.buildAttrs(rweight, signalling, media)\n                    ep_dispatcher_ext.description = Dispatcher.buildDescription(name, gwid)\n                else:\n                    ep_dispatcher_ext = Dispatcher(setid=setid_ext, destination=sip_uri_ext, flags=flags,\n                        rweight=rweight, signalling=signalling, media=media, name=name, gwid=gwid)\n                    db.add(ep_dispatcher_ext)\n            else:\n                # remove the entries if fusionpbx integration is now disabled\n                db.query(Dispatcher).filter(\n                    (Dispatcher.setid == setid_ext) &\n                    Dispatcher.description.regexp_match(f'gwid={gwid}(;|$)')\n                ).delete(synchronize_session=False)\n\n            # update the endpoint in dr_gateways\n            ep_gateway.description = dictToStrFields(endpoint_fields)\n            ep_gateway.address = sip_addr\n            ep_gateway.strip = strip\n            ep_gateway.pri_prefix = prefix\n            ep_gateway.attrs = Gateways.buildAttrs(gwid=gwid, type=current_endpoint['type'], signalling=signalling, media=media)\n\n            gwlist.append(gwid)\n\n        # conditional endpoints to delete\n        # we also cleanup house here in case of stray entries\n        del_gateways = db.query(Gateways).filter(\n            Gateways.gwid.in_(del_gwids) &\n            (Gateways.address != \"localhost\")\n        )\n        del_gateways_cleanup = db.query(Gateways).filter(\n            Gateways.description.regexp_match(gwgroup_filter) &\n            Gateways.gwid.notin_(gwlist) &\n            (Gateways.address != \"localhost\")\n        )\n        # make sure we delete any associated address entries\n        del_addr_ids = []\n        for gateway in del_gateways.union(del_gateways_cleanup):\n            description_dict = strFieldsToDict(gateway.description)\n            if 'addr_id' in description_dict:\n                del_addr_ids.append(description_dict['addr_id'])\n        db.query(Address).filter(Address.id.in_(del_addr_ids)).delete(synchronize_session=False)\n\n        # delete the dispatcher entries that correspond to the endpoints/gateways that was Deleted\n        del_gw_filters = [\n            f'gwid={gateway.gwid}(;|$)' for gateway in del_gateways.union(del_gateways_cleanup)\n        ]\n        # the default match for REGEXP is true, make sure we actually have gwids to match against\n        if len(del_gw_filters) > 0:\n            db.query(Dispatcher).filter(\n                (Dispatcher.setid == gwgroupid) &\n                Dispatcher.description.regexp_match('|'.join(del_gw_filters))\n            ).delete(synchronize_session=False)\n\n        del_gateways.delete(synchronize_session=False)\n        del_gateways_cleanup.delete(synchronize_session=False)\n\n        # set return gwlist and update the endpoint group's gwlist\n        gwgroup_data['endpoints'] = gwlist\n        gwlist_str = ','.join([str(gw) for gw in gwlist])\n        db.query(GatewayGroups).filter(GatewayGroups.id == gwgroupid).update(\n            {\"gwlist\": gwlist_str}, synchronize_session=False)\n\n        # Update notifications\n        if 'notifications' in request_payload:\n            overmaxcalllimit = request_payload['notifications']['overmaxcalllimit'] \\\n                if 'overmaxcalllimit' in request_payload['notifications'] else None\n            endpointfailure = request_payload['notifications']['endpointfailure'] \\\n                if 'endpointfailure' in request_payload['notifications'] else None\n\n            if overmaxcalllimit is not None:\n                # Try to update\n                if db.query(dSIPNotification).filter(dSIPNotification.gwgroupid == gwgroupid).filter(\n                    dSIPNotification.type == 0).update({\"value\": overmaxcalllimit}, synchronize_session=False):\n                    pass\n                # Otherwise Add\n                else:\n                    notif_type = 0\n                    notification = dSIPNotification(gwgroupid, notif_type, 0, overmaxcalllimit)\n                    db.add(notification)\n            # Remove\n            else:\n                db.query(dSIPNotification).filter(dSIPNotification.gwgroupid == gwgroupid).filter(\n                    dSIPNotification.type == 0).delete(synchronize_session=False)\n\n            if endpointfailure is not None:\n                # Try to update\n                if db.query(dSIPNotification).filter(dSIPNotification.gwgroupid == gwgroupid).filter(\n                    dSIPNotification.type == 1).update({\"value\": endpointfailure}, synchronize_session=False):\n                    pass\n                # Otherwise Add\n                else:\n                    notif_type = 1\n                    notification = dSIPNotification(gwgroupid, notif_type, 0, endpointfailure)\n                    db.add(notification)\n            # Remove\n            else:\n                db.query(dSIPNotification).filter(dSIPNotification.gwgroupid == gwgroupid).filter(\n                    dSIPNotification.type == 1).delete(synchronize_session=False)\n\n        # Update CDR\n        if 'cdr' in request_payload:\n            cdr_email = request_payload['cdr']['cdr_email'] \\\n                if 'cdr_email' in request_payload['cdr'] else None\n            cdr_send_interval = request_payload['cdr']['cdr_send_interval'] \\\n                if 'cdr_send_interval' in request_payload['cdr'] else None\n\n            if len(cdr_email) > 0 and len(cdr_send_interval) > 0:\n                cron_cmd = '{} cdr sendreport {}'.format(\n                    (settings.DSIP_PROJECT_DIR + '/gui/dsiprouter_cron.py'),\n                    gwgroupid_str\n                )\n                # Try to update\n                if db.query(dSIPCDRInfo).filter(dSIPCDRInfo.gwgroupid == gwgroupid).update(\n                    {\"email\": cdr_email, \"send_interval\": cdr_send_interval},\n                    synchronize_session=False):\n                    if not updateTaggedCronjob(gwgroupid, cdr_send_interval):\n                        # in-case the entry was modified elsewhere we can just create it again\n                        if not addTaggedCronjob(gwgroupid, cdr_send_interval, cron_cmd):\n                            raise Exception('Crontab entry could not be updated')\n                else:\n                    cdrinfo = dSIPCDRInfo(gwgroupid, cdr_email, cdr_send_interval)\n                    db.add(cdrinfo)\n\n                    if not addTaggedCronjob(gwgroupid, cdr_send_interval, cron_cmd):\n                        raise Exception('Crontab entry could not be created')\n            else:\n                # Remove CDR Info\n                cdrinfo = db.query(dSIPCDRInfo).filter(dSIPCDRInfo.gwgroupid == gwgroupid).first()\n                if cdrinfo is not None:\n                    db.delete(cdrinfo)\n                    if not deleteTaggedCronjob(gwgroupid):\n                        raise Exception('Crontab entry could not be deleted')\n\n            # Update FusionPBX\n            # Update\n            if fusionpbxenabled == 1:\n                # Update\n                if db.query(dSIPMultiDomainMapping).filter(dSIPMultiDomainMapping.pbx_id == gwgroupid).update(\n                    {\"pbx_id\": gwgroupid, \"db_host\": fusionpbxdbhost, \"db_username\": fusionpbxdbuser,\n                     \"db_password\": fusionpbxdbpass}, synchronize_session=False):\n                    pass\n                else:\n                    # Create new record\n                    domainmapping = dSIPMultiDomainMapping(gwgroupid, fusionpbxdbhost, fusionpbxdbuser, fusionpbxdbpass,\n                        type=dSIPMultiDomainMapping.FLAGS.TYPE_FUSIONPBX.value)\n                    db.add(domainmapping)\n            # Delete\n            elif fusionpbxenabled == 0:\n                db.query(dSIPMultiDomainMapping).filter(dSIPMultiDomainMapping.pbx_id == gwgroupid).delete(\n                    synchronize_session=False)\n\n        db.commit()\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return createApiResponse(\n            msg='Endpoint group updated',\n            data=[gwgroup_data],\n            kamreload=True,\n        )\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n# TODO: fix up like updateEnpointGroups()\n# TODO: this is the only route that has the proper updated documentation format\n#       use this route as an example to populate the other route docstings\n@api.route(\"/api/v1/endpointgroups\", methods=['POST'])\n@api_security\ndef addEndpointGroups(data=None, endpointGroupType=None, domain=None):\n    \"\"\"\n    Add a single Endpoint Group\n\n    :<json string name: the endpoint group name\n    :<json integer calllimit: limit concurrent calls to this endpoint group (0=unlimited)\n    :<json object auth: authentication settings for this endpoint group\n    :reqheader Accept: optional, assumed to be application/json\n    :reqheader Authorization: required, dSIPRouter API Bearer token\n    :resheader Content-Type: required, should always be application/json\n    :status 200: on success\n    :status 401: on auth failure\n    :status 500: on unhandled application server error\n    :status 502: when application server is down but reverse proxy is still up\n\n    Example Request:\n\n    .. code-block:: json\n\n        {\n            \"name\": \"example\",\n            \"call_settings\": {\n                limit: 5,\n                timeout: 3600\n            },\n            \"auth\": {\n                \"type\": \"userpwd\",\n                \"user\": \"example\",\n                \"pass\": \"example\",\n                \"domain\": \"example.com\"\n            },\n            \"endpoints\": [\n                {\n                    \"host\": \"www.example.com\",\n                    \"port\": 5060,\n                    \"signalling\": \"proxy\",\n                    \"media\": \"proxy\",\n                    \"description\": \"example\",\n                    \"rweight\": 1,\n                    \"keepalive\": 1\n                }\n            ],\n            \"strip\": 0,\n            \"prefix\": \"\",\n            \"notifications\": {\n                \"overmaxcalllimit\": \"email@example.com\",\n                \"endpointfailure\": \"email@example.com\"\n            },\n            \"cdr\": {\n                \"cdr_email\": \"email@example.com\",\n                \"cdr_send_interval\": \"email@example.com\"\n            },\n            \"fusionpbx\": {\n                \"enabled\": true,\n                \"dbhost\": \"example\",\n                \"dbuser\": \"example\",\n                \"dbpass\": \"example\"\n            }\n        }\n\n    Example Response:\n\n    .. code-block:: json\n\n        {\n            \"error\": \"\",\n            \"msg\": \"Endpoint group created\",\n            \"kamreload\": true,\n            \"dsipreload\": false,\n            \"data\": [\n                {\n                    \"gwgroupid\": 101,\n                    \"gwlist\": \"375\",\n                    \"name\": \"example\"\n                }\n            ]\n        }\n    \"\"\"\n\n    db = DummySession()\n    fusionpbxenabled = 0\n\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    VALID_REQUEST_DATA_ARGS = {\n        \"name\": str, \"call_settings\": dict, \"auth\": dict, \"strip\": int, \"prefix\": str,\n        \"notifications\": dict, \"cdr\": dict, \"fusionpbx\": dict, \"endpoints\": list\n    }\n\n    gwgroup_data = {}\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        db = startSession()\n\n        if data == None:\n            # Convert Request message to Dictionary Object\n            request_payload = getRequestData()\n        else:\n            # Use the data parameter as the payload\n            request_payload = data\n\n        # sanity checks\n        for k, v in request_payload.items():\n            if k not in VALID_REQUEST_DATA_ARGS.keys():\n                raise http_exceptions.BadRequest(\"Request argument '{}' not recognized\".format(k))\n            if not type(v) == VALID_REQUEST_DATA_ARGS[k]:\n                try:\n                    request_payload[k] = VALID_REQUEST_DATA_ARGS[k](v)\n                    continue\n                except:\n                    raise http_exceptions.BadRequest(\"Request argument '{}' not valid\".format(k))\n        if 'prefix' in request_payload:\n            for c in request_payload['prefix']:\n                if c not in settings.DID_PREFIX_ALLOWED_CHARS:\n                    raise http_exceptions.BadRequest(\n                        \"Request argument 'prefix' not valid. Allowed characters: {}\".format(\n                            ','.join(settings.DID_PREFIX_ALLOWED_CHARS)))\n\n        # Process Gateway Name\n        name = request_payload['name']\n        Gwgroup = GatewayGroups(name, type=settings.FLT_PBX)\n        db.add(Gwgroup)\n        db.flush()\n        gwgroupid = Gwgroup.id\n        gwgroup_data['gwgroupid'] = gwgroupid\n\n        # create the call settings for the new gateway group\n        call_settings_data = request_payload['call_settings'] if 'call_settings' in request_payload else {}\n        call_settings = dSIPCallSettings(gwgroupid, **call_settings_data)\n        db.add(call_settings)\n\n        # runtime defaults for this route\n        strip = request_payload['strip'] if 'strip' in request_payload else 0\n        prefix = request_payload['prefix'] if 'prefix' in request_payload else \"\"\n        authtype = request_payload['auth']['type'] \\\n            if 'auth' in request_payload and 'type' in request_payload['auth'] else \"\"\n\n        if authtype == \"userpwd\":\n            # Store Endpoint IP's in address tables\n            authuser = request_payload['auth']['user'] \\\n                if 'user' in request_payload['auth'] \\\n                   and len(request_payload['auth']['user']) > 0 else None\n            authpass = request_payload['auth']['pass'] \\\n                if 'pass' in request_payload['auth'] \\\n                   and len(request_payload['auth']['pass']) > 0 else None\n            authdomain = request_payload['auth']['domain'] \\\n                if 'domain' in request_payload['auth'] \\\n                   and len(request_payload['auth']['domain']) > 0 else settings.DEFAULT_AUTH_DOMAIN\n            authdomain = safeUriToHost(authdomain)\n\n            if authuser is None or authpass is None:\n                raise http_exceptions.BadRequest(\"Auth username or password invalid\")\n            if authdomain is None:\n                raise http_exceptions.BadRequest(\"Auth domain is malformed\")\n\n            if db.query(Subscribers).filter(Subscribers.username == authuser,\n                Subscribers.domain == authdomain).scalar():\n                raise sql_exceptions.SQLAlchemyError(\n                    'DB Entry Taken for Username {} in Domain {}'.format(authuser, authdomain))\n            else:\n                Subscriber = Subscribers(authuser, authpass, authdomain, gwgroupid)\n                db.add(Subscriber)\n\n        # Enable FusionPBX Support for the Endpoint Group\n        if 'fusionpbx' in request_payload:\n            fusionpbxenabled = request_payload['fusionpbx']['enabled'] \\\n                if 'enabled' in request_payload['fusionpbx'] else None\n            fusionpbxclustersupport = request_payload['fusionpbx']['clustersupport'] \\\n                if 'clustersupport' in request_payload['fusionpbx'] else None\n            fusionpbxdbhost = request_payload['fusionpbx']['dbhost'] \\\n                if 'dbhost' in request_payload['fusionpbx'] else None\n            fusionpbxdbuser = request_payload['fusionpbx']['dbuser'] \\\n                if 'dbuser' in request_payload['fusionpbx'] else None\n            fusionpbxdbpass = request_payload['fusionpbx']['dbpass'] \\\n                if 'dbpass' in request_payload['fusionpbx'] else None\n\n            # Convert fusionpbxenabled variable to int\n            if isinstance(fusionpbxenabled, str):\n                fusionpbxenabled = int(fusionpbxenabled)\n\n            # Convert fusionclustersupport variable to int\n            if isinstance(fusionpbxclustersupport, str):\n                fusionpbxclustersupport = int(fusionpbxclustersupport)\n\n            if fusionpbxenabled:\n                if fusionpbxclustersupport == 1:\n                    domainType = dSIPMultiDomainMapping.FLAGS.TYPE_FUSIONPBX_CLUSTER.value\n                else:\n                    domainType = dSIPMultiDomainMapping.FLAGS.TYPE_FUSIONPBX.value\n\n                domainmapping = dSIPMultiDomainMapping(gwgroupid, fusionpbxdbhost, fusionpbxdbuser, fusionpbxdbpass,\n                    type=domainType)\n                db.add(domainmapping)\n\n                # Add the FusionPBX server as an Endpoint if it's not just the DB server\n                if fusionpbxclustersupport is None or fusionpbxclustersupport == False:\n                    endpoint = {}\n                    endpoint['host'] = fusionpbxdbhost\n                    endpoint['description'] = \"FusionPBX Server\"\n                    if \"endpoints\" not in request_payload:\n                        request_payload['endpoints'] = []\n                    request_payload['endpoints'].append(endpoint)\n\n        msteams_domain = domain if domain is not None else ''\n\n        # Setup Endpoints\n        endpoints = request_payload['endpoints'] if \"endpoints\" in request_payload else []\n        gwlist = []\n\n        # endpoints to add unconditionally\n        for endpoint in endpoints:\n            if 'host' not in endpoint:\n                raise http_exceptions.BadRequest(\"Endpoint hostname/address is required\")\n\n            host = endpoint['host']\n            port = int(endpoint['port']) if endpoint.get('port', None) is not None else 5060\n            signalling = endpoint['signalling'] if 'signalling' in endpoint else 'proxy'\n            media = endpoint['media'] if 'media' in endpoint else 'proxy'\n            name = endpoint['description'] if 'description' in endpoint else ''\n            rweight = int(endpoint['rweight']) if endpoint.get('rweight', '') != '' else 1\n            keepalive = int(endpoint['keepalive']) if endpoint.get('keepalive', '') != '' else 0\n\n            flags = 0\n            if keepalive > 0:\n                flags += Dispatcher.FLAGS['KEEP_ALIVE']\n            if rweight == 0:\n                flags += Dispatcher.FLAGS['DISABLED_DST']\n\n            sip_addr = safeUriToHost(host, default_port=port)\n            if sip_addr is None:\n                raise http_exceptions.BadRequest(\"Endpoint hostname/address is malformed\")\n\n            if authtype == \"ip\":\n                host_ip = hostToIP(host)\n\n                # TODO: address entries should include port user specified\n                Addr = Address(name, host_ip, 32, settings.FLT_PBX, gwgroup=gwgroupid)\n                db.add(Addr)\n                db.flush()\n                Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_PBX, gwgroup=gwgroupid, addr_id=Addr.id,\n                    msteams_domain=msteams_domain, signalling=signalling, media=media)\n            else:\n                Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_PBX, gwgroup=gwgroupid,\n                    msteams_domain=msteams_domain, signalling=signalling, media=media)\n\n            db.add(Gateway)\n            db.flush()\n\n            # Create dispatcher group with the set id being the gateway group id\n            # Don't create a dispatcher set for endpoint groups that was created for MSTeams domains    \n            if endpointGroupType != \"msteams\":\n                dispatcher = Dispatcher(setid=gwgroupid, destination=sip_addr, flags=flags, rweight=rweight,\n                    signalling=signalling, media=media, name=name, gwid=Gateway.gwid)\n                db.add(dispatcher)\n\n            # Create dispatcher for FusionPBX external interface if FusionPBX feature is enabled\n            if fusionpbxenabled:\n                sip_uri_ext = f'sip:{safeStripPort(sip_addr)}:5080'\n                setid_ext = gwgroupid + 1000\n                # Add 1000 to the gwgroupid so that the setid for the FusionPBX external interface is 1000 apart\n                dispatcher = Dispatcher(setid=setid_ext, destination=sip_uri_ext, flags=flags, rweight=rweight,\n                    signalling=signalling, media=media, name=name, gwid=Gateway.gwid)\n                db.add(dispatcher)\n\n\n            gwlist.append(Gateway.gwid)\n\n        # set return gwlist and update the endpoint group's gwlist\n        gwgroup_data['endpoints'] = gwlist\n        gwlist_str = ','.join([str(gw) for gw in gwlist])\n\n        # Update Gateway group with the Dispatcher ID.  It's denoted with the LB field\n        fields = strFieldsToDict(Gwgroup.description)\n        # Don't add a load balancing group if the domain is a MSTeams domain\n        if endpointGroupType != \"msteams\":\n            fields['lb'] = gwgroupid\n        if fusionpbxenabled:\n            fields['lb_ext'] = gwgroupid + 1000\n\n        # Update the GatewayGroup with the lists of gateways\n        Gwgroup.gwlist = gwlist_str\n        Gwgroup.description = dictToStrFields(fields)\n\n        # Setup notifications\n        if 'notifications' in request_payload:\n            overmaxcalllimit = request_payload['notifications']['overmaxcalllimit'] \\\n                if 'overmaxcalllimit' in request_payload['notifications'] else None\n            endpointfailure = request_payload['notifications']['endpointfailure'] \\\n                if 'endpointfailure' in request_payload['notifications'] else None\n\n            if overmaxcalllimit is not None:\n                notif_type = dSIPNotification.FLAGS.TYPE_OVERLIMIT.value\n                method = dSIPNotification.FLAGS.METHOD_EMAIL.value\n                notification = dSIPNotification(gwgroupid, notif_type, method, overmaxcalllimit)\n                db.add(notification)\n\n            if endpointfailure is not None:\n                notif_type = dSIPNotification.FLAGS.TYPE_GWFAILURE.value\n                method = dSIPNotification.FLAGS.METHOD_EMAIL.value\n                notification = dSIPNotification(gwgroupid, notif_type, method, endpointfailure)\n                db.add(notification)\n\n        # Enable CDR\n        if 'cdr' in request_payload:\n            cdr_email = request_payload['cdr']['cdr_email'] \\\n                if 'cdr_email' in request_payload['cdr'] else ''\n            cdr_send_interval = request_payload['cdr']['cdr_send_interval'] \\\n                if 'cdr_send_interval' in request_payload['cdr'] else ''\n\n            if len(cdr_email) > 0 and len(cdr_send_interval) > 0:\n                # create DB entry\n                cdrinfo = dSIPCDRInfo(gwgroupid, cdr_email, cdr_send_interval)\n                db.add(cdrinfo)\n\n                # create crontab entry\n                cron_cmd = '{} cdr sendreport {}'.format((settings.DSIP_PROJECT_DIR + '/gui/dsiprouter_cron.py'),\n                    gwgroupid)\n                if not addTaggedCronjob(gwgroupid, cdr_send_interval, cron_cmd):\n                    raise Exception('Crontab entry could not be created')\n\n        db.commit()\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return createApiResponse(\n            msg='Endpoint group created',\n            data=[gwgroup_data],\n            kamreload=True,\n        )\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@api.route(\"/api/v1/numberenrichment\", methods=['GET'])\n@api.route(\"/api/v1/numberenrichment/<int:rule_id>\", methods=['GET'])\n@api_security\ndef getNumberEnrichment(rule_id=None):\n    \"\"\"\n    Get one or multiple number enrichment rules\n\n    ================\n    Response Payload\n    ================\n\n    .. code-block:: json\n\n        {\n            error: <str>,\n            msg: <str>,\n            kamreload: <bool>,\n            data: [\n                {\n                    rule_id: <int>,\n                    dnid: <str>,\n                    country_code: <str>,\n                    routing_number: <str>,\n                    rule_name: <str>\n                },\n                ...\n            ]\n        }\n    \"\"\"\n    db = DummySession()\n\n    response_data = []\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        db = startSession()\n\n        # get a single enrichment rule\n        if rule_id is not None:\n            rule = db.query(dSIPDNIDEnrichment).filter(dSIPDNIDEnrichment.id == rule_id).first()\n            if rule is not None:\n                response_data.append({\n                    'rule_id': rule.id,\n                    'dnid': rule.dnid,\n                    'country_code': rule.country_code,\n                    'routing_number': rule.routing_number,\n                    'rule_name': strFieldsToDict(rule.description)['name']\n                })\n            else:\n                raise http_exceptions.NotFound(\"Enrichment Rule Does Not Exist\")\n        # get all enrichment rules\n        else:\n            rules = db.query(dSIPDNIDEnrichment).all()\n\n            for rule in rules:\n                response_data.append({\n                    'rule_id': rule.id,\n                    'dnid': rule.dnid,\n                    'country_code': rule.country_code,\n                    'routing_number': rule.routing_number,\n                    'rule_name': strFieldsToDict(rule.description)['name']\n                })\n\n        return createApiResponse(\n            msg='Enrichment Rule(s) found',\n            data=response_data,\n        )\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@api.route(\"/api/v1/numberenrichment\", methods=['POST'])\n@api_security\ndef addNumberEnrichment(request_payload=None):\n    \"\"\"\n    Add a one or multiple enrichment rules\n\n    ===============\n    Request Payload\n    ===============\n\n    .. code-block:: json\n\n        {\n            data: [\n                {\n                    dnid: <str>,\n                    country_code: <str>,\n                    routing_number: <str>,\n                    rule_name: <str>\n                },\n                ...\n            ]\n        }\n\n    ================\n    Response Payload\n    ================\n\n    .. code-block:: json\n\n        {\n            error: <str>,\n            msg: <str>,\n            kamreload: <bool>,\n            data: [\n                {\n                    rule_id: <int>\n                },\n                ...\n            ]\n        }\n    \"\"\"\n\n    db = DummySession()\n\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    VALID_REQUEST_DATA_ARGS = {\n        \"dnid\": str, \"country_code\": str, \"routing_number\": str, \"rule_name\": str\n    }\n\n    # ensure requred args are provided\n    REQUIRED_REQUEST_DATA_ARGS = {'dnid'}\n\n    response_data = []\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        db = startSession()\n\n        # allow calling function with payload data\n        if request_payload is None:\n            request_payload = getRequestData()\n\n        # sanity checks\n        if 'data' not in request_payload or len(request_payload['data']) == 0:\n            raise http_exceptions.BadRequest(\"Request argument 'data' is required and must be non-zero length\")\n        for data in request_payload['data']:\n            if not isinstance(data, dict):\n                raise http_exceptions.BadRequest(\"Request argument 'data' not valid\")\n            for k, v in data.items():\n                if k not in VALID_REQUEST_DATA_ARGS.keys():\n                    raise http_exceptions.BadRequest(\"Request argument '{}' not recognized\".format(k))\n                if not type(v) == VALID_REQUEST_DATA_ARGS[k]:\n                    try:\n                        request_payload[k] = VALID_REQUEST_DATA_ARGS[k](v)\n                        continue\n                    except:\n                        raise http_exceptions.BadRequest(\"Request argument '{}' not valid\".format(k))\n            for k in REQUIRED_REQUEST_DATA_ARGS:\n                if k not in data:\n                    raise http_exceptions.BadRequest(\"Required argument '{}' missing in data entry\".format(k))\n\n        # don't allow duplicate dnid's\n        new_dnid_list = [x['dnid'] for x in request_payload['data']]\n        if db.query(dSIPDNIDEnrichment).filter(dSIPDNIDEnrichment.dnid.in_(new_dnid_list)).scalar():\n            raise http_exceptions.BadRequest(\"Duplicate DNID's are not allowed\")\n\n        for rule_data in request_payload['data']:\n            rule = dSIPDNIDEnrichment(**rule_data)\n            db.add(rule)\n            db.flush()\n            response_data.append({'rule_id': rule.id})\n\n        db.commit()\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return createApiResponse(\n            msg='Enrichment Rule(s) created',\n            data=response_data,\n            kamreload=True,\n        )\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@api.route(\"/api/v1/numberenrichment\", methods=['PUT'])\n@api.route(\"/api/v1/numberenrichment/<int:rule_id>\", methods=['PUT'])\n@api_security\ndef updateNumberEnrichment(rule_id=None, request_payload=None):\n    \"\"\"\n    Update one or multiple enrichment rules\\n\n    The rule_id can be provided in url path or payload\n\n    ===============\n    Request Payload\n    ===============\n\n    .. code-block:: json\n\n        {\n            data: [\n                {\n                    rule_id: <int>\n                    dnid: <str>,\n                    country_code: <str>,\n                    routing_number: <str>,\n                    rule_name: <str>\n                },\n                ...\n            ]\n        }\n\n    ================\n    Response Payload\n    ================\n\n    .. code-block:: json\n\n        {\n            error: <str>,\n            msg: <str>,\n            kamreload: <bool>,\n            data: [\n                {\n                    rule_id: <int>\n                },\n                ...\n            ]\n        }\n    \"\"\"\n\n    db = DummySession()\n\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    VALID_REQUEST_DATA_ARGS = {\n        \"rule_id\": int, \"dnid\": str, \"country_code\": str, \"routing_number\": str, \"rule_name\": str\n    }\n\n    # ensure requred args are provided\n    REQUIRED_REQUEST_DATA_ARGS = {'dnid'}\n\n    response_data = []\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        # allow calling function with payload data\n        if request_payload is None:\n            request_payload = getRequestData()\n\n        # sanity checks\n        if 'data' not in request_payload or len(request_payload['data']) == 0:\n            raise http_exceptions.BadRequest(\"Request argument 'data' is required and must be non-zero length\")\n        else:\n            if rule_id is not None and isinstance(request_payload['data'][0], dict):\n                request_payload['data'][0]['rule_id'] = int(rule_id)\n        for data in request_payload['data']:\n            if not isinstance(data, dict):\n                raise http_exceptions.BadRequest(\"Request argument 'data' not valid\")\n            for k, v in data.items():\n                if k not in VALID_REQUEST_DATA_ARGS.keys():\n                    raise http_exceptions.BadRequest(\"Request argument '{}' not recognized\".format(k))\n                if not type(v) == VALID_REQUEST_DATA_ARGS[k]:\n                    try:\n                        request_payload[k] = VALID_REQUEST_DATA_ARGS[k](v)\n                        continue\n                    except:\n                        raise http_exceptions.BadRequest(\"Request argument '{}' not valid\".format(k))\n            for k in REQUIRED_REQUEST_DATA_ARGS:\n                if k not in data:\n                    raise http_exceptions.BadRequest(\"Required argument '{}' missing in data entry\".format(k))\n\n        # don't allow duplicate dnid's\n        upd_ruleids = [x['rule_id'] for x in request_payload['data'] if 'rule_id' in x]\n        old_dnids = set(x.dnid for x in db.query(dSIPDNIDEnrichment).all() if x.id not in upd_ruleids)\n        add_dnids = set(x['dnid'] for x in request_payload['data'] if not 'rule_id' in x)\n        upd_dnids = set(x['dnid'] for x in request_payload['data'] if 'rule_id' in x)\n        if len(list(add_dnids) + list(upd_dnids) + list(old_dnids)) > len(set.union(add_dnids, upd_dnids, old_dnids)):\n            raise http_exceptions.BadRequest(\"Duplicate DNID's are not allowed\")\n\n        for rule_data in request_payload['data']:\n            # adding new rules\n            if not 'rule_id' in rule_data:\n                rule = dSIPDNIDEnrichment(**rule_data)\n                db.add(rule)\n                db.flush()\n                response_data.append({'rule_id': rule.id})\n            # updating existing rules\n            else:\n                rule_id = rule_data.pop('rule_id')\n                description = {'name': rule_data.pop('rule_name')}\n                rule_data['description'] = dictToStrFields(description)\n\n                if not db.query(dSIPDNIDEnrichment).filter(dSIPDNIDEnrichment.id == rule_id).update(\n                    rule_data, synchronize_session=False):\n                    raise http_exceptions.BadRequest(\"Enrichment Rule with id '{}' does not exist\".format(str(rule_id)))\n                response_data.append({'rule_id': rule_id})\n\n        db.commit()\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return createApiResponse(\n            msg='Enrichment Rule(s) updated',\n            data=response_data,\n            kamreload=True,\n        )\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@api.route(\"/api/v1/numberenrichment\", methods=['DELETE'])\n@api.route(\"/api/v1/numberenrichment/<int:rule_id>\", methods=['DELETE'])\n@api_security\ndef deleteNumberEnrichment(rule_id, request_payload=None):\n    \"\"\"\n    Delete one or multiple enrichment rules\\n\n    The rule_id can be provided in url path or payload\n\n    ===============\n    Request Payload\n    ===============\n\n    .. code-block:: json\n\n        {\n            data: [\n                {\n                    rule_id: <int>\n                },\n                ...\n            ]\n        }\n\n    ================\n    Response Payload\n    ================\n\n    .. code-block:: json\n\n        {\n            error: <str>,\n            msg: <str>,\n            kamreload: <bool>,\n            data: []\n        }\n    \"\"\"\n\n    db = DummySession()\n\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    VALID_REQUEST_DATA_ARGS = {\"rule_id\": int}\n\n    # ensure requred args are provided\n    REQUIRED_REQUEST_DATA_ARGS = {'rule_id'}\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        db = startSession()\n\n        # allow calling function with payload data\n        if request_payload is None:\n            request_payload = getRequestData()\n\n        # sanity checks\n        if rule_id is not None:\n            request_payload['data'] = [{'rule_id': int(rule_id)}]\n        elif 'data' not in request_payload or len(request_payload['data']) == 0:\n            raise http_exceptions.BadRequest(\"Request argument 'data' is required and must be non-zero length\")\n        for data in request_payload['data']:\n            if not isinstance(data, dict):\n                raise http_exceptions.BadRequest(\"Request argument 'data' not valid\")\n            for k, v in data.items():\n                if k not in VALID_REQUEST_DATA_ARGS.keys():\n                    raise http_exceptions.BadRequest(\"Request argument '{}' not recognized\".format(k))\n                if not type(v) == VALID_REQUEST_DATA_ARGS[k]:\n                    try:\n                        request_payload[k] = VALID_REQUEST_DATA_ARGS[k](v)\n                        continue\n                    except:\n                        raise http_exceptions.BadRequest(\"Request argument '{}' not valid\".format(k))\n            for k in REQUIRED_REQUEST_DATA_ARGS:\n                if k not in data:\n                    raise http_exceptions.BadRequest(\"Required argument '{}' missing in data entry\".format(k))\n\n        rule_ids = [x['rule_id'] for x in request_payload['data']]\n        rules = db.query(dSIPDNIDEnrichment).filter(dSIPDNIDEnrichment.id.in_(rule_ids))\n        if rules is not None:\n            rules.delete(synchronize_session=False)\n        else:\n            raise http_exceptions.NotFound(\"The enrichment rule(s) do not exist\")\n\n        db.commit()\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return createApiResponse(\n            msg='Enrichment Rule(s) deleted',\n            kamreload=True,\n        )\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@api.route(\"/api/v1/numberenrichment/fetch\", methods=['POST'])\n@api_security\ndef fetchNumberEnrichment(request_payload=None):\n    \"\"\"\n    Fetch a CSV to import from an external server\\n\n    Updates and adds rules by default\\n\n    If replace_rules is True then current rules are replaced\n\n    ===============\n    Request Payload\n    ===============\n\n    .. code-block:: json\n\n        {\n            data: [\n                {\n                    url: <str>\n                },\n                ...\n            ],\n            replace_rules: <bool>\n        }\n\n    ================\n    Response Payload\n    ================\n\n    .. code-block:: json\n\n        {\n            error: <str>,\n            msg: <str>,\n            kamreload: <bool>,\n            data: [\n                {\n                    rule_id: <int>,\n                    dnid: <str>,\n                    country_code: <str>,\n                    routing_number: <str>,\n                    rule_name: <str>\n                },\n                ...\n            ]\n        }\n    \"\"\"\n\n    # for validation of request args\n    VALID_REQUEST_ARGS = {\"data\": list, \"replace_rules\": bool}\n\n    # for validation of data args\n    VALID_REQUEST_DATA_ARGS = {\"url\": str}\n\n    # ensure required args are provided\n    REQUIRED_REQUEST_DATA_ARGS = {'url'}\n\n    response_payload = {'data': []}\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        db = startSession()\n\n        # allow calling function with payload data\n        if request_payload is None:\n            request_payload = getRequestData()\n\n        # sanity checks\n        if len(request_payload['data']) == 0:\n            raise http_exceptions.BadRequest(\"Request argument 'data' must be non-zero length\")\n        for k, v in request_payload.items():\n            if k not in VALID_REQUEST_ARGS.keys():\n                raise http_exceptions.BadRequest(\"Request argument '{}' not recognized\".format(k))\n            if not type(v) == VALID_REQUEST_ARGS[k]:\n                try:\n                    request_payload[k] = VALID_REQUEST_ARGS[k](v)\n                    continue\n                except:\n                    raise http_exceptions.BadRequest(\"Request argument '{}' not valid\".format(k))\n        for data in request_payload['data']:\n            for k, v in data.items():\n                if k not in VALID_REQUEST_DATA_ARGS.keys():\n                    raise http_exceptions.BadRequest(\"Request argument '{}' not recognized\".format(k))\n                if not type(v) == VALID_REQUEST_DATA_ARGS[k]:\n                    try:\n                        request_payload[k] = VALID_REQUEST_DATA_ARGS[k](v)\n                        continue\n                    except:\n                        raise http_exceptions.BadRequest(\"Request argument '{}' not valid\".format(k))\n            for k in REQUIRED_REQUEST_DATA_ARGS:\n                if k not in data:\n                    raise http_exceptions.BadRequest(\"Required argument '{}' missing in data entry\".format(k))\n\n        # prep for gathering data from csv\n        new_rules = []\n        resource_urls = [x['url'] for x in request_payload['data']]\n\n        # TODO: we should not blindly download the file, we need to some security checks here\n        # TODO: we should be doing downloads in separate threads\n        for url in resource_urls:\n            with closing(requests.get(url, stream=True)) as resp:\n                # iterator for csv stream lines\n                iter_lines = codecs.iterdecode(resp.iter_lines(), 'utf-8')\n\n                # lines for sniffer to detect csv type\n                line_1 = next(iter_lines, None)\n                if line_1 is None:\n                    raise http_exceptions.BadRequest(\"File at fetch URL is invalid\")\n                line_2 = next(iter_lines, None)\n\n                # detect csv dialect and if header is present\n                sniffer_lines = line_1 + '\\n' + line_2 if line_2 is not None else line_1\n                dialect = csv.Sniffer().sniff(sniffer_lines, delimiters=',;|')\n                has_header = csv.Sniffer().has_header(sniffer_lines)\n\n                # add valid values from sniffer lines to rules\n                start_lines = []\n                if not has_header:\n                    start_lines.append(line_1)\n                if line_2 is not None:\n                    start_lines.append(line_2)\n                for row in csv.reader((line for line in start_lines), dialect):\n                    new_rules.append({\n                        \"dnid\": row[0],\n                        \"country_code\": row[1],\n                        \"routing_number\": row[2],\n                        \"rule_name\": row[3]\n                    })\n\n                # read rest of csv stream and add to rules\n                for row in csv.reader(iter_lines, dialect):\n                    new_rules.append({\n                        \"dnid\": row[0],\n                        \"country_code\": row[1],\n                        \"routing_number\": row[2],\n                        \"rule_name\": row[3]\n                    })\n\n        # if replacing rules delete all then add\n        # if updating rules merge based on dnid (must be unique still)\n        replace_rules = request_payload['replace_rules'] if 'replace_rules' in request_payload else False\n        if replace_rules:\n            db.query(dSIPDNIDEnrichment).delete(synchronize_session=False)\n            db.flush()\n\n            ret = addNumberEnrichment(request_payload={'data': new_rules})\n            if ret[1] != StatusCodes.HTTP_OK:\n                resp = ret[0].get_json(force=True)\n                response_payload['error'] = resp['error']\n                response_payload['msg'] = resp['msg']\n                return createApiResponse(**response_payload, status_code=ret[1])\n            response_payload['msg'] = \"Enrichment Rule(s) replaced\"\n        else:\n            current_rules_lut = {\n                x.dnid: {'rule_id': x.id}\n                for x in db.query(dSIPDNIDEnrichment).all()\n            }\n            current_dnids = set(current_rules_lut.keys())\n            new_rules_lut = {\n                x['dnid']: {'dnid': x['dnid'], 'country_code': x['country_code'],\n                            'routing_number': x['routing_number'], 'rule_name': x['rule_name']}\n                for x in new_rules\n            }\n            new_dnids = set(new_rules_lut.keys())\n\n            add_dnids = new_dnids - current_dnids\n            upd_dnids = current_dnids & new_dnids\n\n            updated_rules = []\n            for dnid in add_dnids:\n                updated_rules.append(new_rules_lut[dnid])\n            for dnid in upd_dnids:\n                updated_rules.append({'rule_id': current_rules_lut[dnid]['rule_id'], **new_rules_lut[dnid]})\n\n            ret = updateNumberEnrichment(request_payload={'data': updated_rules})\n            if ret[1] != StatusCodes.HTTP_OK:\n                resp = ret[0].get_json(force=True)\n                response_payload['error'] = resp['error']\n                response_payload['msg'] = resp['msg']\n                return createApiResponse(**response_payload, status_code=ret[1])\n            response_payload['msg'] = \"Enrichment Rule(s) updated\"\n\n        # let requestor know what rules are now available\n        rules = db.query(dSIPDNIDEnrichment).all()\n        for rule in rules:\n            response_payload['data'].append({\n                'rule_id': rule.id,\n                'dnid': rule.dnid,\n                'country_code': rule.country_code,\n                'routing_number': rule.routing_number,\n                'rule_name': strFieldsToDict(rule.description)['name']\n            })\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return createApiResponse(\n            **response_payload,\n            kamreload=True,\n        )\n\n    except Exception as ex:\n        return showApiError(ex, response_payload)\n\n\n# TODO: standardize response payload (use data param)\n# TODO: stop shadowing builtin functions -> type == builtin\n# TODO: too manu use cases in this one function, split it up into constituent pieces\ndef generateCDRS(\n    gwgroupid,\n    report_type=None,\n    send_email=None,\n    dtfilter=None,\n    cdrfilter=None,\n    nonCompletedCalls=None,\n    run_standalone=False,\n    filter_params=None,\n):\n    \"\"\"\n    Generate CDRs Report for a gwgroup\n\n    :param gwgroupid:       gwgroup to generate cdr's for\n    :type gwgroupid:        int|str\n    :param report_type:            type of report (json|csv)\n    :type report_type:             str\n    :param send_email:           whether the report should be emailed\n    :type send_email:            bool\n    :param dtfilter:        time before which cdr's are not returned\n    :type dtfilter:         datetime\n    :param cdrfilter:       comma seperated cdr id's to include\n    :type cdrfilter:        str\n    :return:                returns a json response or file\n    :rtype:                 flask.Response\n    \"\"\"\n\n    # if run in standalone mode we need to setup logging\n    initSyslogLogger()\n\n    db = DummySession()\n\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': None, 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n    # define here so we can cleanup in finally statement\n    csv_file = ''\n\n    try:\n        db = startSession()\n\n        if isinstance(gwgroupid, int):\n            gwgroupid = str(gwgroupid)\n        if report_type is None:\n            report_type = 'json'\n        else:\n            report_type = report_type.lower()\n        if send_email is None:\n            send_email = False\n        if dtfilter is None:\n            dtfilter = datetime.min\n        if cdrfilter is None:\n            cdrfilter = tuple()\n        else:\n            cdrfilter = tuple(cdrfilter.split(','))\n        if nonCompletedCalls is None:\n            nonCompletedCalls = True\n        if filter_params is None:\n            filter_params = {'paging': False, 'searching': False, 'ordering': False}\n\n        gwgroup = db.query(GatewayGroups).filter(GatewayGroups.id == gwgroupid).first()\n        if gwgroup is not None:\n            gwgroupName = strFieldsToDict(gwgroup.description)['name']\n        else:\n            response_payload['status'] = \"0\"\n            response_payload['message'] = \"Endpont group doesn't exist\"\n            if run_standalone:\n                IO.logerr(f'Endpont group {gwgroupid} does not exist')\n                return None\n            return jsonify(response_payload)\n\n        if len(cdrfilter) > 0:\n            sql_params = {\"gwgroupid\": gwgroupid, \"dtfilter\": dtfilter, \"cdrfilter\": cdrfilter}\n            query1 = (\n                \"\"\"SELECT t1.cdr_id, t1.call_start_time, t1.duration AS call_duration, t1.calltype AS call_direction,\n                          t2.id AS src_gwgroupid, substring_index(substring_index(t2.description, 'name:', -1), ',', 1) AS src_gwgroupname,\n                          t3.id AS dst_gwgroupid, substring_index(substring_index(t3.description, 'name:', -1), ',', 1) AS dst_gwgroupname,\n                          t1.src_username, t1.dst_username, t1.src_ip AS src_address, t1.dst_domain AS dst_address, t1.sip_call_id AS call_id\n                FROM cdrs t1\n                JOIN dr_gw_lists t2 ON (t1.src_gwgroupid = t2.id)\n                JOIN dr_gw_lists t3 ON (t1.dst_gwgroupid = t3.id)\n                WHERE (t2.id = :gwgroupid OR t3.id = :gwgroupid) AND t1.call_start_time >= :dtfilter AND t1.cdr_id IN :cdrfilter\n                ORDER BY t1.call_start_time DESC\"\"\"\n            )\n        else:\n            sql_params = {\"gwgroupid\": gwgroupid, \"dtfilter\": dtfilter}\n            query1 = (\n                \"\"\"SELECT t1.cdr_id, t1.call_start_time, t1.duration AS call_duration, t1.calltype AS call_direction,\n                          t2.id AS src_gwgroupid, substring_index(substring_index(t2.description, 'name:', -1), ',', 1) AS src_gwgroupname,\n                          t3.id AS dst_gwgroupid, substring_index(substring_index(t3.description, 'name:', -1), ',', 1) AS dst_gwgroupname,\n                          t1.src_username, t1.dst_username, t1.src_ip AS src_address, t1.dst_domain AS dst_address, t1.sip_call_id AS call_id\n                FROM cdrs t1\n                JOIN dr_gw_lists t2 ON (t1.src_gwgroupid = t2.id)\n                JOIN dr_gw_lists t3 ON (t1.dst_gwgroupid = t3.id)\n                WHERE (t2.id = :gwgroupid OR t3.id = :gwgroupid) AND t1.call_start_time >= :dtfilter\n                ORDER BY t1.call_start_time DESC\"\"\"\n            )\n\n        if nonCompletedCalls:\n            query2 = (\n                \"\"\"SELECT acc.id AS cdr_id, acc.time, 0, acc.calltype,\n                acc.src_gwgroupid, SUBSTRING_INDEX(SUBSTRING_INDEX(t2.description, 'name:', -1), ',', 1) AS src_gwgroupname,\n                acc.dst_gwgroupid, SUBSTRING_INDEX(SUBSTRING_INDEX(t3.description, 'name:', -1), ',', 1) AS dst_gwgroupname,\n                acc.src_user,acc.dst_user,acc.src_ip,acc.dst_domain,acc.callid\n                FROM acc\n                JOIN dr_gw_lists t2 ON (acc.src_gwgroupid = t2.id)\n                LEFT JOIN dr_gw_lists t3 ON (acc.dst_gwgroupid = t3.id)\n                WHERE (t2.id = :gwgroupid OR t3.id = :gwgroupid) AND acc.time >= :dtfilter\n                ORDER BY acc.time DESC\"\"\"\n            )\n            sql = f'SELECT SQL_CALC_FOUND_ROWS * FROM (({query1}) UNION ({query2})) t_all'\n        else:\n            sql = f'SELECT SQL_CALC_FOUND_ROWS * FROM ({query1}) t_all'\n\n        if filter_params['searching']:\n            if filter_params['search_regex']:\n                sql_params['search_val'] = filter_params['search_val']\n                sql = sql + ' WHERE ' + '''\n                `cdr_id` REGEXP :search_val\n                OR `call_start_time` REGEXP :search_val\n                OR `call_duration` REGEXP :search_val\n                OR `call_direction` REGEXP :search_val\n                OR `src_gwgroupname` REGEXP :search_val\n                OR `dst_gwgroupname` REGEXP :search_val\n                OR `src_username` REGEXP :search_val\n                OR `dst_username` REGEXP :search_val\n                OR `src_address` REGEXP :search_val\n                OR `dst_address` REGEXP :search_val\n                OR `call_id` REGEXP :search_val\n                '''\n            else:\n                sql_params['search_val'] = f\"%{filter_params['search_val']}%\"\n                sql = sql + ' WHERE ' + '''\n                `cdr_id` LIKE :search_val\n                OR `call_start_time` LIKE :search_val\n                OR `call_duration` LIKE :search_val\n                OR `call_direction` LIKE :search_val\n                OR `src_gwgroupname` LIKE :search_val\n                OR `dst_gwgroupname` LIKE :search_val\n                OR `src_username` LIKE :search_val\n                OR `dst_username` LIKE :search_val\n                OR `src_address` LIKE :search_val\n                OR `dst_address` LIKE :search_val\n                OR `call_id` LIKE :search_val\n                '''\n        if filter_params['ordering']:\n            sql_params['order_col'] = filter_params['order_col']\n            if filter_params['order_dir'] == 'desc':\n                sql = sql + ' ORDER BY :order_col DESC'\n            else:\n                sql = sql + ' ORDER BY :order_col ASC'\n        if filter_params['paging']:\n            sql_params['page_start'] = filter_params['page_start']\n            sql_params['page_len'] = filter_params['page_len']\n            sql = sql + ' LIMIT :page_len OFFSET :page_start'\n        sql = text(sql)\n\n        rows = db.execute(sql, sql_params).all()\n        total_rows = db.execute(text('SELECT FOUND_ROWS()')).scalar()\n        # TODO: does not work as described by datatables, clientside JS expects the count before limiting\n        # only effects the text displayed on the bottom left (filter shown when searching)\n        #filtered_rows = len(rows)\n        filtered_rows = total_rows\n\n        cdrs = []\n        dataFields = [\n            'cdr_id', 'call_start_time', 'call_duration', 'call_direction', 'src_gwgroupid',\n            'src_gwgroupname', 'dst_gwgroupid', 'dst_gwgroupname', 'src_username',\n            'dst_username', 'src_address', 'dst_address', 'call_id'\n        ]\n        for row in rows:\n            data = {}\n            data['cdr_id'] = int(row[0])\n            data['call_start_time'] = row[1]\n            data['call_duration'] = str(row[2])\n            data['call_direction'] = row[3]\n            data['src_gwgroupid'] = row[4]\n            data['src_gwgroupname'] = row[5]\n            data['dst_gwgroupid'] = row[6]\n            data['dst_gwgroupname'] = row[7]\n            data['src_username'] = row[8]\n            data['dst_username'] = row[9]\n            data['src_address'] = row[10]\n            data['dst_address'] = row[11]\n            data['call_id'] = row[12]\n            cdrs.append(data)\n\n        response_payload['status'] = \"200\"\n        response_payload['data'] = cdrs\n        response_payload['total_rows'] = total_rows\n        response_payload['filtered_rows'] = filtered_rows\n\n        # Convert array of dicts to csv format\n        if report_type == \"csv\":\n            now = time.strftime('%Y%m%d-%H%M%S')\n            filename = secure_filename('{}_{}.csv'.format(gwgroupName, now))\n            csv_file = '/tmp/{}'.format(filename)\n            with open(csv_file, 'w', newline='') as csv_fp:\n                if len(cdrs) > 0:\n                    dict_writer = csv.DictWriter(csv_fp, fieldnames=cdrs[0].keys())\n                    dict_writer.writeheader()\n                    dict_writer.writerows(cdrs)\n                else:\n                    dict_writer = csv.DictWriter(csv_fp, fieldnames=dataFields)\n                    dict_writer.writeheader()\n\n            if send_email:\n                # recipients required\n                cdr_info = db.query(dSIPCDRInfo).filter(dSIPCDRInfo.gwgroupid == gwgroupid).first()\n                if cdr_info is not None:\n                    # Setup the parameters to send the email\n                    data = {}\n                    data['html_body'] = \"<html>CDR Report for {}</html>\".format(gwgroupName)\n                    data['text_body'] = \"CDR Report for {}\".format(gwgroupName)\n                    data['subject'] = \"CDR Report for {}\".format(gwgroupName)\n                    data['attachments'] = [csv_file]\n                    data['recipients'] = cdr_info.email.split(',')\n                    sendEmail(**data)\n                    # remove CDRs from the payload that is being returned\n                    response_payload.pop('data')\n                    response_payload['format'] = 'csv'\n                    response_payload['type'] = 'email'\n                    if run_standalone:\n                        IO.loginfo(f'Sent CDR report for endpoint group {gwgroupid}')\n                        return None\n                    return jsonify(response_payload)\n\n            if run_standalone:\n                IO.logerr(f'Nowhere to send CDR report for endpoint group {gwgroupid} run in standalone mode')\n                return None\n            return send_file(csv_file, as_attachment=True), StatusCodes.HTTP_OK\n\n        if run_standalone:\n            IO.logerr(f'Nowhere to send CDR report for endpoint group {gwgroupid} run in standalone mode')\n            return None\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n        if os.path.exists(csv_file):\n            os.remove(csv_file)\n\n\n@api.route(\"/api/v1/cdrs/endpointgroups/<int:gwgroupid>\", methods=['GET'])\n@api_security\ndef getGatewayGroupCDRS(gwgroupid=None):\n    \"\"\"\n    Purpose\n\n    Getting the cdrs for a gatewaygroup\n\n    \"\"\"\n    if (settings.DEBUG):\n        debugEndpoint()\n\n    report_type = request.args.get('type', 'json')\n    if 'email' in request.args:\n        send_email = json.loads(request.args['email'])\n    else:\n        send_email = False\n    cdrfilter = request.args.get('filter', None)\n    if 'dtfilter' in request.args:\n        dtfilter = datetime.strptime(request.args['dtfilter'], \"%Y-%m-%d\")\n    else:\n        dtfilter = None\n    if 'nonCompletedCalls' in request.args:\n        nonCompletedCalls = json.loads(request.args['nonCompletedCalls'])\n    else:\n        nonCompletedCalls = True\n\n    if 'start' in request.args or 'length' in request.args:\n        page_start = int(request.args.get('start', 0))\n        page_len = int(request.args.get('length', 100))\n        filter_params = {\n            'paging': True,\n            'page_start': page_start,\n            'page_len': page_len,\n        }\n    else:\n        filter_params = {\n            'paging': False,\n        }\n    if 'search[value]' in request.args and len(request.args['search[value]']) > 0:\n        filter_params['searching'] = True\n        filter_params['search_val'] = request.args['search[value]']\n        if 'search[regex]' in request.args:\n            filter_params['search_regex'] = json.loads(request.args['search[regex]'])\n        else:\n            filter_params['search_regex'] = False\n    else:\n        filter_params['searching'] = False\n    if 'order[0][column]' in request.args:\n        filter_params['ordering'] = True\n        filter_params['order_col'] = int(request.args['order[0][column]']) + 1\n        filter_params['order_dir'] = request.args['order[0][dir]']\n    else:\n        filter_params['ordering'] = False\n\n    return generateCDRS(gwgroupid, report_type, send_email, dtfilter, cdrfilter, nonCompletedCalls, filter_params=filter_params)\n\n\n# TODO: standardize response payload (use createApiResponse())\n@api.route(\"/api/v1/cdrs/endpoint/<int:gwid>\", methods=['GET'])\n@api_security\ndef getGatewayCDRS(gwid=None):\n    \"\"\"\n    Purpose\n\n    Getting the cdrs for a gateway thats part of a gateway group\n\n    \"\"\"\n    db = DummySession()\n\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': None, 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n    try:\n        db = startSession()\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        query = text(\n            \"SELECT cdr_id, gwid, call_start_time, calltype AS direction, dst_username AS number,src_ip, dst_domain, duration,sip_call_id \"\n            \"FROM dr_gateways, cdrs \"\n            \"WHERE cdrs.src_ip=dr_gateways.address AND gwid=:gwid \"\n            \"ORDER BY call_start_time DESC\"\n        )\n        cdrs = db.execute(query, {'gwid':gwid})\n\n        rows = []\n        for cdr in cdrs:\n            row = {}\n            row['cdr_id'] = cdr[0]\n            row['gwid'] = cdr[1]\n            row['call_start_time'] = str(cdr[2])\n            row['calltype'] = cdr[3]\n            row['number'] = cdr[4]\n            row['src_ip'] = cdr[5]\n            row['dst_domain'] = cdr[6]\n            row['duration'] = cdr[7]\n            row['sip_call_id'] = cdr[8]\n\n            rows.append(row)\n\n            response_payload['cdrs'] = rows\n            response_payload['recordCount'] = len(rows)\n            response_payload['status'] = \"200\"\n\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@api.route(\"/api/v1/backupandrestore/backup\", methods=['GET'])\n@api_security\ndef createBackup():\n    \"\"\"\n    Generate a backup of the database\n    \"\"\"\n\n    # in case we error early make sure the variable is set\n    backup_path = ''\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        backup_path = os.path.join(settings.BACKUP_FOLDER, f'{time.strftime(\"%s\")}-db.sql')\n\n        dumpcmd = ['sudo', 'dsiprouter', 'backup', '-f', backup_path]\n        proc = subprocess.run(\n            dumpcmd,\n            capture_output=True,\n            text=True\n        )\n        proc.check_returncode()\n\n        return send_file(backup_path, as_attachment=True), StatusCodes.HTTP_OK\n\n    except Exception as ex:\n        return showApiError(ex)\n    finally:\n        if os.path.exists(backup_path):\n            os.remove(backup_path)\n\n\n@api.route(\"/api/v1/backupandrestore/restore\", methods=['POST'])\n@api_security\ndef restoreBackup():\n    \"\"\"\n    Restore backup of the database\n    \"\"\"\n\n    # in case we error early make sure the variable is set\n    restore_path = ''\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        if 'file' not in request.files:\n            raise http_exceptions.BadRequest(\"No file was sent\")\n\n        file = request.files['file']\n\n        if file.filename == '':\n            raise http_exceptions.BadRequest(\"No file name was sent\")\n\n        if file and allowed_file(file.filename, ALLOWED_EXTENSIONS={'sql'}):\n            filename = secure_filename(file.filename)\n            restore_path = os.path.join(settings.BACKUP_FOLDER, filename)\n            file.save(restore_path)\n        else:\n            raise http_exceptions.BadRequest(\"Improper file upload\")\n\n        restorecmd = ['sudo', 'dsiprouter', 'restore', '-f', restore_path]\n        proc = subprocess.run(\n            restorecmd,\n            capture_output=True,\n            text=True\n        )\n        proc.check_returncode()\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return createApiResponse(\n            msg='The restore was successful',\n            kamreload=True,\n        )\n\n    except Exception as ex:\n        return showApiError(ex)\n    finally:\n        if os.path.exists(restore_path):\n            os.remove(restore_path)\n\n\n@api.route(\"/api/v1/sys/generatepassword\", methods=['GET'])\n@api_security\ndef generatePassword():\n    \"\"\"\n    Generate a random password\n    \"\"\"\n\n    DEF_PASSWORD_LEN = 32\n\n    try:\n        return createApiResponse(\n            msg='Successfully generated password',\n            data=[urandomChars(DEF_PASSWORD_LEN)],\n        )\n    except Exception as ex:\n        return showApiError(ex)\n\n\n# TODO:  The response coming back from Kamailio Command Line\n#        is not in proper JSON format.  The field and Attributes\n#        are missing double quotes.  So, we need to fix the JSON\n#        payload and loop thru the result vs doing a find.\n#        Or if we do a JSONRPC call instead we will get proper JSON\ndef getOptionMessageStatus(domain):\n    \"\"\"\n    Check if Domain is receiving replies from OPTION Messages\n    :param domain:  domain to check\n    :type domain:   str\n    :return:        whether domain handled OPTION message correctly\n    :rtype:         bool\n    \"\"\"\n\n    domain_active = False\n    cmdset = {\"method\": \"dispatcher.list\", \"jsonrpc\": \"2.0\", \"id\": 1}\n\n    r = requests.get('http://127.0.0.1:5060/api/kamailio', json=cmdset)\n    if r.status_code >= 400:\n        try:\n            msg = r.json()['error']['message']\n        except:\n            msg = r.reason\n        ex = http_exceptions.HTTPException(msg)\n        ex.code = r.status_code\n        raise ex\n    else:\n        response = r.json()\n        records = response['result']['RECORDS']\n\n    if not response:\n        return False\n\n    try:\n        # Loop thru each record in the dispatcher list\n        for record in range(0, len(records)):\n            sets = records[record]\n            # Loop thru each set\n            for set in sets:\n                # print(\"{},{}\".format(sets[set]['ID'],domain))\n                # Loop thru each target\n                targets = sets[set]['TARGETS']\n                # Loop thru each destination within a target\n                for dest in range(0, len(targets)):\n                    # Grab the destination body and flags\n                    # The Body contains the domain name of the destionation\n                    dest_body = format(targets[dest]['DEST']['ATTRS']['BODY'])\n                    # The Flags specify is the destionation is sending and recieving option messages\n                    dest_flags = format(targets[dest]['DEST']['FLAGS'])\n                    if domain in dest_body:\n                        if dest_flags == \"AP\":\n                            domain_active = True\n                            continue\n    except Exception as ex:\n        raise ex\n\n    if not domain_active:\n        return False\n\n    return True\n\n\n@api.route(\"/api/v1/domains/msteams/test/<string:domain>\", methods=['GET'])\n@api_security\ndef testConnectivity(domain):\n    try:\n        test_data = {\"hostname_check\": False, \"tls_check\": False, \"option_check\": False}\n\n        external_ip_addr = getExternalIP()\n        internal_ip_address = hostToIP(domain)\n\n        # Check the external ip matchs the ip set on the server\n        if external_ip_addr == internal_ip_address:\n            test_data['hostname_check'] = True\n        # Try again, but use Google DNS resolver if the check fails with local DNS\n        else:\n            # Does the IP address of this server resolve to the domain\n            import dns.resolver\n\n            # Get the IP address of the domain from Google DNS\n            resolver = dns.resolver.Resolver()\n            resolver.nameservers = ['8.8.8.8']\n            try:\n                answers = resolver.query(domain, 'A')\n                for a in answers:\n                    # If the External IP and IP from DNS match then it passes the check\n                    if a.to_text() == external_ip_addr:\n                        test_data['hostname_check'] = True\n            except:\n                pass\n\n        # Check if Domain is the root of the CN\n        # GoDaddy Certs Don't work with Microsoft Direct routing\n        certInfo = isCertValid(domain, external_ip_addr, 5061)\n        if certInfo:\n            test_data['tls_check'] = certInfo\n\n        # check if domain can process OPTIONS messages\n        if getOptionMessageStatus(domain):\n            test_data['option_check'] = True\n\n        return createApiResponse(\n            msg='Tests ran without error',\n            data=[test_data],\n        )\n\n    except Exception as ex:\n        return showApiError(ex)\n\n\n# TODO: implement GET for single domain\n@api.route(\"/api/v1/certificates\", methods=['GET'])\n@api.route(\"/api/v1/certificates/<string:domain>\", methods=['GET'])\n@api_security\ndef getCertificates(domain=None):\n    db = DummySession()\n\n    response_data = []\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        db = startSession()\n\n        # if domain is not None:\n        #     domain_configs = getCustomTLSConfigs(domain)\n        # else:\n        # domain_configs = getCustomTLSConfigs()\n        if domain == None:\n            certificates = db.query(dSIPCertificates).all()\n        else:\n            certificates = db.query(dSIPCertificates).filter(dSIPCertificates.domain == domain).all()\n\n        for certificate in certificates:\n            # append summary of endpoint group data\n            response_data.append({\n                'id': certificate.id,\n                'domain': certificate.domain,\n                'type': certificate.type,\n                'assigned_domains': ''\n            })\n\n        db.commit()\n\n        return createApiResponse(\n            msg='Certificates found' if len(certificates) > 0 else 'No Certificates',\n            data=response_data,\n        )\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@api.route(\"/api/v1/certificates\", methods=['POST', 'PUT'])\n@api_security\ndef createCertificate():\n    \"\"\"\n    Create TLS cert for a domain\n\n    ===============\n    Request Payload\n    ===============\n\n    .. code-block:: json\n\n    {\n        domain: <string>,\n        ip: <int>,\n        port: <int>\n        server_name_mode: <int>\n    }\n    \"\"\"\n\n    db = DummySession()\n\n    CERT_TYPE_GENERATED = \"generated\"\n    CERT_TYPE_UPLOADED = \"uploaded\"\n\n    # Check parameters and raise exception if missing required parameters\n    requiredParameters = ['domain']\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        db = startSession()\n\n        request_payload = getRequestData()\n\n        for parameter in requiredParameters:\n            if parameter not in request_payload:\n                raise http_exceptions.BadRequest(\"Request Argument '{}' is Required\".format(parameter))\n            elif request_payload[parameter] is None or len(request_payload[parameter]) == 0:\n                raise http_exceptions.BadRequest(\"Value for Request Argument '{}' is Not Valid\".format(parameter))\n\n        # Process Request\n        domain = request_payload['domain']\n        if domain == \"default\":\n            domain = socket.gethostname()\n        ip = request_payload['ip'] if 'ip' in request_payload else settings.EXTERNAL_IP_ADDR\n        port = request_payload['port'] if 'port' in request_payload else 5061\n        server_name_mode = request_payload['server_name_mode'] \\\n            if 'server_name_mode' in request_payload else kamtls.KAM_TLS_SNI_ALL\n        key = request_payload['key'] if 'key' in request_payload else None\n        replace_default_cert = request_payload[\n            'replace_default_cert'] if 'replace_default_cert' in request_payload else None\n        cert = request_payload['cert'] if 'cert' in request_payload else None\n        email = request_payload['email'] if 'email' in request_payload else \"admin@\" + settings.DEFAULT_AUTH_DOMAIN\n\n        # Request Certificate via Let's Encrypt\n        if key is None and cert is None:\n            type = CERT_TYPE_GENERATED\n            try:\n                if settings.DEBUG:\n                    # Use the LetsEncrypt Staging Server\n                    key, cert = letsencrypt.generateCertificate(domain, email, debug=True, default=replace_default_cert)\n\n                else:\n                    # Use the LetsEncrypt Prod Server\n                    key, cert = letsencrypt.generateCertificate(domain, email, default=replace_default_cert)\n\n            except Exception as ex:\n                getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = False\n                raise http_exceptions.BadRequest(\n                    \"Issue with validating ownership of the domain.  Please add a DNS record for this domain and try again\")\n\n        # Convert Certificate and key to base64 so that they can be stored in the database\n        cert_base64 = base64.b64encode(cert.encode('ascii'))\n        key_base64 = base64.b64encode(key.encode('ascii'))\n\n        # Check if the domain exists.  If so, update versus writing new\n        certificate = db.query(dSIPCertificates).filter(dSIPCertificates.domain == domain).first()\n        if certificate is not None:\n            # Update\n            db.query(dSIPCertificates).filter(dSIPCertificates.domain == domain).update(\n                {'email': email, 'type': type, 'cert': cert_base64, 'key': key_base64})\n            db.commit()\n            # Update the Kamailio TLS Configuration\n            if not kamtls.updateCustomTLSConfig(domain, ip, port, server_name_mode):\n                raise Exception('Failed to add Certificate to Kamailio')\n\n        else:\n            # Store a new Certificate in dSIPCertificate Table\n            certificate = dSIPCertificates(domain, type, email, cert_base64, key_base64)\n            db.add(certificate)\n            db.commit()\n            # Write the Kamailio TLS Configuration\n            if not kamtls.addCustomTLSConfig(domain, ip, port, server_name_mode):\n                raise Exception('Failed to add Certificate to Kamailio')\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return createApiResponse(\n            msg=\"Certificate creation succeeded\",\n            data=[{\"id\": certificate.id}],\n            kamreload=True,\n        )\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@api.route(\"/api/v1/certificates/<string:domain>\", methods=['DELETE'])\n@api_security\ndef deleteCertificates(domain=None):\n    db = DummySession()\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        db = startSession()\n\n        # if domain is not None:\n        #     domain_configs = getCustomTLSConfigs(domain)\n        # else:\n        # domain_configs = getCustomTLSConfigs()\n\n        Certificates = db.query(dSIPCertificates).filter(dSIPCertificates.domain == domain)\n        Certificates.delete(synchronize_session=False)\n\n        # Remove the certificate from the file system\n        letsencrypt.deleteCertificate(domain)\n\n        # Remove from Kamailio TLS\n        kamtls.deleteCustomTLSConfig(domain)\n\n        db.commit()\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return createApiResponse(\n            msg='Certificate Deleted',\n            kamreload=True,\n        )\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@api.route(\"/api/v1/certificates/upload/<string:domain>\", methods=['POST'])\n@api_security\ndef uploadCertificates(domain=None):\n    db = DummySession()\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        data = getRequestData()\n\n        if 'domain' in data.keys():\n            domain = data['domain'][0]\n            print(domain)\n        else:\n            domain = \"default\"\n\n        if 'replace_default_cert' in data.keys():\n            replace_default_cert = data['replace_default_cert'][0]\n        else:\n            replace_default_cert = None\n\n        ip = settings.EXTERNAL_IP_ADDR\n        port = 5061\n        server_name_mode = kamtls.KAM_TLS_SNI_ALL\n\n        if replace_default_cert == \"true\":\n            # Replacing the default cert\n            cert_domain_dir = settings.DSIP_CERTS_DIR\n        else:\n            # Adding a another domain cert\n            cert_domain_dir = os.path.join(settings.DSIP_CERTS_DIR, domain)\n\n        # Create a directory for the domain\n        if not os.path.exists(cert_domain_dir):\n            os.makedirs(cert_domain_dir)\n            os.chmod(cert_domain_dir, 0o770)\n\n        files = request.files.getlist(\"certandkey\")\n        try:\n            keycert_pair = KeyCertPair(files)\n            keycert_pair.validateKeyCertPair()\n            pkey_bytes = keycert_pair.dumpPkey()\n            cert_bytes = keycert_pair.dumpCerts()\n        except Exception as ex:\n            raise http_exceptions.BadRequest(str(ex))\n\n        key_file = os.path.join(cert_domain_dir, 'dsiprouter-key.pem')\n        cert_file = os.path.join(cert_domain_dir, 'dsiprouter-cert.pem')\n\n        with open(key_file, 'wb') as newfile:\n            newfile.write(pkey_bytes)\n        with open(cert_file, 'wb') as newfile:\n            newfile.write(cert_bytes)\n\n        # Change owner to dsiprouter:kamailio so that Kamailio can load the configurations\n        change_owner(cert_domain_dir, \"dsiprouter\", \"kamailio\")\n        change_owner(key_file, \"dsiprouter\", \"kamailio\")\n        change_owner(cert_file, \"dsiprouter\", \"kamailio\")\n\n        # Convert Certificate and key to base64 so that they can be stored in the database\n        key_base64 = base64.b64encode(pkey_bytes)\n        cert_base64 = base64.b64encode(cert_bytes)\n\n        # Store Certificate in dSIPCertificate Table\n        certificate = dSIPCertificates(domain, \"uploaded\", None, cert_base64, key_base64)\n        db.add(certificate)\n\n        if not replace_default_cert:\n            # Write the Kamailio TLS Configuration if it's not the default\n            if not kamtls.addCustomTLSConfig(domain, ip, port, server_name_mode):\n                raise Exception('Failed to add Certificate to Kamailio')\n\n        db.commit()\n\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return createApiResponse(\n            msg=\"Certificate and Key were uploaded\",\n            data=[{\"id\": certificate.id}],\n            kamreload=True,\n        )\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n"
  },
  {
    "path": "gui/modules/api/auth/__init__.py",
    "content": ""
  },
  {
    "path": "gui/modules/api/auth/functions.py",
    "content": "from shared import debugException, debugEndpoint, stripDictVals, showError\nfrom database import startSession, DummySession, dSIPUser\nfrom sqlalchemy import exc as sql_exceptions\nfrom werkzeug import exceptions as http_exceptions\nfrom flask import request\nimport settings\nimport datetime\nfrom util.security import AES_CTR\n\n\ndef addDSIPUser(data=None):\n    \"\"\"\n    Add or Update a group of carriers\n    \"\"\"\n\n    db = DummySession()\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        if data is not None:\n            # Set the form variables to data parameter\n            form = data\n        else:\n            form = stripDictVals(request.form.to_dict())\n\n        username = form['username']\n        password = AES_CTR.encrypt(form['password'])\n        firstname = form['firstname']\n        lastname = form['lastname']\n        roles = ''\n        domains = ''\n        token = ''\n        token_expiration = datetime.datetime.now()\n\n        new_user = dSIPUser(firstname, lastname, username, password, roles, domains, token, token_expiration)\n        db.add(new_user)\n        db.flush()\n        db.commit()\n\n        return new_user\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        error = \"http\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n"
  },
  {
    "path": "gui/modules/api/auth/ldap/__init__.py",
    "content": ""
  },
  {
    "path": "gui/modules/api/auth/ldap/interface.py",
    "content": "import sys\n\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nimport ldap\nimport settings\n\n# required plugin global\nMETADATA = {\n    'name': 'LDAP Authentication',\n    'version': '1.0.0'\n}\n\n# required plugin interface\ndef initialize():\n    \"\"\"\n    Validate the module settings and perform any verification needed\n\n    :return:    None\n    :rtype:     None\n    :raises:    ValueError - when settings are invalid\n    \"\"\"\n\n    mod_settings = settings.AUTH_MODULES['ldap']\n    for req_key in ['LDAP_HOST', 'USER_ATTRIBUTE', 'USER_SEARCH_BASE']:\n        if req_key not in mod_settings:\n            raise ValueError(f'ldap module failed initialization: missing required setting \"{req_key}\"')\n    if 'REQUIRED_GROUP' in mod_settings:\n        if not 'GROUP_SEARCH_BASE' in mod_settings or not 'GROUP_MEMBER_ATTRIBUTE' in mod_settings:\n            raise ValueError(f'ldap module failed initialization: \"REQUIRED_GROUP\" requires \"GROUP_SEARCH_BASE\" avd \"GROUP_MEMBER_ATTRIBUTE\" to be set')\n\n    # TODO: validate ldap connection / store connection object\n\n# required plugin interface\ndef teardown():\n    \"\"\"\n    Cleanup any artifacts from the module\n\n    :return:    None\n    :rtype:     None\n    \"\"\"\n    pass\n\n# required plugin interface\ndef authenticate(username, password):\n    \"\"\"\n    Authenticate a user via an external LDAP server\n\n    :param username:        The username to authenticate with\n    :type username:         str\n    :param password:        The password to authenticate with\n    :type password:         str\n    :return:                Whether authentication was successful or not\n    :rtype:                 bool\n    \"\"\"\n\n    mod_settings = settings.AUTH_MODULES['ldap']\n\n    try:\n        # Enable TLS if ldaps is specified in the URI\n        if mod_settings['LDAP_HOST'][0:4].lower() == \"ldaps\":\n            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)\n\n        connect = ldap.initialize(mod_settings['LDAP_HOST'])\n        connect.set_option(ldap.OPT_REFERRALS, 0)\n\n        ldap_bind_user = \"{}={},{}\".format(\n            mod_settings['USER_ATTRIBUTE'],\n            username,\n            mod_settings['USER_SEARCH_BASE']\n        )\n        connect.simple_bind_s(ldap_bind_user, password)\n        if settings.DEBUG:\n            print(f'{METADATA[\"name\"]} - User authenticated: {ldap_bind_user}')\n\n        if 'REQUIRED_GROUP' in mod_settings:\n            ldap_member_filter = \"{}={}\".format(mod_settings['GROUP_MEMBER_ATTRIBUTE'], username)\n            if settings.DEBUG:\n                print(\"LDAP Member Filter: {}\".format(ldap_member_filter))\n\n            groups = connect.search_s(\n                mod_settings['GROUP_SEARCH_BASE'],\n                ldap.SCOPE_SUBTREE,\n                ldap_member_filter,\n                ['dn']\n            )\n            if settings.DEBUG:\n                print(\"List of groups found: {}\".format(groups))\n\n            for group in groups:\n                if mod_settings['REQUIRED_GROUP'] in group[0]:\n                    return True\n            return False\n\n        return True\n    except ldap.INVALID_CREDENTIALS:\n        return False\n"
  },
  {
    "path": "gui/modules/api/auth/routes.py",
    "content": "# make sure the generated source files are imported instead of the template ones\nimport sys\n\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nimport datetime, uuid\nfrom flask import Blueprint, jsonify\nfrom util.security import AES_CTR\nfrom shared import debugEndpoint, StatusCodes, getRequestData\nfrom database import DummySession, startSession, dSIPUser\nfrom modules.api.api_functions import showApiError, createApiResponse, api_security\nfrom modules.api.auth.functions import addDSIPUser\nfrom util.ipc import STATE_SHMEM_NAME, getSharedMemoryDict\nimport settings\n\nuser = Blueprint('user', __name__)\n\n\n# TODO: standardize response payloads using new createApiResponse()\n#       marked for implementation in v0.74\n\n\n@user.route('/api/v1/auth/login', methods=['POST'])\n# @api_security\ndef login():\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    VALID_REQUEST_DATA_ARGS = {\"username\": str, \"password\": str}\n\n    # ensure requred args are provided\n    REQUIRED_ARGS = {'username', 'password'}\n\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n    db = DummySession()\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        # get request data\n        request_data = getRequestData()\n\n        db = startSession()\n\n        # Check for existing user\n        existing_user = db.query(dSIPUser).filter(dSIPUser.username == (request_data['username'])).first()\n\n        if existing_user:\n            print(\"Saved Password: \", existing_user.password)\n            print(\"Decrypted: \", AES_CTR.decrypt(existing_user.password))\n            print(\"Provided: \", request_data['password'])\n\n            if str(request_data['password']) == AES_CTR.decrypt(existing_user.password):\n                if (not existing_user.token) or (datetime.datetime.now() > existing_user.token_expiration):\n                    existing_user.token = uuid.uuid4()\n                    existing_user.token_expiration = datetime.datetime.now() + datetime.timedelta(days=1)\n                    db.commit()\n\n                response_payload = {\n                    'message': 'Login successful',\n                    'token': existing_user.token,\n                    'expiration_date': existing_user.token_expiration\n                }\n\n                return jsonify(response_payload), StatusCodes.HTTP_OK\n            else:\n                response_payload = {\n                    \"message\": 'Invalid credentials',\n                    'payload': request_data\n                }\n\n                return jsonify(response_payload), StatusCodes.HTTP_UNAUTHORIZED\n        else:\n            # If user does not exist  return an invalid credentials message\n            response_payload['data'] = {\n                \"message\": 'Invalid credentials',\n                'payload': request_data\n            }\n\n            return jsonify(response_payload), StatusCodes.HTTP_UNAUTHORIZED\n\n    except Exception as ex:\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@user.route('/api/v1/auth/user', methods=['POST'])\n@api_security\ndef createUser():\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    VALID_REQUEST_DATA_ARGS = {\"firstname\": str, \"lastname\": str, \"username\": str, \"password\": str, \"roles\": dict,\n                               \"domains\": dict}\n\n    # ensure requred args are provided\n    REQUIRED_ARGS = {'firstname', 'lastname', 'username', 'password', 'roles', 'domains'}\n\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n    db = DummySession()\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        # get request data\n        request_data = getRequestData()\n\n        db = startSession()\n\n        # Check for existing user\n        existing_user = db.query(dSIPUser).filter(dSIPUser.username.like(request_data['username'])).all()\n        if (existing_user):\n            response_payload = {'error': 'User Already Exists', 'data': request_data}\n            return jsonify(response_payload), StatusCodes.HTTP_CONFLICT\n\n        # If user does not exist proceed to creat the new user\n        new_user = addDSIPUser(request_data)\n\n        # sanity checks\n        response_payload['data'] = {\n            \"message\": 'User Created Successfully',\n            'payload': request_data\n        }\n\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except Exception as ex:\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@user.route('/api/v1/auth/user', methods=['GET'])\ndef listUsers():\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n    db = DummySession()\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        # Check for existing user\n        user_list = db.query(dSIPUser).all()\n        response_payload = []\n\n        for the_user in user_list:\n            response_payload.append(\n                {\n                    \"id\": the_user.id,\n                    \"username\": the_user.username,\n                    \"firstname\": the_user.firstname,\n                    \"lastname\": the_user.lastname,\n                    \"roles\": [],\n                    \"domains\": []\n                }\n            )\n\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except Exception as ex:\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@user.route('/api/v1/auth/user/<int:id>', methods=['GET'])\n# @api_security\ndef getUser(id=None):\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    # VALID_REQUEST_DATA_ARGS = {\"firstname\": str, \"lastname\": str, \"username\": str, \"password\": str, \"roles\": dict, \"domains\": dict}\n\n    # ensure requred args are provided\n    # REQUIRED_ARGS = {'firstname', 'lastname', 'username', 'password', 'roles', 'domains'}\n\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n    db = DummySession()\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        if id is None:\n            response_payload = {'error': 'Invalid Request',\n                                'msg': 'Invalid Request. You seem to be missing the ID parameter.'}\n            return jsonify(response_payload), StatusCodes.HTTP_BAD_REQUEST\n\n        db = startSession()\n\n        # Check for existing user\n        existing_user = db.query(dSIPUser).get(id)\n        if (existing_user):\n            response_payload = {\n                \"username\": existing_user.username,\n                \"firstname\": existing_user.firstname,\n                \"lastname\": existing_user.lastname,\n                \"roles\": [],\n                \"domains\": []\n            }\n            return jsonify(response_payload), StatusCodes.HTTP_OK\n\n        else:\n            response_payload = {'error': 'User Not Found', 'msg': 'User Not Found'}\n            return jsonify(response_payload), StatusCodes.HTTP_NOT_FOUND\n\n    except Exception as ex:\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@user.route('/api/v1/auth/user/<int:id>', methods=['PUT'])\n# @api_security\ndef updateUser(id=None):\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    VALID_REQUEST_DATA_ARGS = {\"firstname\": str, \"lastname\": str, \"username\": str, \"password\": str, \"roles\": dict,\n                               \"domains\": dict}\n\n    # ensure requred args are provided\n    REQUIRED_ARGS = {'firstname', 'lastname', 'username', 'password', 'roles', 'domains'}\n\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n    db = DummySession()\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        if id is None:\n            response_payload = {'error': 'Invalid Request',\n                                'msg': 'Invalid Request. You seem to be missing the ID parameter.'}\n            return jsonify(response_payload), StatusCodes.HTTP_BAD_REQUEST\n\n        # get request data\n        request_data = getRequestData()\n\n        db = startSession()\n\n        # Check for existing user\n        existing_user = db.query(dSIPUser).get(id)\n        if existing_user:\n\n            existing_user.firstname = request_data['firstname']\n            existing_user.lastname = request_data['lastname']\n            existing_user.username = request_data['username']\n            existing_user.password = AES_CTR.encrypt(request_data['password'])\n            existing_user.roles = ''\n            existing_user.domains = ''\n            db.commit()\n\n            response_payload = {\"message\": 'User updated successfully', 'data': {\n                \"username\": existing_user.username,\n                \"firstname\": existing_user.firstname,\n                \"lastname\": existing_user.lastname,\n                \"roles\": [],\n                \"domains\": []\n            }}\n\n            return jsonify(response_payload), StatusCodes.HTTP_OK\n\n        else:\n            response_payload = {'error': 'User Not Found', 'msg': 'User Not Found'}\n            return jsonify(response_payload), StatusCodes.HTTP_NOT_FOUND\n\n    except Exception as ex:\n        return showApiError(ex)\n    finally:\n        db.close()\n\n\n@user.route('/api/v1/auth/user/<int:id>', methods=['DELETE'])\n# @api_security\ndef deleteUser(id=None):\n    db = DummySession()\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        if id is None:\n            response_payload = {'error': 'Invalid Request',\n                                'msg': 'Invalid Request. You seem to be missing the ID parameter.'}\n            return jsonify(response_payload), StatusCodes.HTTP_BAD_REQUEST\n\n        db = startSession()\n\n        # Check for existing user\n        existing_user = db.query(dSIPUser).get(id)\n        if existing_user:\n            db.delete(existing_user)\n            db.commit()\n            response_payload = {\"message\": 'User deleted successfully'}\n            return jsonify(response_payload), StatusCodes.HTTP_OK\n\n        else:\n            response_payload = {'error': 'User Not Found', 'msg': 'User Not Found'}\n            return jsonify(response_payload), StatusCodes.HTTP_NOT_FOUND\n\n    except Exception as ex:\n        return showApiError(ex)\n    finally:\n        db.close()\n"
  },
  {
    "path": "gui/modules/api/carriergroups/functions.py",
    "content": "import json, sys\nfrom flask import request, session, redirect, url_for, render_template\nfrom sqlalchemy import exc as sql_exceptions, text, cast, Integer, func\nfrom werkzeug import exceptions as http_exceptions\n# make sure the generated source files are imported instead of the template ones\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\nimport settings\nfrom shared import debugException, debugEndpoint, stripDictVals, strFieldsToDict, dictToStrFields, showError\nfrom database import startSession, DummySession, Gateways, Address, UAC, GatewayGroups, Dispatcher, DsipGwgroup2LB, \\\n    OutboundRoutes\nfrom util.ipc import STATE_SHMEM_NAME, getSharedMemoryDict\nfrom util.networking import safeUriToHost, safeFormatSipUri, safeStripPort, encodeSipUser\n\n\ndef displayCarrierGroups(gwgroup=None):\n    \"\"\"\n    Display the carrier groups in the view\n    :param gwgroup:\n    \"\"\"\n\n    # TODO: track related dr_rules and update lists on delete\n\n    db = DummySession()\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        query = db.query(\n            GatewayGroups.id,\n            GatewayGroups.description,\n            GatewayGroups.gwlist,\n            UAC.r_username,\n            UAC.auth_password,\n            UAC.r_domain,\n            UAC.auth_username,\n            UAC.auth_proxy,\n            cast(DsipGwgroup2LB.enabled, Integer).label('lb_enabled')\n        ).outerjoin(\n            UAC, GatewayGroups.id == UAC.l_uuid\n        ).outerjoin(\n            DsipGwgroup2LB, GatewayGroups.id == DsipGwgroup2LB.gwgroupid\n        ).filter(\n            GatewayGroups.description.regexp_match(GatewayGroups.FILTER.CARRIER.value)\n        )\n\n        # res must be a list()\n        if gwgroup is not None and gwgroup != \"\":\n            res = [\n                query.filter(\n                    GatewayGroups.id == gwgroup\n                ).first()\n            ]\n        else:\n            res = query.all()\n\n        return render_template('carriergroups.html', rows=res)\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\ndef addUpdateCarrierGroups(data=None):\n    \"\"\"\n    Add or Update a group of carriers\n    \"\"\"\n\n    db = DummySession()\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        if data is None:\n            # called from flask url router, make sure user is logged in\n            # TODO: toss this in a decorator func with error handling and use for gui auth checks\n            if not session.get('logged_in'):\n                return redirect(url_for('index'))\n\n            # grab data from gui form\n            form = stripDictVals(request.form.to_dict())\n        else:\n            # Set the form variables to data parameter\n            form = data\n\n        gwgroup = form['gwgroupid']\n        name = form['name']\n        lb_enabled = int(form['lb_enabled']) if 'lb_enabled' in form else 0\n        new_name = form['new_name'] if 'new_name' in form else ''\n        plugin_name = form['plugin_name'] if 'plugin_name' in form else ''\n        authtype = form['authtype'] if 'authtype' in form else ''\n        username = form['r_username'] if 'r_username' in form else ''\n        auth_username = form['auth_username'] if 'auth_username' in form else ''\n        auth_password = form['auth_password'] if 'auth_password' in form else ''\n        auth_domain = form['auth_domain'] if 'auth_domain' in form else settings.DEFAULT_AUTH_DOMAIN\n        auth_proxy = form['auth_proxy'] if 'auth_proxy' in form else ''\n\n        # Workaround: for Twilio Elastic SIP and Programmable SIP\n        # Set the Realm to sip.twilio.com if the domain contains a pstn.twilio.com or sip.twilio.com domain.\n        # Otherwise, set it to the name of the auth domain\n        auth_realm = \"sip.twilio.com\" if \"twilio.com\" in auth_domain else auth_domain\n\n        # format data\n        if authtype == \"userpwd\" and (plugin_name is None or plugin_name == ''):\n            auth_domain = safeUriToHost(auth_domain)\n            if auth_domain is None:\n                raise http_exceptions.BadRequest(\"Auth domain hostname/address is malformed\")\n            if len(auth_proxy) == 0:\n                auth_proxy = auth_domain\n            auth_proxy = safeFormatSipUri(auth_proxy)\n            if auth_proxy is None:\n                raise http_exceptions.BadRequest('Auth domain or proxy is malformed')\n            if len(auth_username) == 0:\n                auth_username = username\n            auth_username = encodeSipUser(auth_username)\n\n        # Adding\n        if len(gwgroup) <= 0:\n            Gwgroup = GatewayGroups(name, type=settings.FLT_CARRIER)\n            db.add(Gwgroup)\n            db.flush()\n            gwgroup = Gwgroup.id\n\n            # Add auth_domain(aka registration server) to the gateway list\n            if authtype == \"userpwd\" and (plugin_name is None or plugin_name == ''):\n                Uacreg = UAC(gwgroup, username, auth_password, realm=auth_realm, auth_username=auth_username, auth_proxy=auth_proxy,\n                    local_domain=settings.EXTERNAL_FQDN, remote_domain=auth_domain)\n                Addr = Address(name + \"-uac\", auth_domain, 32, settings.FLT_CARRIER, gwgroup=gwgroup)\n                db.add(Uacreg)\n                db.add(Addr)\n\n        # Updating\n        else:\n            # config form\n            if len(name) > 0:\n                Gwgroup = db.query(GatewayGroups).filter(GatewayGroups.id == gwgroup).first()\n                gwgroup_fields = strFieldsToDict(Gwgroup.description)\n                old_name = gwgroup_fields['name']\n                gwgroup_fields['name'] = name\n                Gwgroup.description = dictToStrFields(gwgroup_fields)\n\n                Addr = db.query(Address).filter(\n                    Address.tag.regexp_match(f'name:{old_name}-uac(,|$)')\n                ).first()\n                if Addr is not None:\n                    addr_fields = strFieldsToDict(Addr.tag)\n                    addr_fields['name'] = 'name:{}-uac'.format(new_name)\n                    Addr.tag = dictToStrFields(addr_fields)\n\n            # auth form\n            if authtype == \"userpwd\" and (plugin_name is None or plugin_name == ''):\n                # update uacreg if exists, otherwise create\n                if not db.query(UAC).filter(UAC.l_uuid == gwgroup).update(\n                    {\n                        'l_username': username, 'r_username': username, 'auth_username': auth_username,\n                        'auth_password': auth_password, 'r_domain': auth_domain, 'realm': auth_realm,\n                        'auth_proxy': auth_proxy, 'flags': UAC.FLAGS.REG_ENABLED.value\n                    },\n                    synchronize_session=False\n                ):\n                    Uacreg = UAC(gwgroup, username, auth_password, realm=auth_domain, auth_username=auth_username,\n                                    auth_proxy=auth_proxy, local_domain=settings.EXTERNAL_FQDN, remote_domain=auth_domain)\n                    db.add(Uacreg)\n\n                # update address if exists, otherwise create\n                if not db.query(Address).filter(\n                    Address.tag.contains(\"name:{}-uac\".format(name))\n                ).update({'ip_addr': auth_domain}, synchronize_session=False):\n                    Addr = Address(name + \"-uac\", auth_domain, 32, settings.FLT_CARRIER, gwgroup=gwgroup)\n                    db.add(Addr)\n            else:\n                # delete uacreg and address if they exist\n                db.query(UAC).filter(UAC.l_uuid == gwgroup).delete(synchronize_session=False)\n                db.query(Address).filter(\n                    Address.tag.regexp_match(f'name:{name}-uac(,|$)')\n                ).delete(synchronize_session=False)\n\n        # toggle load balancing based on user input\n        # TODO: this WILL be changed in the future when we refactor load balancing\n        fields = strFieldsToDict(Gwgroup.description)\n        fields['lb'] = gwgroup\n        Gwgroup.description = dictToStrFields(fields)\n        db.flush()\n        db.execute(\n            text(\"UPDATE dsip_gwgroup2lb SET enabled = :enabled WHERE gwgroupid = :gwgroupid\"),\n            {'enabled': lb_enabled, 'gwgroupid': gwgroup}\n        )\n\n        db.commit()\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        if data is None:\n            return displayCarrierGroups()\n        else:\n            return gwgroup\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        error = \"http\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\ndef displayCarriers(gwid=None, gwgroup=None, newgwid=None):\n    \"\"\"\n    Display the carriers table\n    :param gwid:\n    :param gwgroup:\n    :param newgwid:\n    \"\"\"\n\n    db = DummySession()\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        # carriers is a list of carriers matching query\n        # carrier_routes is a list of associated rules for each carrier\n        carriers = []\n        carrier_rules = []\n\n        # get carrier by id\n        if gwid is not None:\n            carriers = [\n                db.query(\n                    Gateways.gwid, Gateways.description, Gateways.address, Gateways.strip, Gateways.pri_prefix,\n                    Dispatcher.attrs.label(\"dispatcher_attrs\")\n                ).filter(\n                    Gateways.gwid == gwid\n                ).outerjoin(\n                    Dispatcher,\n                    Dispatcher.description.regexp_match(f'gwid={gwid}(;|$)')\n                ).first()\n            ]\n            rules = db.query(OutboundRoutes).filter(OutboundRoutes.groupid == settings.FLT_OUTBOUND).all()\n            gateway_rules = {}\n            for rule in rules:\n                if str(gwid) in filter(None, rule.gwlist.split(',')):\n                    gateway_rules[rule.ruleid] = strFieldsToDict(rule.description)['name']\n            carrier_rules.append(json.dumps(gateway_rules, separators=(',', ':')))\n\n        # get carriers by carrier group\n        elif gwgroup is not None:\n            Gatewaygroup = db.query(GatewayGroups).filter(GatewayGroups.id == gwgroup).first()\n            # check if any endpoints in group b4 converting to list(int)\n            if Gatewaygroup is not None and Gatewaygroup.gwlist != \"\":\n                gwlist = [int(gw) for gw in filter(None, Gatewaygroup.gwlist.split(\",\"))]\n                carriers = db.query(\n                    Gateways.gwid, Gateways.description, Gateways.address, Gateways.strip, Gateways.pri_prefix,\n                    Dispatcher.attrs.label(\"dispatcher_attrs\")\n                ).filter(\n                    Gateways.gwid.in_(gwlist)\n                ).outerjoin(\n                    Dispatcher,\n                    Dispatcher.description.regexp_match(func.CONCAT('gwid=', Gateways.gwid, '(;|$)'))\n                ).all()\n                rules = db.query(OutboundRoutes).filter(OutboundRoutes.groupid == settings.FLT_OUTBOUND).all()\n                for gateway_id in filter(None, Gatewaygroup.gwlist.split(\",\")):\n                    gateway_rules = {}\n                    for rule in rules:\n                        if gateway_id in filter(None, rule.gwlist.split(',')):\n                            gateway_rules[rule.ruleid] = strFieldsToDict(rule.description)['name']\n                    carrier_rules.append(json.dumps(gateway_rules, separators=(',', ':')))\n\n        # get all carriers\n        else:\n            carriers = db.query(\n                Gateways.gwid, Gateways.description, Gateways.address, Gateways.strip, Gateways.pri_prefix,\n                Dispatcher.attrs.label(\"dispatcher_attrs\")\n            ).filter(\n                Gateways.type == settings.FLT_CARRIER\n            ).outerjoin(\n                Dispatcher,\n                Dispatcher.description.regexp_match(func.CONCAT('gwid=', Gateways.gwid, '(;|$)'))\n            ).all()\n            rules = db.query(OutboundRoutes).filter(OutboundRoutes.groupid == settings.FLT_OUTBOUND).all()\n            for gateway in carriers:\n                gateway_rules = {}\n                for rule in rules:\n                    if str(gateway.gwid) in filter(None, rule.gwlist.split(',')):\n                        gateway_rules[rule.ruleid] = strFieldsToDict(rule.description)['name']\n                carrier_rules.append(json.dumps(gateway_rules, separators=(',', ':')))\n\n        return render_template('carriers.html', rows=carriers, routes=carrier_rules, gwgroup=gwgroup, new_gwid=newgwid,\n            kam_reload_required=getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'])\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        db.rollback()\n        db.flush()\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\ndef addUpdateCarriers(data=None):\n    \"\"\"\n    Add or Update a carrier\n    \"\"\"\n\n    db = DummySession()\n    newgwid = None\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        if data is None:\n            # called from flask url router, make sure user is logged in\n            # TODO: toss this in a decorator func with error handling and use for gui auth checks\n            if not session.get('logged_in'):\n                return redirect(url_for('index'))\n            # grab data from gui form\n            form = stripDictVals(request.form.to_dict())\n        else:\n            # Set the form variables to data parameter\n            form = data\n\n        \n        # match what the API would send us\n        if 'gwgroupid' in form:\n            form['gwgroupid'] = str(form['gwgroupid'])\n\n        # match what the UI would send us\n        if 'gwgroup' in form:\n            form['gwgroupid'] = str(form['gwgroup'])\n        if 'ip_addr' in form:\n            form['hostname'] = str(form['ip_addr'])\n\n\n        gwid = form['gwid'] if 'gwid' in form else ''\n        gwgroup = form['gwgroupid'] if len(form['gwgroupid']) > 0 else ''\n        name = form['name'] if len(form['name']) > 0 else ''\n        hostname = form['hostname'] if len(form['hostname']) > 0 else ''\n        strip = form['strip'] if len(form['strip']) > 0 else '0'\n        prefix = form['prefix'] if len(form['prefix']) > 0 else ''\n        rweight = int(form['rweight']) if form.get('rweight', '') != '' else 1\n\n        if len(hostname) == 0:\n            raise http_exceptions.BadRequest(\"Carrier hostname/address is required\")\n\n        sip_addr = safeUriToHost(hostname, default_port=5060)\n        if sip_addr is None:\n            raise http_exceptions.BadRequest(\"Endpoint hostname/address is malformed\")\n        host_addr = safeStripPort(sip_addr)\n\n        # Adding\n        if len(gwid) <= 0:\n            if len(gwgroup) > 0:\n                Addr = Address(name, host_addr, 32, settings.FLT_CARRIER, gwgroup=gwgroup)\n                db.add(Addr)\n                db.flush()\n\n                Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_CARRIER, gwgroup=gwgroup, addr_id=Addr.id)\n                db.add(Gateway)\n                db.flush()\n\n                newgwid = Gateway.gwid\n                gwid = str(newgwid)\n                Gatewaygroup = db.query(GatewayGroups).filter(GatewayGroups.id == gwgroup).first()\n                gwlist = list(filter(None, Gatewaygroup.gwlist.split(\",\")))\n                gwlist.append(gwid)\n                Gatewaygroup.gwlist = ','.join(gwlist)\n\n                # Create dispatcher group with the set id being the gateway group id\n                dispatcher = Dispatcher(setid=gwgroup, destination=sip_addr, rweight=rweight, name=name, gwid=gwid)\n                db.add(dispatcher)\n            else:\n                Addr = Address(name, host_addr, 32, settings.FLT_CARRIER)\n                db.add(Addr)\n                db.flush()\n\n                Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_CARRIER, addr_id=Addr.id)\n                db.add(Gateway)\n\n        # Updating\n        else:\n            Gateway = db.query(Gateways).filter(Gateways.gwid == gwid).first()\n            Gateway.address = sip_addr\n            Gateway.strip = strip\n            Gateway.pri_prefix = prefix\n\n            gw_fields = strFieldsToDict(Gateway.description)\n            gw_fields['name'] = name\n            if len(gwgroup) <= 0:\n                gw_fields['gwgroup'] = gwgroup\n\n            # update dispatcher entry\n            if db.query(Dispatcher).filter(\n                Dispatcher.description.regexp_match(f'gwid={gwid}(;|$)')\n            ).update({\n                \"attrs\": Dispatcher.buildAttrs(rweight=rweight)\n            }, synchronize_session=False):\n                pass\n            # add new dispatcher entry if it did not exist (gwgroup must also exist)\n            elif len(gwgroup) > 0:\n                dispatcher = Dispatcher(setid=gwgroup, destination=sip_addr, rweight=rweight, name=name, gwid=gwid)\n                db.add(dispatcher)\n\n            # if address exists update\n            address_exists = False\n            if 'addr_id' in gw_fields and len(gw_fields['addr_id']) > 0:\n                Addr = db.query(Address).filter(Address.id == gw_fields['addr_id']).first()\n\n                # if entry is non existent handle in next block\n                if Addr is not None:\n                    address_exists = True\n\n                    Addr.ip_addr = host_addr\n                    addr_fields = strFieldsToDict(Addr.tag)\n                    addr_fields['name'] = name\n\n                    if len(gwgroup) > 0:\n                        addr_fields['gwgroup'] = gwgroup\n                    Addr.tag = dictToStrFields(addr_fields)\n\n            # otherwise create the address\n            if not address_exists:\n                if len(gwgroup) > 0:\n                    Addr = Address(name, host_addr, 32, settings.FLT_CARRIER, gwgroup=gwgroup)\n                else:\n                    Addr = Address(name, host_addr, 32, settings.FLT_CARRIER)\n\n                db.add(Addr)\n                db.flush()\n                gw_fields['addr_id'] = str(Addr.id)\n\n            # gw_fields may be updated above so set after\n            Gateway.description = dictToStrFields(gw_fields)\n\n        db.commit()\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n\n        if data is None:\n            return displayCarriers(gwgroup=gwgroup, newgwid=newgwid)\n        return gwid\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        db.rollback()\n        db.flush()\n        return showError(type='http', code=ex.code, msg=ex.description)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n"
  },
  {
    "path": "gui/modules/api/carriergroups/plugin/twilio/carrier_plugintype_version.py",
    "content": ""
  },
  {
    "path": "gui/modules/api/carriergroups/plugin/twilio/interface.py",
    "content": "import os\nfrom twilio.rest import Client\n\n# Metadata about plugin\nplugin_version = 1.0\nplugin_name = \"twilio\"\ndefault_twilio_domain_name = \"pstn.twilio.com\"\n\n\n# Get plugin meta data\ndef getPluginMetaData():\n    return \"{'authfields': ['account_sid':'string','account_token':'string']}, \\\n             'prefix': '+', \\\n            }\"\n\n# Initializes the plugin\ndef init(account_sid,auth_token):\n    \n    \n    try:\n\n        # Find your Account SID and Auth Token at twilio.com/console\n        # and set the environment variables. See http://twil.io/secure\n        account_sid = os.environ['TWILIO_ACCOUNT_SID'] if account_sid == None else account_sid\n        auth_token = os.environ['TWILIO_AUTH_TOKEN'] if auth_token == None else auth_token\n    \n        client = Client(account_sid, auth_token)\n        return client\n    \n    except KeyError as ke:\n        print(\"The {} key is not set\".format(ke))\n\n    except Exception as ex:\n        raise Exception( type(ex).__name__ + \"-\" + str(ex))\n        print(ex)\n        return False\n\n\ndef createTrunk(client,trunk_name,dsip_ip_address,twilio_domain_name=default_twilio_domain_name):\n\n    try:\n        # Convert trunk name to a lower case\n        trunk_name = trunk_name.lower()\n        fqdn_domain_name = \"{}.{}\".format(trunk_name,twilio_domain_name)\n        trunk = client.trunking.trunks.create(friendly_name=trunk_name,domain_name=fqdn_domain_name)\n       \n        if trunk:\n            trunk.ip_access_control_lists.create(createIPAccessControlList(client,trunk_name,dsip_ip_address))\n   \n    except Exception as ex:\n        raise Exception( type(ex).__name__ + \"-\" + str(ex))\n\n    return trunk.sid\n\ndef createIPAccessControlList(client,trunk_name,dsip_ip_address):\n\n    ip_access_control_list = client.sip \\\n                                .ip_access_control_lists \\\n                                .create(friendly_name=trunk_name)\n\n\n    ip_address = client.sip \\\n            .ip_access_control_lists(ip_access_control_list.sid) \\\n            .ip_addresses \\\n            .create(friendly_name=trunk_name, ip_address=dsip_ip_address)\n\n\n    return ip_access_control_list.sid\n\n# Used for unit testing\n\ndef main():\n\n    trunk_name=\"dSIPRouter\"\n    dsip_ip_address=\"138.197.157.191/32\"\n    try:\n        if init():\n            createTrunk(trunk_name, dsip_ip_address)\n    except Exception as ex:\n        print(ex)\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "gui/modules/api/carriergroups/routes.py",
    "content": "import sys, os, importlib.util\n# make sure the generated source files are imported instead of the template ones\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\nfrom flask import Blueprint, jsonify\nfrom database import startSession, DummySession, GatewayGroups\nfrom shared import debugEndpoint, StatusCodes, getRequestData, strFieldsToDict\nfrom util.ipc import STATE_SHMEM_NAME, getSharedMemoryDict\nfrom util.networking import getExternalIP\nfrom modules.api.api_functions import showApiError, api_security\nfrom modules.api.carriergroups.functions import addUpdateCarrierGroups, addUpdateCarriers\nimport settings\n\ncarriergroups = Blueprint('carriergroups','__name__')\n\n\n# TODO: standardize response payloads using new createApiResponse()\n#       marked for implementation in v0.74\n\n\n@carriergroups.route('/api/v1/carriergroups',methods=['GET'])\n@carriergroups.route('/api/v1/carriergroups/<string:id>',methods=['GET'])\n@carriergroups.route('/api/v1/carriergroups/<string:id>',methods=['DELETE'])\n@api_security\ndef listCarrierGroups():\n    \"\"\"\n    List all Carrier Groups\\n\n\n    ===============\n    Request Payload\n    ===============\n\n    .. code-block:: json\n\n\n    {}\n\n    ================\n    Response Payload\n    ================\n\n    .. code-block:: json\n\n        {\n            error: <string>,\n            msg: <string>,\n            kamreload: <bool>,\n            data: [\n                carriergroups: [\n                    {\n                    gwgroupid: <int>,\n                    name: <string>,\n                    gwlist: <string>\n                    }\n                ]\n            ]\n        }\n    \"\"\"\n    db = DummySession()\n\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        db = startSession()\n\n        carriergroups = db.query(GatewayGroups).filter(\n            GatewayGroups.description.regexp_match(GatewayGroups.FILTER.CARRIER.value)\n        ).all()\n\n        for carriergroup in carriergroups:\n            # Grap the description field, which is comma seperated key/value pair\n            fields = strFieldsToDict(carriergroup.description)\n\n            # append summary of endpoint group data\n            response_payload['data'].append({\n                'gwgroupid': carriergroup.id,\n                'name': fields['name'],\n                'gwlist': carriergroup.gwlist\n            })\n\n        response_payload['msg'] = 'Endpoint groups found'\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n\n@carriergroups.route('/api/v1/carriergroups/plugin/<string:plugin_name>/config',methods=['GET'])\n@api_security\ndef getPluginMetaData(plugin_name):\n    \"\"\"\n    Will return meta data about the plug\n\n    ===============\n    Request Payload\n    ===============\n\n    .. code-block:: json\n\n\n    {}\n\n    ================\n    Response Payload\n    ================\n\n    .. code-block:: json\n\n        {\n            authfields: [],\n            config <string>,\n            kamreload: <bool>,\n            data: [\n                carriergroups: [\n                    {\n                    gwgroupid: <int>,\n                    name: <string>,\n                    gwlist: <string>\n                    }\n                ]\n            ]\n        }\n    \"\"\"\n    # Import plugin\n    # Returns the Base directory of this file\n    base_dir = os.path.dirname(__file__)\n    try:\n        # Use the Base Dir to specify the location of the plugin required for this domain\n        spec = importlib.util.spec_from_file_location(format(plugin_name), \"{}/plugin/{}/interface.py\".format(base_dir,plugin_name))\n        plugin  = importlib.util.module_from_spec(spec)\n        spec.loader.exec_module(plugin)\n        if plugin:\n            return plugin.getPluginMetaData()\n        else:\n            return None\n\n    except Exception as ex:\n        print(ex)\n\n@carriergroups.route('/api/v1/carriergroups',methods=['PUT'])\n@carriergroups.route('/api/v1/carriergroups/<string:id>',methods=['PUT'])\n@carriergroups.route('/api/v1/carriergroups',methods=['POST'])\n@api_security\ndef addCarrierGroups(id=None):\n    \"\"\"\n    ================\n    Response Payload\n    ================\n\n    .. code-block:: json\n\n        {\n            error: <string>,\n            msg: <string>,\n            kamreload: <bool>,\n            data: [\n                {\n                    gwgroupid: <int>,\n                    endpoints: [\n                        <int>,\n                        ...\n                    ]\n                }\n            ]\n        }\n    \"\"\"\n\n    db = DummySession()\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        db = startSession()\n\n        # defaults.. keep data returned separate from returned metadata\n        response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n        # Dictionary to store request parameters\n        data = {}\n\n        # get request data\n        request_payload = getRequestData()\n        data['name'] = request_payload['name']\n        data['lb_enabled'] = int(request_payload['lb_enabled']) if 'lb_enabled' in request_payload else 0\n        if id == None:\n            data['gwgroupid'] = request_payload['gwgroupid'] if 'gwgroupid' in request_payload else ''\n        else:\n            data['gwgroupid'] = id\n        data['strip'] = request_payload['strip'] if 'strip' in request_payload else ''\n        data['prefix'] = request_payload['prefix'] if 'prefix' in request_payload else ''\n\n        auth  = request_payload['auth'] if 'auth' in request_payload else None\n        if auth:\n            data['authtype'] = auth['type']\n            data['r_username'] = auth['r_username'] if 'r_username' in auth else ''\n            data['auth_username'] = auth['auth_username'] if 'auth_username' in auth else ''\n            data['auth_password'] = auth['auth_password'] if 'auth_password' in auth else ''\n            data['auth_domain'] = auth['auth_domain'] if 'auth_domain' in auth else settings.DEFAULT_AUTH_DOMAIN\n            data['auth_proxy'] = auth['auth_proxy'] if 'auth_proxy' in auth  else ''\n\n        plugin = request_payload['plugin'] if 'plugin' in request_payload else None\n        plugin_made_updates = False\n        if plugin is not None:\n            data['plugin_name'] = plugin['name']\n            data['plugin_prefix'] = plugin['plugin_prefix'] if 'plugin_prefix' in plugin else 'dsip-'\n            data['plugin_account_sid'] = plugin['account_sid'] if 'account_sid' in plugin else ''\n            data['plugin_account_token'] = plugin['account_token'] if 'account_token' in plugin else ''\n\n            if data['plugin_name'] != \"\":\n                # Import Plugin\n                from modules.api.carriergroups.plugin.twilio.interface import init, createTrunk, createIPAccessControlList\n                client = init(data['plugin_account_sid'], data['plugin_account_token'])\n\n                if client:\n                    trunk_name = \"{}{}\".format(data['plugin_prefix'], data['name'])\n                    trunk_sid = createTrunk(client, trunk_name, getExternalIP())\n                    if trunk_sid:\n                        createIPAccessControlList(client, trunk_name, getExternalIP())\n                        plugin_made_updates = True\n\n        endpoints = request_payload['endpoints'] if 'endpoints' in request_payload else []\n\n        # This creates the carrier group only\n        gwgroupid = addUpdateCarrierGroups(data)\n\n        # This creates the Twilio Elastic SIP Entry in the Carrier Group\n        if plugin_made_updates:\n            carrier_data = {}\n            carrier_data['gwgroupid'] = gwgroupid\n            carrier_data['name'] = trunk_name\n            carrier_data['ip_addr'] = \"{}.{}\".format(trunk_name, \"pstn.twilio.com\")\n            carrier_data['strip'] = ''\n            carrier_data['prefix'] = ''\n            addUpdateCarriers(carrier_data)\n\n        # Add endpoints\n        for endpoint in endpoints:\n            #Add the gwgroupdid\n            endpoint['gwgroupid'] = gwgroupid\n            addUpdateCarriers(endpoint)\n\n        gwgroup_data = {}\n        gwgroup_data['gwgroupid'] = gwgroupid\n        response_payload['data'].append(gwgroup_data)\n\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except Exception as ex:\n        db.rollback()\n        db.flush()\n        return showApiError(ex)\n    finally:\n        db.close()\n"
  },
  {
    "path": "gui/modules/api/cron_functions.py",
    "content": "from datetime import datetime\nfrom shared import debugException\nfrom database import startSession, DummySession, Subscribers, dSIPLeases, Gateways\n\ndef cleanupLeases():\n    db = DummySession()\n\n    try:\n        db = startSession()\n\n        Leases = db.query(dSIPLeases).filter(datetime.now() >= dSIPLeases.expiration).all()\n        for Lease in Leases:\n            # Remove the entry in the Subscribers table\n            db.query(Subscribers).filter(Subscribers.id == Lease.sid).delete(synchronize_session=False)\n\n            # Remove the entry in the Gateway table\n            db.query(Gateways).filter(Gateways.gwid == Lease.gwid).delete(synchronize_session=False)\n\n            # Remove the entry in the Lease table\n            db.delete(Lease)\n\n        db.commit()\n\n    except Exception as ex:\n        debugException(ex)\n        db.rollback()\n        db.flush()\n    finally:\n        db.close()\n"
  },
  {
    "path": "gui/modules/api/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# ENABLED=1 --> install, ENABLED=0 --> do nothing, ENABLED=-1 uninstall\nENABLED=1\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction installSQL {\n    # Check to see if the acc table or cdr tables are in use\n    MERGE_DATA=0\n    count=$(withRootDBConn --db=\"$KAM_DB_NAME\" mysql -sN -e \"select count(*) from dsip_endpoint_lease limit 10\" 2> /dev/null)\n    if [ ${count:-0} -gt 0 ]; then\n        MERGE_DATA=1\n    fi\n\n    if [ ${MERGE_DATA} -eq 1 ]; then\n\t    printwarn \"The endpoint lease table (dsip_endpoint_lease) in Kamailio already exists. Merging table data\"\n\t    (\n\t        cat ${DSIP_PROJECT_DIR}/gui/modules/api/api.sql;\n            withRootDBConn --db=\"$KAM_DB_NAME\" mysqldump --single-transaction --skip-triggers --skip-add-drop-table \\\n                --no-create-info --insert-ignore dsip_endpoint_lease\n        ) | withRootDBConn --db=\"$KAM_DB_NAME\" mysql\n    else\n        # Replace the api tables\n        printwarn \"Adding/Replacing the tables needed for API module within dSIPRouter...\"\n        withRootDBConn --db=\"$KAM_DB_NAME\" mysql -sN <${DSIP_PROJECT_DIR}/gui/modules/api/api.sql\n    fi\n}\n\nfunction install {\n    installSQL\n    cronRemove -u dsiprouter 'dsiprouter_cron.py api'\n    cronAppend -u dsiprouter \"*/1 * * * * ${PYTHON_CMD} ${DSIP_PROJECT_DIR}/gui/dsiprouter_cron.py api cleanleases\"\n    cronAppend -u dsiprouter \"0 0 * * * ${PYTHON_CMD} ${DSIP_PROJECT_DIR}/gui/dsiprouter_cron.py api synclicenses\"\n    printdbg \"API module installed\"\n}\n\nfunction uninstall {\n    cronRemove -u dsiprouter 'dsiprouter_cron.py api'\n    printdbg \"API module uninstalled\"\n}\n\nfunction main {\n    if [[ ${ENABLED} -eq 1 ]]; then\n        install && exit 0 || exit 1\n    elif [[ ${ENABLED} -eq -1 ]]; then\n        uninstall && exit 0 || exit 1\n    else\n        exit 0\n    fi\n}\n\nmain\n"
  },
  {
    "path": "gui/modules/api/kamailio/errors.py",
    "content": "class KamailioError(Exception):\n    \"\"\"\n    There was an error communicating with Kamailio\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n\nclass NoDispatcherSets(KamailioError):\n    \"\"\"\n    No dispatcher sets exist but the module is loaded\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)"
  },
  {
    "path": "gui/modules/api/kamailio/functions.py",
    "content": "import requests, sys\nfrom time import sleep, time\nfrom typing import Union\nfrom werkzeug import exceptions as http_exceptions\n\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nfrom shared import IO\nfrom util.security import AES_CTR\nfrom modules.api.kamailio.errors import NoDispatcherSets, KamailioError\nimport settings\n\n\ndef sendJsonRpcCmd(host, method, params=(), timeout=settings.KAM_JSONRPC_TIMEOUT):\n    \"\"\"\n    Send a JSONRPC command to Kamailio\n\n    :param host:      the host to send the request to\n    :type host:       str\n    :param method:    method as parsed by `kamcmd <https://github.com/kamailio/kamailio/tree/master/utils/kamcmd>`_\n    :type method:     str\n    :param params:    parameters for the command\n    :type params:     tuple|list\n    :param timeout:   timeout in seconds for command to finish\n    :type timeout:    int\n    :return:          The result from Kamailio\n    :rtype:           dict\n    :raises requests.exceptions.HTTPError:          if an HTTP error occurred\n    :raises requests.exceptions.JSONDecodeError:    if a JSON parsing error occurred\n    :raises KamailioError:                          if communicating with Kamailio failed\n    :raises Exception:                              for any other error\n    \"\"\"\n\n    if settings.DEBUG:\n        IO.printdbg(f'sending jsonrpc command to {host}: {method} {str(params)}')\n\n    headers = {\n        \"Accept\": \"application/json\",\n        \"Content-Type\": \"application/json\"\n    }\n    payload = {\n        \"method\": method,\n        \"jsonrpc\": settings.KAM_JSONRPC_VERSION,\n        \"id\": settings.KAM_JSONRPC_ID\n    }\n    if len(params) > 0:\n        payload['params'] = params\n\n    def sendit(host):\n        return requests.post(\n            f'http://{host}:5060{settings.KAM_JSONRPC_ROOTPATH}',\n            headers=headers,\n            json=payload,\n            timeout=timeout,\n        )\n\n    r: Union[requests.Response, None] = None\n    cutoff_time = time() + timeout\n    while time() < cutoff_time:\n        try:\n            r = sendit(host)\n            r.raise_for_status()\n            break\n        except requests.exceptions.HTTPError as ex:\n            if ex.response.status_code == 500 and ex.response.json()['error']['message'] == 'ongoing reload':\n                sleep(settings.KAM_JSONRPC_RETRYIVAL)\n                continue\n            raise\n\n    data = r.json()\n    if \"error\" in data:\n        # specific cases\n        if data[\"error\"] == \"No Destination Sets\":\n            raise NoDispatcherSets(data[\"error\"])\n        # general case\n        raise KamailioError(data[\"error\"])\n\n    return data['result']\n\n\ndef reloadKamailio():\n    try:\n        # format some settings for kam config\n        dsip_api_url = settings.DSIP_API_PROTO + '://' + '127.0.0.1' + ':' + str(settings.DSIP_API_PORT)\n        if isinstance(settings.DSIP_API_TOKEN, bytes):\n            dsip_api_token = AES_CTR.decrypt(settings.DSIP_API_TOKEN)\n        else:\n            dsip_api_token = settings.DSIP_API_TOKEN\n\n        # data that is always reloaded, part of core dsiprouter\n        rpc_args = [\n            ('127.0.0.1', 'cfg.sets', ['server', 'role', settings.ROLE]),\n            ('127.0.0.1', 'cfg.sets', ['server', 'api_server', dsip_api_url]),\n            ('127.0.0.1', 'cfg.sets', ['server', 'api_token', dsip_api_token]),\n            ('127.0.0.1', 'htable.reload', ['maintmode']),\n            ('127.0.0.1', 'htable.reload', ['gw2gwgroup']),\n            ('127.0.0.1', 'htable.reload', ['gwgroup2lb']),\n            ('127.0.0.1', 'htable.reload', ['inbound_hardfwd']),\n            ('127.0.0.1', 'htable.reload', ['inbound_failfwd']),\n            ('127.0.0.1', 'htable.reload', ['prefix_to_route']),\n        ]\n\n        # reload data depending on the features enabled\n        features_enabled = {\n            x['name'] for x in sendJsonRpcCmd('127.0.0.1', 'core.ppdefines_full') \\\n            if x['value'] == 'none'\n        }\n        if 'WITH_AUTH' in features_enabled and 'WITH_IPAUTH' in features_enabled:\n            rpc_args.append(('127.0.0.1', 'permissions.addressReload'))\n        if 'WITH_UAC' in features_enabled:\n            rpc_args.append(('127.0.0.1', 'uac.reg_reload'))\n        if 'WITH_DROUTE' in features_enabled:\n            rpc_args.append(('127.0.0.1', 'drouting.reload'))\n        if 'WITH_DISPATCHER' in features_enabled:\n            rpc_args.append(('127.0.0.1', 'dispatcher.reload'))\n            rpc_args.append(('127.0.0.1', 'keepalive.flush'))\n        if 'WITH_CALL_SETTINGS' in features_enabled:\n            rpc_args.append(('127.0.0.1', 'htable.reload', ['call_settings']))\n        if 'WITH_MULTIDOMAIN' in features_enabled:\n            rpc_args.append(('127.0.0.1', 'domain.reload'))\n        if 'WITH_TELEBLOCK' in features_enabled:\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['teleblock', 'gw_enabled', str(settings.TELEBLOCK_GW_ENABLED)]))\n        if 'WITH_LCR' in features_enabled:\n            rpc_args.append(('127.0.0.1', 'htable.reload', ['tofromprefix']))\n        #if 'WITH_TLS' in features_enabled:\n        #    # TODO: tls.reload is VERY slow on some systems. Commented out until we get a resolution\n        #    rpc_args.append(('127.0.0.1', 'tls.reload', [], 20))\n        if 'WITH_WEBSOCKETS' in features_enabled:\n            rpc_args.append(('127.0.0.1', 'ws.enable'))\n        if 'WITH_DNID_LNP_ENRICHMENT' in features_enabled:\n            rpc_args.append(('127.0.0.1', 'htable.reload', ['enrichdnid_lnpmap']))\n        if 'WITH_RTPENGINE' in features_enabled:\n            rpc_args.append(('127.0.0.1', 'rtpengine.enable', ['all', 1]))\n        if 'WITH_TRANSNEXUS' in features_enabled:\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['transnexus', 'authservice_enabled', str(settings.TRANSNEXUS_AUTHSERVICE_ENABLED)]))\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['transnexus', 'authservice_host', str(settings.TRANSNEXUS_AUTHSERVICE_HOST)]))\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['transnexus', 'verifyservice_enabled', str(settings.TRANSNEXUS_VERIFYSERVICE_ENABLED)]))\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['transnexus', 'verifyservice_host', str(settings.TRANSNEXUS_VERIFYSERVICE_HOST)]))\n        if 'WITH_STIRSHAKEN' in features_enabled:\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['stir_shaken', 'stir_shaken_enabled', str(settings.STIR_SHAKEN_ENABLED)]))\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['stir_shaken', 'stir_shaken_prefix_a', str(settings.STIR_SHAKEN_PREFIX_A)]))\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['stir_shaken', 'stir_shaken_prefix_b', str(settings.STIR_SHAKEN_PREFIX_B)]))\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['stir_shaken', 'stir_shaken_prefix_c', str(settings.STIR_SHAKEN_PREFIX_C)]))\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['stir_shaken', 'stir_shaken_prefix_invalid', str(settings.STIR_SHAKEN_PREFIX_INVALID)]))\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['stir_shaken', 'stir_shaken_block_invalid', str(settings.STIR_SHAKEN_BLOCK_INVALID)]))\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['stir_shaken', 'stir_shaken_key_path', str(settings.STIR_SHAKEN_KEY_PATH)]))\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['stir_shaken', 'stir_shaken_cert_url', str(settings.STIR_SHAKEN_CERT_URL)]))\n\n        # data that is conditionally reloaded based on dsiprouter settings\n        if settings.TELEBLOCK_GW_ENABLED:\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['teleblock', 'gw_ip', str(settings.TELEBLOCK_GW_IP)]))\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['teleblock', 'gw_port', str(settings.TELEBLOCK_GW_PORT)]))\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['teleblock', 'media_ip', str(settings.TELEBLOCK_MEDIA_IP)]))\n            rpc_args.append(('127.0.0.1', 'cfg.sets', ['teleblock', 'media_port', str(settings.TELEBLOCK_MEDIA_PORT)]))\n\n        # send off all the jsonrpc requests and handle any failures if possible\n        for cmdset in rpc_args:\n            try:\n                sendJsonRpcCmd(*cmdset)\n            except NoDispatcherSets:\n                pass\n            except KamailioError as ex:\n                err = http_exceptions.HTTPException(str(ex))\n                err.code = 500\n                raise err\n            except requests.exceptions.HTTPError as ex:\n                err = http_exceptions.HTTPException(response=ex.response)\n                raise err\n\n        IO.printinfo(\"[---- Reloaded Kamailio with dSIPRouter Settings ----]\")\n    except Exception as ex:\n        IO.printerr(\"[---- Could not reload Kamailio with dSIPRouter Settings ----]\")\n        raise ex\n"
  },
  {
    "path": "gui/modules/api/licensemanager/classes.py",
    "content": "import sys\n\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nimport requests, secrets, datetime, functools\nfrom util.security import Credentials, AES_CTR\n\n\ndef wcApiPropagateHttpError(method):\n    @functools.wraps(method)\n    def wrapper(self, *args, **kwargs):\n        try:\n            return method(self, *args, **kwargs)\n        except requests.HTTPError as ex:\n            ex.response._content = ex.response.content.replace(self._license_key.encode('utf-8'), b'<redacted>')\n            raise WoocommerceError(response=ex.response)\n\n    return wrapper\n\nclass WoocommerceError(requests.HTTPError):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n    def __str__(self):\n        r_json = self.response.json()\n        return '\\n'.join(' '.join(r_json['data']['errors'][k]) for k in r_json['data']['errors'])\n\nclass WoocommerceLicense(object):\n    # this API key is locked down to only allow functionality below\n    SERVER = {\n        \"baseurl\": \"https://dopensource.com/wp-json/lmfwc/v2/licenses/\",\n        \"key\": \"ck_068f510a518ff5ecf1cbdcbc7db7f9bac2331613\",\n        \"secret\": \"cs_5ae2f3decfa59f427a59b41f2e41459d18023dd7\",\n    }\n    # constant salt used for equality checks\n    # should not be used for generating a hash that is stored\n    CMP_SALT = 'A' * Credentials.SALT_LEN\n    # the woocommerce license length may change due to application specific requirements\n    # therefore we parse it out based on the length of the machine id, which will not change\n    MACHINE_ID_LEN = 32\n\n    def __init__(self, license_key=None, key_combo=None, decrypt=False):\n        # set attributes based on clientside data\n        if license_key is not None:\n            if key_combo is not None:\n                raise ValueError('license_key and key_combo are mutually exclusive')\n\n            if isinstance(license_key, str):\n                if len(license_key) == 0:\n                    raise ValueError('license_key must not be empty')\n                if decrypt:\n                    license_key = AES_CTR.decrypt(license_key)\n            elif isinstance(license_key, bytes):\n                if len(license_key) == 0:\n                    raise ValueError('license_key must not be empty')\n                if decrypt:\n                    license_key = AES_CTR.decrypt(license_key)\n                else:\n                    license_key = license_key.decode('utf-8')\n            else:\n                raise ValueError('license_key must be a string or byte string')\n            self._license_key = license_key\n\n            with open('/etc/machine-id', 'r') as f:\n                server_machine_id = f.read(WoocommerceLicense.MACHINE_ID_LEN)\n                if len(server_machine_id) != WoocommerceLicense.MACHINE_ID_LEN:\n                    raise Exception(\"the server machine-id is invalid\")\n                self.__machine_id = server_machine_id\n                self.__machine_match = True\n        elif key_combo is not None:\n            if isinstance(key_combo, str):\n                if len(key_combo) == 0:\n                    raise ValueError('key_combo must not be empty')\n                if decrypt:\n                    key_combo = AES_CTR.decrypt(key_combo.encode('utf-8'))\n            elif isinstance(key_combo, bytes):\n                if len(key_combo) == 0:\n                    raise ValueError('key_combo must not be empty')\n                if decrypt:\n                    key_combo = AES_CTR.decrypt(key_combo)\n                else:\n                    key_combo = key_combo.decode('utf-8')\n            else:\n                raise ValueError('key_combo must be a string or byte string')\n\n            try:\n                self._license_key = key_combo[:-WoocommerceLicense.MACHINE_ID_LEN]\n                self.__machine_id = key_combo[-WoocommerceLicense.MACHINE_ID_LEN:]\n            except IndexError:\n                raise ValueError('key_combo is invalid')\n\n            with open('/etc/machine-id', 'r') as f:\n                server_machine_id = f.read(WoocommerceLicense.MACHINE_ID_LEN)\n                if len(server_machine_id) != WoocommerceLicense.MACHINE_ID_LEN:\n                    raise Exception(\"the server machine-id is invalid\")\n                self.__machine_match = self.__machine_id == server_machine_id\n        else:\n            raise ValueError('one of license_key or key_combo is required')\n\n        # set attributes based on serverside data\n        # noinspection PyArgumentList\n        self.sync()\n\n    def __eq__(self, obj):\n        return isinstance(obj, WoocommerceLicense) and \\\n               secrets.compare_digest(self.hash(WoocommerceLicense.CMP_SALT), obj.hash(WoocommerceLicense.CMP_SALT))\n\n    def __ne__(self, obj):\n        return not self.__eq__(obj)\n\n    # allow passing to dict() and iterable()\n    def __iter__(self):\n        for k, v in self.asDict().items():\n            yield k, v\n\n    # only return select attributes in the iterable/dict representation\n    def asDict(self):\n        return {\n            'id': self.id,\n            'tags': self.tags,\n            'expires': self.expires,\n            'active': self.active,\n            'valid': self.valid,\n            'license_key': self.license_key,\n        }\n\n    @property\n    def machine_match(self):\n        return self.__machine_match\n\n    @property\n    def tags(self):\n        return self._tags\n\n    @property\n    def expires(self):\n        if self._expires_at is not None:\n            return datetime.datetime.strptime(self._expires_at, '%Y-%m-%d %H:%M:%S')\n        if self._valid_for is not None:\n            return datetime.datetime.strptime(self._created_at, '%Y-%m-%d %H:%M:%S') + datetime.timedelta(days=self._valid_for)\n        # no expires set, it is valid forever\n        return datetime.datetime.max\n\n    @property\n    def active(self):\n        return (self._status == 3 or self._status == 2) and self._times_activated > 0 and self.expires > datetime.datetime.now()\n\n    @property\n    def license_key(self):\n        return self._license_key\n\n    @property\n    def valid(self):\n        return self.active and self.machine_match\n\n    @property\n    def id(self):\n        return self._id\n\n    @wcApiPropagateHttpError\n    def sync(self):\n        res = requests.get(\n            WoocommerceLicense.SERVER['baseurl'] + self._license_key,\n            auth=(\n                WoocommerceLicense.SERVER['key'],\n                WoocommerceLicense.SERVER['secret']\n            )\n        )\n        res.raise_for_status()\n        r_json = res.json()\n\n        if 'errors' in r_json['data']:\n            raise requests.HTTPError(response=res)\n\n        self._id = r_json['data'][\"id\"]\n        # self._order_id = r_json['data'][\"orderId\"]\n        # self._user_id = r_json['data'][\"userId\"]\n        self._expires_at = r_json['data'][\"expiresAt\"]\n        self._valid_for = r_json['data'][\"validFor\"] if r_json['data'][\"validFor\"] != 0 else None\n        # self._source = r_json['data'][\"source\"]\n        self._status = r_json['data'][\"status\"]\n        self._times_activated = r_json['data'][\"timesActivated\"]\n        self._times_activated_max = r_json['data'][\"timesActivatedMax\"]\n        self._created_at = r_json['data'][\"createdAt\"]\n        # self._created_by = r_json['data'][\"createdBy\"]\n        self._updated_at = r_json['data'][\"updatedAt\"]\n        # self._updated_by = r_json['data'][\"updatedBy\"]\n        self._tags = r_json['data']['tags']\n\n    @wcApiPropagateHttpError\n    def activate(self):\n        res = requests.get(\n            WoocommerceLicense.SERVER['baseurl'] + 'activate/' + self._license_key,\n            auth=(\n                WoocommerceLicense.SERVER['key'],\n                WoocommerceLicense.SERVER['secret']\n            )\n        )\n        res.raise_for_status()\n        r_json = res.json()\n\n        if 'errors' in r_json['data']:\n            raise requests.HTTPError(response=res)\n\n        self._times_activated = r_json['data']['timesActivated']\n        self._times_activated_max = r_json['data']['timesActivatedMax']\n        self._updated_at = r_json['data']['updatedAt']\n\n        return r_json.get('success', False)\n\n    @wcApiPropagateHttpError\n    def deactivate(self):\n        res = requests.get(\n            WoocommerceLicense.SERVER['baseurl'] + 'deactivate/' + self._license_key,\n            auth=(\n                WoocommerceLicense.SERVER['key'],\n                WoocommerceLicense.SERVER['secret']\n            )\n        )\n        res.raise_for_status()\n        r_json = res.json()\n\n        if 'errors' in r_json['data']:\n            raise requests.HTTPError(response=res)\n\n        self._times_activated = r_json['data']['timesActivated']\n        self._times_activated_max = r_json['data']['timesActivatedMax']\n        self._updated_at = r_json['data']['updatedAt']\n\n        return r_json.get('success', False)\n\n    def hash(self, salt=None):\n        return Credentials.hashCreds('{}{}'.format(self._license_key, self.__machine_id), salt)\n\n    # need original key for queries to woocommerce\n    # format: <license_key><machine_id>\n    def encrypt(self):\n        return AES_CTR.encrypt('{}{}'.format(self._license_key, self.__machine_id))"
  },
  {
    "path": "gui/modules/api/licensemanager/cli.py",
    "content": "# TODO: this is an ugly implementation but better than inline python in the shell script\n#       marked for refactoring in v0.80\n\nimport csv, os, sys\nDSIP_PROJECT_DIR = os.environ.get('DSIP_PROJECT_DIR', '/opt/dsiprouter')\nDSIP_SYSTEM_CONFIG_DIR = os.environ.get('DSIP_SYSTEM_CONFIG_DIR', '/etc/dsiprouter')\nsys.path = [\n    f'{DSIP_SYSTEM_CONFIG_DIR}/gui',\n    f'{DSIP_PROJECT_DIR}/gui',\n    f'{DSIP_PROJECT_DIR}/gui/modules/api/licensemanager'\n] + sys.path[1:]\nif __name__ == '__main__':\n    __package__ = 'modules.api.licensemanager'\nimport settings\nfrom shared import IO\nfrom .classes import WoocommerceLicense\nfrom .functions import getLicenseStatus, licenseDictToStateDict, searchLicenses, addToLicenseStore, removeFromLicenseStore\n\ndef main():\n    args = sys.argv[1:]\n\n    if len(args) == 0:\n        sys.exit(unknownCmd())\n\n    sub_cmd = args.pop(0)\n\n    if sub_cmd == '-retrieve':\n        sys.exit(cmdRetrieve(args))\n    if sub_cmd == '-list':\n        sys.exit(cmdList(args))\n    if sub_cmd == '-activate':\n        sys.exit(cmdActivate(args))\n    if sub_cmd == '-import':\n        sys.exit(cmdImport(args))\n    if sub_cmd == '-deactivate':\n        sys.exit(cmdDeactivate(args))\n    if sub_cmd == '-clear':\n        sys.exit(cmdClear(args))\n    if sub_cmd == '-check':\n        sys.exit(cmdCheck(args))\n    sys.exit(unknownCmd())\n\ndef cmdRetrieve(args):\n    if len(args) != 1:\n        sys.exit(unknownCmd())\n    if args[0][:4] == 'tag=':\n        key = None\n        tag = args[0][4:]\n    else:\n        key = args[0]\n        tag = None\n\n    if isDsiprouterRunning():\n        result = searchLicenses(license_key=key, license_tag=tag)\n    else:\n        result = searchLicenses(\n            license_key=key,\n            license_tag=tag,\n            state_dict=licenseDictToStateDict(settings.DSIP_LICENSE_STORE)\n        )\n\n    if len(result) == 0:\n        IO.printwarn('license(s) does not exist on this system')\n        return 1\n\n    output_data = []\n    for license in result:\n        data = dict(license)\n        data['tags'] = ','.join(data['tags'])\n        output_data.append(data)\n    fields = ['id', 'tags', 'expires', 'active', 'valid', 'license_key']\n    tsv = csv.DictWriter(sys.stdout, delimiter='\\t', quoting=csv.QUOTE_NONE, fieldnames=fields)\n    tsv.writeheader()\n    for row in output_data:\n        tsv.writerow(row)\n\n    return 0\n\ndef cmdList(args):\n    if len(args) != 0:\n        sys.exit(unknownCmd())\n\n    if isDsiprouterRunning():\n        result = searchLicenses()\n    else:\n        result = searchLicenses(state_dict=licenseDictToStateDict(settings.DSIP_LICENSE_STORE))\n\n    output_data = []\n    for license in result:\n        data = dict(license)\n        data['tags'] = ','.join(data['tags'])\n        output_data.append(data)\n    fields = ['id', 'tags', 'expires', 'active', 'valid', 'license_key']\n    tsv = csv.DictWriter(sys.stdout, delimiter='\\t', quoting=csv.QUOTE_NONE, fieldnames=fields)\n    tsv.writeheader()\n    for row in output_data:\n        tsv.writerow(row)\n\n    return 0\n\ndef cmdActivate(args):\n    if len(args) != 1:\n        sys.exit(unknownCmd())\n    key = args[0]\n\n    if isDsiprouterRunning():\n        state_dict = None\n        matches = searchLicenses(license_key=key)\n    else:\n        state_dict = licenseDictToStateDict(settings.DSIP_LICENSE_STORE)\n        matches = searchLicenses(license_key=key, state_dict=state_dict)\n    if len(matches) == 0:\n        license = WoocommerceLicense(license_key=key)\n    else:\n        license = matches[0]\n\n    license.activate()\n    try:\n        addToLicenseStore(license, state_dict=state_dict)\n    except:\n        license.deactivate()\n        raise\n\n    IO.printinfo('license successfully activated')\n    return 0\n\ndef cmdImport(args):\n    if len(args) != 1:\n        sys.exit(unknownCmd())\n\n    dsip_runnning = isDsiprouterRunning()\n    if dsip_runnning:\n        state_dict = None\n    else:\n        state_dict = licenseDictToStateDict(settings.DSIP_LICENSE_STORE)\n\n    with open(args[0], 'r') as fp:\n        license_keys = [line.strip() for line in fp]\n\n    for license_key in license_keys:\n        if dsip_runnning:\n            matches = searchLicenses(license_key=license_key)\n        else:\n            matches = searchLicenses(license_key=license_key, state_dict=state_dict)\n        if len(matches) == 0:\n            license = WoocommerceLicense(license_key=license_key)\n        else:\n            license = matches[0]\n\n        license.activate()\n        try:\n            addToLicenseStore(license, state_dict)\n        except:\n            license.deactivate()\n            raise\n\n    IO.printinfo('license(s) successfully activated')\n    return 0\n\ndef cmdDeactivate(args):\n    if len(args) != 1:\n        sys.exit(unknownCmd())\n    if args[0][:4] == 'tag=':\n        key = None\n        tag = args[0][4:]\n    else:\n        key = args[0]\n        tag = None\n\n    dsip_runnning = isDsiprouterRunning()\n    if dsip_runnning:\n        state_dict = None\n    else:\n        state_dict = licenseDictToStateDict(settings.DSIP_LICENSE_STORE)\n\n    if dsip_runnning:\n        result = searchLicenses(license_key=key, license_tag=tag)\n    else:\n        result = searchLicenses(license_key=key, license_tag=tag, state_dict=state_dict)\n\n    if len(result) == 0:\n        IO.printwarn('license(s) does not exist on this system')\n        return 1\n\n    for license in result:\n        license.deactivate()\n        try:\n            removeFromLicenseStore(license, state_dict=state_dict)\n        except:\n            license.activate()\n            raise\n\n    IO.printinfo('license(s) successfully deactivated')\n    return 0\n\ndef cmdClear(args):\n    if len(args) != 0:\n        sys.exit(unknownCmd())\n\n    dsip_runnning = isDsiprouterRunning()\n    if dsip_runnning:\n        state_dict = None\n    else:\n        state_dict = licenseDictToStateDict(settings.DSIP_LICENSE_STORE)\n\n    if dsip_runnning:\n        result = searchLicenses()\n    else:\n        result = searchLicenses(state_dict=state_dict)\n\n    if len(result) == 0:\n        IO.printwarn('no licenses exist on this system')\n        return 0\n\n    for license in result:\n        license.deactivate()\n        try:\n            removeFromLicenseStore(license, state_dict=state_dict)\n        except:\n            license.activate()\n            raise\n\n    IO.printinfo('all system licenses successfully deactivated')\n    return 0\n\ndef cmdCheck(args):\n    if len(args) != 1:\n        sys.exit(unknownCmd())\n    if args[0][:4] == 'tag=':\n        key = None\n        tag = args[0][4:]\n    else:\n        key = args[0]\n        tag = None\n\n    if isDsiprouterRunning():\n        status = getLicenseStatus(license_key=key, license_tag=tag)\n    else:\n        state_dict = licenseDictToStateDict(settings.DSIP_LICENSE_STORE)\n        status = getLicenseStatus(license_key=key, license_tag=tag, state_dict=state_dict)\n\n    if status == 0:\n        IO.printwarn('license does not exist on this system')\n        return 1\n    if status == 1:\n        IO.printwarn('license is present but not active')\n        return 1\n    if status == 2:\n        IO.printwarn('license is present but associated with another machine')\n        return 1\n    if status == 3:\n        IO.printinfo('license is present and active')\n        return 0\n    IO.printerr('unable to determine status of license')\n    return 1\n\ndef unknownCmd():\n    IO.printerr(f'{sys.argv[0]}: Invalid arguments provided')\n    return 1\n\ndef isDsiprouterRunning():\n    return os.waitstatus_to_exitcode(os.system('systemctl is-active -q dsiprouter')) == 0\n\ndef handleFatalException(ex_type, ex_value, ex_tb):\n    IO.printerr('A fatal error occurred')\n    IO.printerr(str(ex_value))\n    sys.exit(1)\n\nif __name__ == '__main__':\n    if getattr(sys, 'gettrace', lambda: None)() is None:\n        sys.excepthook = handleFatalException\n    main()\n"
  },
  {
    "path": "gui/modules/api/licensemanager/functions.py",
    "content": "import sys\n\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nfrom database import updateDsipSettingsTable\nfrom shared import updateConfig\nfrom util.ipc import STATE_SHMEM_NAME, getSharedMemoryDict\nfrom util.pyasync import mpexec\nfrom modules.api.licensemanager.classes import WoocommerceLicense, AES_CTR\nimport settings\n\n\n# must be defined here or mpexec will fail to copy the func to child procs\ndef __getLicense(license):\n    return WoocommerceLicense(key_combo=license, decrypt=True)\n\ndef quickLoadLicenses(keystore):\n    # task for parallel processing (will send external requests on instantiation)\n\n    return mpexec(__getLicense, ([x] for x in keystore.values()))\n\ndef licenseDictToStateDict(keystore):\n    \"\"\"\n    Transform the license store into a lookup dict based on key\n\n    :param keystore:    the key store\n    :type keystore:     dict\n    :return:            keys formatted as a status lookup dict\n    :rtype:             dict\n    \"\"\"\n\n    return {lc.license_key: lc for lc in quickLoadLicenses(keystore)}\n\ndef syncLicensesToGlobalState():\n    getSharedMemoryDict(STATE_SHMEM_NAME)['dsip_license_store'] = licenseDictToStateDict(settings.DSIP_LICENSE_STORE)\n\ndef getLicenseStatusFromStateDict(license_state, tag):\n    \"\"\"\n    Get license status given the license state dict and a license tag\n\n    :param license_state:   the license key store in global state\n    :type license_state:    dict\n    :param tag:             license tag to lookup\n    :type tag:              str\n    :return:                status of first license with that tag (valid licenses have priority)\n    :rtype:                 int\n\n    the license status corresponds to::\n        -1  ==  an unknown error occurred looking up the status\n        0   ==  no license present\n        1   ==  license present but not active\n        2   ==  license present but associated with another machine\n        3   ==  license present and valid\n    \"\"\"\n\n    res = [lc for lc in license_state.values() if tag in lc.tags]\n    if len(res) == 0:\n        return 0\n    lic = next((lc for lc in res if lc.valid), None)\n    if lic is not None:\n        return 3\n    lic = res[0]\n    if not lic.machine_match:\n        return 2\n    if not lic.active:\n        return 1\n    return -1\n\ndef searchLicenses(license_key=None, key_combo=None, decrypt=False, license_tag=None, state_dict=None):\n    \"\"\"\n    Search licenses from the global state\n\n    :param license_key:     license key to lookup\n    :type license_key:      str|None\n    :param key_combo:       license key/machine ID to lookup\n    :type key_combo:        str|None\n    :param decrypt:         whether the key or key_combo provided needs decrypted\n    :type decrypt:          bool\n    :param license_tag:     license tag to lookup\n    :type license_tag:      str|None\n    :param state_dict:      the state dict to load the license from\n    :type state_dict:       dict|None\n    :return:                matching licenses\n    :rtype:                 list[WoocommerceLicense]\n    \"\"\"\n\n    if state_dict is None:\n        state_dict = getSharedMemoryDict(STATE_SHMEM_NAME)['dsip_license_store']\n\n    if license_key is not None:\n        if key_combo is not None:\n            raise ValueError('license_key and key_combo are mutually exclusive')\n\n        if isinstance(license_key, str):\n            if len(license_key) == 0:\n                raise ValueError('license_key must not be empty')\n            if decrypt:\n                license_key = AES_CTR.decrypt(license_key)\n        elif isinstance(license_key, bytes):\n            if len(license_key) == 0:\n                raise ValueError('license_key must not be empty')\n            if decrypt:\n                license_key = AES_CTR.decrypt(license_key)\n            else:\n                license_key = license_key.decode('utf-8')\n        else:\n            raise ValueError('license_key must be a string or byte string')\n\n        try:\n            return [state_dict[license_key]]\n        except KeyError:\n            return []\n\n    if key_combo is not None:\n        if isinstance(key_combo, str):\n            if len(key_combo) == 0:\n                raise ValueError('key_combo must not be empty')\n            if decrypt:\n                key_combo = AES_CTR.decrypt(key_combo.encode('utf-8'))\n        elif isinstance(key_combo, bytes):\n            if len(key_combo) == 0:\n                raise ValueError('key_combo must not be empty')\n            if decrypt:\n                key_combo = AES_CTR.decrypt(key_combo)\n            else:\n                key_combo = key_combo.decode('utf-8')\n        else:\n            raise ValueError('key_combo must be a string or byte string')\n\n        try:\n            license_key = key_combo[:-WoocommerceLicense.MACHINE_ID_LEN]\n        except IndexError:\n            raise ValueError('key_combo is invalid')\n\n        try:\n            return [state_dict[license_key]]\n        except KeyError:\n            return []\n\n    if license_tag is not None:\n        return [\n            lc for lc in state_dict.values() if license_tag in lc.tags\n        ]\n\n    return list(state_dict.values())\n\n\ndef getLicenseStatus(license_key=None, license_tag=None, state_dict=None):\n    \"\"\"\n    Get license status directly or filtered by license tag\n\n    :param tag: license tag to lookup\n    :type tag:  str\n    :return:    status of first license with that tag (valid licenses have priority)\n    :rtype:     int\n\n\n    :param license_key:     license key to lookup\n    :type license_key:      str|None\n    :param license_tag:     license tag to lookup\n    :type license_tag:      str|None\n    :param state_dict:      the state dict to load the license from\n    :type state_dict:       dict|None\n    :return:            status of first license with that tag (valid licenses have priority)\n    :rtype:             int\n\n    the license status corresponds to::\n\n        0 == no license present\n        1 == license present but not valid\n        2 == license present but associated with another machine\n        3 == license present and valid\n    \"\"\"\n    res = searchLicenses(license_key=license_key, license_tag=license_tag, state_dict=state_dict)\n    if len(res) == 0:\n        return 0\n    lic = next((lc for lc in res if lc.valid), None)\n    if lic is not None:\n        return 3\n    lic = res[0]\n    if not lic.machine_match:\n        return 2\n    if not lic.active:\n        return 1\n    raise Exception('could not determine status of license')\n\ndef addToLicenseStore(license, state_dict=None):\n    if state_dict is None:\n        state_dict = getSharedMemoryDict(STATE_SHMEM_NAME)['dsip_license_store']\n    license_store = settings.DSIP_LICENSE_STORE\n\n    license_store[str(license.id)] = license.encrypt()\n    state_dict[license.license_key] = license\n    if settings.LOAD_SETTINGS_FROM == 'db':\n        updateDsipSettingsTable({'DSIP_LICENSE_STORE': license_store})\n    updateConfig(settings, {'DSIP_LICENSE_STORE': license_store}, hot_reload=True)\n\ndef removeFromLicenseStore(license, state_dict=None):\n    if state_dict is None:\n        state_dict = getSharedMemoryDict(STATE_SHMEM_NAME)['dsip_license_store']\n    license_store = settings.DSIP_LICENSE_STORE\n\n    license_store.pop(str(license.id), None)\n    state_dict.pop(license.license_key, None)\n    if settings.LOAD_SETTINGS_FROM == 'db':\n        updateDsipSettingsTable({'DSIP_LICENSE_STORE': license_store})\n    updateConfig(settings, {'DSIP_LICENSE_STORE': license_store}, hot_reload=True)\n"
  },
  {
    "path": "gui/modules/api/licensemanager/routes.py",
    "content": "import sys\n\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nfrom flask import Blueprint, jsonify, request\nfrom werkzeug import exceptions as http_exceptions\nfrom shared import debugEndpoint, debugException, StatusCodes, getRequestData, updateConfig\nfrom modules.api.api_functions import showApiError, api_security\nfrom modules.api.licensemanager.classes import WoocommerceLicense, WoocommerceError\nfrom modules.api.licensemanager.functions import searchLicenses, addToLicenseStore, removeFromLicenseStore\nfrom util.ipc import STATE_SHMEM_NAME, getSharedMemoryDict\nimport settings\n\nlicense_manager = Blueprint('licensing', '__name__')\n\n#=================================================================================\n# api standard response format\n#=================================================================================\n# {\n#     \"error\": str,\n#     \"msg\": str,\n#     \"kamreload\": bool,\n#     \"data\": list\n# }\n#=================================================================================\n# error:      error if one occurred (db|http|server|other) or empty string\n# msg:        response message (human readable)\n# kamreload:  whether kamailio settings need reloaded or not\n# data:       data returned, if any\n#=================================================================================\n# TODO: standardize response payloads using new createApiResponse()\n#       marked for implementation in v0.76\n#=================================================================================\n\ndef showWoocommerceError(ex):\n    debugException(ex)\n    payload = {\n        'error': 'woocommerce',\n        'msg': str(ex),\n        'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'],\n        'data': []\n    }\n    return jsonify(payload), ex.response.status_code\n\n\ndef validateRequestArgs(data, allowed_args=dict(), required_args=set(), strict_mode=False, extra_checks=dict()):\n    # whitelist of extra params allowed if strict mode is disabled\n    # typically used with clientside-app-specific args that we ignore in our serverside code\n    # for example; the '_' request arg is used by jquery for enabling/disabling caching\n    if not strict_mode:\n        ignored_args = {'_'}\n    else:\n        ignored_args = set()\n\n    # validate required args (by key)\n    for arg in required_args:\n        if arg not in data.keys():\n            raise http_exceptions.BadRequest(\"Missing required request argument: {}\".format(arg))\n\n    # validate whitelisted args (key -> value dict)\n    for k, v in data.items():\n        # is the arg provided in the ignore list\n        if k in ignored_args:\n            continue\n        # is the arg provided in the whitelist\n        if k not in allowed_args.keys():\n            raise http_exceptions.BadRequest(\"Invalid request argument provided: {}\".format(k))\n        # is the arg provided the right type\n        if not type(v) == allowed_args[k]:\n            # in strict mode we do not attempt casting\n            if strict_mode:\n                raise http_exceptions.BadRequest(\"Invalid request argument '{}' wrong type\".format(k))\n            # try casting to correct type in case the query arg was parsed as the wrong type\n            # this typically only happens with query args in the url (they are usually set to str by Flask)\n            try:\n                data[k] = allowed_args[k](v)\n            except ValueError:\n                raise http_exceptions.BadRequest(\"Invalid request argument '{}' wrong type\".format(k))\n\n        # does this argument have extra constraints to follow?\n        try:\n            if not extra_checks[k](v):\n                raise http_exceptions.BadRequest(\"Invalid request argument '{}' failed constraint checks\".format(k))\n        except KeyError:\n            pass\n\n\n@license_manager.route('/api/v1/licensing/validate', methods=['GET'])\n@api_security\ndef validateLicense():\n    \"\"\"\n    Validate a License\n\n    =================\n    Request Arguments\n    =================\n\n    .. code-block:: text\n\n        license_key=<string>\n        key_encrypted=<bool>\n\n    ================\n    Response Payload\n    ================\n\n    .. code-block:: json\n\n        {\n            error: <string>,\n            msg: <string>,\n            kamreload: <bool>,\n            data: [\n                <bool>\n            ]\n        }\n    \"\"\"\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        data = request.args.to_dict()\n\n        # sanity checks (whitelist of acceptable args in the request body)\n        validateRequestArgs(data, allowed_args={'license_key': str, 'key_encrypted': bool}, required_args={'license_key'})\n\n        # retrieve the license\n        if data.get('key_encrypted', False):\n            matches = searchLicenses(key_combo=data['license_key'], decrypt=True)\n        else:\n            matches = searchLicenses(data['license_key'])\n\n        if len(matches) == 0:\n            response_payload['data'].append(False)\n            response_payload['msg'] = 'license does not exist'\n            return jsonify(response_payload), StatusCodes.HTTP_NOT_FOUND\n\n        lc = matches[0]\n        if not lc.active:\n            response_payload['data'].append(False)\n            response_payload['msg'] = 'license has not been activated'\n            return jsonify(response_payload), StatusCodes.HTTP_OK\n\n        if not lc.machine_match:\n            response_payload['data'].append(False)\n            response_payload['msg'] = 'license is not registered to this node'\n            return jsonify(response_payload), StatusCodes.HTTP_OK\n\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except WoocommerceError as ex:\n        return showWoocommerceError(ex)\n    except Exception as ex:\n        return showApiError(ex)\n\n\n@license_manager.route('/api/v1/licensing/retrieve', methods=['GET'])\n@api_security\ndef retrieveLicense():\n    \"\"\"\n    Retrieve details about a License\n\n    =================\n    Request Arguments\n    =================\n\n    .. code-block:: text\n\n        license_key=<string>\n        key_encrypted=<bool>\n\n\n    ================\n    Response Payload\n    ================\n\n    .. code-block:: json\n\n        {\n            error: <string>,\n            msg: <string>,\n            kamreload: <bool>,\n            data: [\n                {\n                    license_key: <string>\n                    type: <string>\n                    expires: <string>\n                    active: <bool>\n                    valid: <bool>\n                }\n            ]\n        }\n    \"\"\"\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        data = request.args.to_dict()\n\n        # sanity checks (whitelist of acceptable args in the request body)\n        validateRequestArgs(data, allowed_args={'license_key': str, 'key_encrypted': bool}, required_args={'license_key'})\n\n        if data.get('key_encrypted', False):\n            matches = searchLicenses(key_combo=data['license_key'], decrypt=True)\n        else:\n            matches = searchLicenses(data['license_key'])\n\n        if len(matches) == 0:\n            response_payload['data'].append({})\n            response_payload['msg'] = 'license does not exist'\n            return jsonify(response_payload), StatusCodes.HTTP_NOT_FOUND\n\n        response_payload['data'].append(dict(matches[0]))\n        response_payload['msg'] = 'successfully retrieved license'\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except WoocommerceError as ex:\n        return showWoocommerceError(ex)\n    except Exception as ex:\n        return showApiError(ex)\n\n\n@license_manager.route('/api/v1/licensing/list', methods=['GET'])\n@api_security\ndef listLicenses():\n    \"\"\"\n    Retrieve details about all License's associated to this node\n\n    ================\n    Response Payload\n    ================\n\n    .. code-block:: json\n\n        {\n            error: <string>,\n            msg: <string>,\n            kamreload: <bool>,\n            data: [\n                {\n                    license_key: <string>\n                    type: <string>\n                    expires: <string>\n                    active: <bool>\n                    valid: <bool>\n                },\n                ...\n            ]\n        }\n    \"\"\"\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        response_payload['data'] = [dict(lc) for lc in searchLicenses()]\n        response_payload['msg'] = 'successfully retrieved licenses'\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except WoocommerceError as ex:\n        return showWoocommerceError(ex)\n    except Exception as ex:\n        return showApiError(ex)\n\n\n@license_manager.route('/api/v1/licensing/activate', methods=['PUT'])\n@api_security\ndef activateLicense():\n    \"\"\"\n    Activate a License\n\n    ===============\n    Request Payload\n    ===============\n\n    .. code-block:: json\n\n        {\n            license_key: <string>,\n            key_encrypted: <bool>\n        }\n\n    ================\n    Response Payload\n    ================\n\n    .. code-block:: json\n\n        {\n            error: <string>,\n            msg: <string>,\n            kamreload: <bool>,\n            data: [\n                {\n                    license_key: <string>\n                    type: <string>\n                    expires: <string>\n                    active: <bool>\n                    valid: <bool>\n                }\n            ]\n        }\n    \"\"\"\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        data = getRequestData()\n\n        # sanity checks (whitelist of acceptable args in the request body)\n        validateRequestArgs(data, allowed_args={'license_key': str, 'key_encrypted': bool}, required_args={'license_key'})\n\n        if data.get('key_encrypted', False):\n            args = dict(key_combo=data['license_key'], decrypt=True)\n        else:\n            args = dict(license_key=data['license_key'])\n\n        matches = searchLicenses(**args)\n        if len(matches) == 0:\n            lc = WoocommerceLicense(**args)\n        else:\n            lc = matches[0]\n\n        lc.activate()\n        addToLicenseStore(lc)\n\n        response_payload['data'].append(dict(lc))\n        response_payload['msg'] = 'activation succeeded'\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except WoocommerceError as ex:\n        return showWoocommerceError(ex)\n    except Exception as ex:\n        return showApiError(ex)\n\n\n@license_manager.route('/api/v1/licensing/deactivate', methods=['PUT'])\n@api_security\ndef deactivateLicense():\n    \"\"\"\n    Deactivate a License\n\n    ===============\n    Request Payload\n    ===============\n\n    .. code-block:: json\n\n        {\n            license_key: <string>,\n            key_encrypted: <bool>\n        }\n\n    ================\n    Response Payload\n    ================\n\n    .. code-block:: json\n\n        {\n            error: <string>,\n            msg: <string>,\n            kamreload: <bool>,\n            data: []\n        }\n    \"\"\"\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        data = getRequestData()\n\n        # sanity checks (whitelist of acceptable args in the request body)\n        validateRequestArgs(data, allowed_args={'license_key': str, 'key_encrypted': bool}, required_args={'license_key'})\n\n        if data.get('key_encrypted', False):\n            args = dict(key_combo=data['license_key'], decrypt=True)\n        else:\n            args = dict(license_key=data['license_key'])\n\n        matches = searchLicenses(**args)\n        if len(matches) == 0:\n            response_payload['error'] = 'other'\n            response_payload['msg'] = 'submitted key does not exist on this system'\n            return jsonify(response_payload), StatusCodes.HTTP_NOT_FOUND\n        else:\n            lc = matches[0]\n\n        lc.deactivate()\n        removeFromLicenseStore(lc)\n\n        response_payload['msg'] = 'deactivation succeeded'\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except WoocommerceError as ex:\n        return showWoocommerceError(ex)\n    except Exception as ex:\n        return showApiError(ex)\n"
  },
  {
    "path": "gui/modules/api/mediaserver/plugin/fusion/interface.py",
    "content": "# FusionPBX Plugin for the Media Server api\nimport psycopg2\nimport psycopg2.extras\nimport json\nimport uuid\n\n\n# Defining a dialplan object, which allows you to define how the\n# dialplan within a domain behaves\nclass cos_dialplan():\n\n    name = None\n    dialplan_xml = None\n\n    def __init__(self):\n        self.dialplan_xml = []\n\n    def add(self,data):\n        dialplan_entry = {}\n        dialplan_entry['name'] = data['name']\n        dialplan_entry['number'] = data['number']\n        dialplan_entry['continue'] = data['continue']\n        dialplan_entry['order'] = data['order']\n        dialplan_entry['description'] = data['description']\n        dialplan_entry['enabled'] = data['enabled']\n        dialplan_entry['hostname'] = data['hostname']\n        dialplan_entry['logic'] = data['logic']\n        self.dialplan_xml.append(dialplan_entry)\n\n\n\nclass cos():\n\n    domain_uuid=\"\"\n    db = \"\"\n    dialplan_list = []\n    domain = None\n\n    def __init__(self, domain,data=None):\n        if domain.db:\n            self.db = domain.db\n        self.domain_uuid = domain.domain_id\n        self.domain = domain\n\n\n        # Load up some default CoS definitions\n\n        # The default class of service will use the global dialplan within FUSIONPBX\n        # In other words no changes to the Domain dialplan manager\n        cos_default = cos_dialplan()\n        cos_default.name=\"default\"\n        cos_default.dialplan_xml = None\n        self.dialplan_list.append(cos_default)\n\n        # This class of service will disable inbound caller id\n        cos_standard = cos_dialplan()\n        cos_standard.name=\"standard\"\n        entry = {}\n        entry['name'] = \"block_caller_id\"\n        entry['number'] = \"\"\n        entry['continue'] = \"true\"\n        entry['order'] = 700\n        entry['enabled'] = \"true\"\n        entry['description'] = \"Block inbound caller id\"\n        entry['hostname'] = \"\"\n        entry['logic'] = \"<extension name=\\\"local_extension\\\" continue=\\\"true\\\" uuid=\\\"fbba5244-7453-4390-8976-2c307140529g\\\"> \\\n            <condition field=\\\"${user_exists}\\\" expression=\\\"true\\\"> \\\n               <action application=\\\"export\\\" data=\\\"sip_cid_type=none\\\"/> \\\n               <action application=\\\"export\\\" data=\\\"origination_caller_id_name=Blocked\\\"/> \\\n               <action application=\\\"export\\\" data=\\\"origination_caller_id_number=0000000000\\\"/> \\\n            </condition> \\\n            </extension>\"\n        cos_standard.add(entry)\n        self.dialplan_list.append(cos_standard)\n\n        if data is not None and 'settings' in data:\n            domain_settings = data['settings']\n            cos_settings = cos_dialplan()\n            cos_settings.name=\"domain_settings\"\n            entry = {}\n            entry['name'] = \"domain_settings\"\n            entry['number'] = \"\"\n            entry['continue'] = \"true\"\n            entry['order'] = 20\n            entry['enabled'] = \"true\"\n            entry['description'] = \"Domain Settings\"\n            entry['hostname'] = \"\"\n            entry['logic'] = \"<extension name=\\\"domain_settings\\\" continue=\\\"true\\\" uuid=\\\"af2cd91f-1db2-4742-b8c2-ef519ba6e8b5\\\"> \\\n    \t             <condition field=\\\"\\\" expression=\\\"\\\">  \\\n    \t\t               <action application=\\\"set\\\" data=\\\"default_language={}\\\"/> \\\n    \t\t               <action application=\\\"set\\\" data=\\\"default_dialect={}\\\"/> \\\n    \t\t               <action application=\\\"set\\\" data=\\\"default_voice={}\\\"/> \\\n    \t             </condition> \\\n                     </extension>\".format(domain_settings['default_language'],domain_settings['default_dialect'],domain_settings['default_voice'])\n            cos_settings.add(entry)\n            self.dialplan_list.append(cos_settings)\n\n\n    def create(self, name):\n        psycopg2.extras.register_uuid()\n        app_uuid = uuid.uuid4()\n        cur = self.db.cursor()\n\n        for cos_dp in self.dialplan_list:\n            print(\"{},{}\".format(cos_dp.name,name))\n            if cos_dp.name == name:\n                if cos_dp.dialplan_xml == None:\n                    continue\n                for xml in cos_dp.dialplan_xml:\n                    dialplan_uuid = uuid.uuid4()\n                    query = \"insert into v_dialplans (domain_uuid,dialplan_uuid,app_uuid,dialplan_context, \\\n                            dialplan_name,dialplan_number,dialplan_continue,dialplan_order,dialplan_enabled, \\\n                            dialplan_description, hostname, dialplan_xml) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)\"\n\n\n\n                    values = [self.domain_uuid,dialplan_uuid,app_uuid,self.domain.name, \\\n                            xml['name'], xml['number'], xml['continue'], \\\n                            xml['order'], xml['enabled'],xml['description'], \\\n                            xml['hostname'],xml['logic']]\n\n                    cur.execute(query,values)\n\n                self.db.commit()\n\n        cur.close()\n\n        def delete(self):\n            psycopg2.extras.register_uuid()\n            cur = self.db.cursor()\n\n            cur.execute(\"\"\"delete from v_dialplans where domain_uuid = %s\"\"\",(self.domain_uuid))\n\n            self.db.commit()\n            cur.close()\n            return True\n\n\nclass mediaserver():\n\n    hostname=''\n    port=''\n    auth_type=''\n    username=''\n    password=''\n    dbname=\"fusionpbx\"\n    db=''\n\n\n    def __init__(self, config):\n        self.hostname =config.hostname\n        self.port = config.port\n        self.auth_type = config.auth_type\n        self.username = config.username\n        self.password = config.password\n        self.dbname = config.dbname\n\n    def testConnection(self):\n        print(\"testConnection\")\n        pass\n\n    def getConnection(self):\n        try:\n            db = psycopg2.connect(host=self.hostname,port=self.port,user=self.username,password=self.password,dbname=self.dbname)\n            if db is not None:\n                print(\"Connection to FusionPBX: {} database was successful\".format(self.hostname))\n                return db\n\n        except Exception as ex:\n            raise\n\n    def closeConnection(self):\n        db.close()\n\nclass domain():\n    domain_id=\"\"\n    name = \"\"\n    enabled = \"\"\n    description = \"\"\n    cos = \"\" # Class of Service\n    db = None\n\n    def toJSON(self):\n        return json.dumps(self, default=lambda o: o.__dict__,\n            sort_keys=True, indent=4)\n\n\nclass domains():\n\n    mediaserver=''\n    db=''\n    domain_list = []\n\n    def __init__(self, mediaserver):\n        self.mediaserver = mediaserver\n        self.db = self.mediaserver.getConnection()\n\n    def create(self,data):\n        #Todo: Check if the domain exists before creating it\n        # call it in any place of your program\n        # before working with UUID objects in PostgreSQL\n        psycopg2.extras.register_uuid()\n        domain_uuid = uuid.uuid4()\n        cur = self.db.cursor()\n\n        # Check for Duplicate domains\n        cur.execute(\"\"\"select domain_name from v_domains where domain_name = %s\"\"\",(data['name'],))\n        rows = cur.fetchall()\n        if len(rows) > 0:\n            raise Exception (\"The domain already exists\")\n            return\n\n        # Create the new domain\n        cur.execute(\"\"\"insert into v_domains (domain_uuid,domain_name,domain_enabled,domain_description) \\\n                    values (%s,%s,%s,%s)\"\"\",(domain_uuid,data['name'],data['enabled'],data['description']))\n        self.db.commit()\n        d = domain()\n        d.domain_id = domain_uuid\n        d.name = data['name']\n        d.enabled = data['enabled']\n        d.description = data['description']\n        d.db = self.db\n        d.cos = data['cos']\n        cur.close()\n        return d\n\n\n    def read(self,domain_id=None):\n        cur = self.db.cursor()\n        query = \"select domain_uuid,domain_name,domain_enabled,domain_description from v_domains\"\n        values = []\n\n        if domain_id:\n            query = query + \" where domain_uuid = %s\"\n            values.append(domain_id)\n\n        cur.execute(query,(values))\n        rows = cur.fetchall()\n        if rows is not None:\n            for row in rows:\n                d = {}\n                d['domain_uuid'] = row[0]\n                d['domain_id'] = row[0]\n                d['name']= row[1]\n                d['enabled'] = row[2]\n                d['description'] = row[3]\n\n\n                self.domain_list.append(d)\n\n        return self.domain_list\n\n\n\n\n    def update(self,data):\n        cur = self.db.cursor()\n        # Convert UUID string to UUID type\n        domain_uuid=data['domain_id']\n        # Get the current data for the domain from the database\n        current_data = self.read(domain_uuid)\n        # Update the current data with the new data\n        if len(current_data) >= 1:\n            current_data[0]['name'] = data['name']\n            current_data[0]['enabled'] = data['enabled']\n            current_data[0]['description'] = data['description']\n\n            cur.execute(\"\"\"update v_domains set domain_name= %s,domain_enabled = %s,domain_description = %s where domain_uuid= %s\"\"\", \\\n                    (current_data[0]['name'],current_data[0]['enabled'],current_data[0]['description'],domain_uuid))\n            rows = cur.rowcount\n            self.db.commit()\n\n        cur.close()\n        return True\n\n\n    def delete(self,domain_id):\n        #Todo: Check if the domain exists before creating it\n        # call it in any place of your program\n        # before working with UUID objects in PostgreSQL\n        psycopg2.extras.register_uuid()\n        domain_uuid = uuid.uuid4()\n        cur = self.db.cursor()\n\n        # Delete from domains\n        cur.execute(\"\"\"delete from v_domains where domain_uuid = %s\"\"\",(domain_id,))\n\n        # Delete the inbound dialplan for the domain\n        cur.execute(\"\"\"delete from v_dialplans where domain_uuid = %s\"\"\",(domain_id,))\n\n\n        self.db.commit()\n        cur.close()\n        return True\n\n    def getExtensions():\n        pass\n\n\n\n\n\n\nclass extension():\n\n    extension_id=\"\"\n    domain_id=\"\"\n    account_code=\"\"\n    extension=\"\"\n    password=\"\"\n    outbound_caller_number=\"\"\n    outbound_caller_name=\"\"\n    vm_enabled=True\n    vm_password=\"\"\n    vm_notify_email=\"\"\n    enabled=False\n    call_timeout=30\n\n    def __init__(self):\n        pass\n\n    def toJSON(self):\n        return json.dumps(self, default=lambda o: o.__dict__,\n            sort_keys=True, indent=4)\n\nclass extensions():\n\n    mediaserver=None\n    domain=None\n    domain_name=''\n    domain_uuid=''\n    db=''\n    extension_list = []\n\n    def __init__(self, mediaserver, domain ,extension=None):\n        self.mediaserver = mediaserver\n        self.db = self.mediaserver.getConnection()\n        self.domain = domain\n        self.domain_uuid = self.domain['domain_id']\n        self.domain_name = self.domain['name']\n\n    def create(self,data):\n        psycopg2.extras.register_uuid()\n        extension_uuid = uuid.uuid4()\n        voicemail_uuid = uuid.uuid4()\n        cur = self.db.cursor()\n        cur.execute(\"\"\"insert into v_extensions (extension_uuid,domain_uuid,extension,password, \\\n                    user_context,call_timeout,enabled,outbound_caller_id_number, \\\n                    outbound_caller_id_name,accountcode) \\\n                    values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)\"\"\", \\\n                    (extension_uuid, self.domain_uuid, data.extension, \\\n                    data.password, self.domain['name'], data.call_timeout, data.enabled, \\\n                    data.outbound_caller_number,data.outbound_caller_name,data.account_code))\n\n        # Create voicemail box\n        query = \"insert into v_voicemails (domain_uuid,voicemail_uuid,voicemail_id, \\\n                voicemail_password,greeting_id,voicemail_alternate_greet_id, \\\n                voicemail_mail_to,voicemail_sms_to,voicemail_transcription_enabled,voicemail_attach_file, \\\n                voicemail_file,voicemail_local_after_email,voicemail_enabled,voicemail_description, \\\n                voicemail_name_base64,voicemail_tutorial) \\\n                values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) \"\n\n        values = [self.domain_uuid,voicemail_uuid, \\\n                 data.extension,data.vm_password,None,None,data.vm_notify_email,None,None,None,'attach',\"true\", data.vm_enabled,None,None,None]\n\n        cur.execute(query,values)\n\n        self.db.commit()\n        cur.close()\n\n\n    def read(self, extension_id=None):\n        cur = self.db.cursor()\n        query = \"select extension_uuid,domain_uuid,extension,password,user_context,call_timeout::integer as call_timeout, \\\n        enabled,outbound_caller_id_number,outbound_caller_id_name, accountcode from v_extensions where domain_uuid = %s\"\n        values = [self.domain_uuid]\n\n        if extension_id:\n            query = query + \" and extension_uuid = %s\"\n            values.append(extension_id)\n\n        cur.execute(query,(values))\n        rows = cur.fetchall()\n        if rows is not None:\n            for row in rows:\n                d = {}\n                d['extensions_id'] = row[0]\n                d['domain_uuid']= row[1]\n                d['extension'] = row[2]\n                #d['password'] = row[3]\n                d['user_context'] = row[4]\n                d['call_timeout'] = row[5]\n                d['enabled'] = row[6]\n                d['outbound_caller_number'] = row[7]\n                d['outbound_caller_name'] = row[8]\n                d['account_code'] = row[9]\n                self.extension_list.append(d)\n\n        return self.extension_list\n\n    def update(self,data):\n        psycopg2.extras.register_uuid()\n        cur = self.db.cursor()\n        query=\"update v_extensions set \"\n        values=[]\n        db_data = {}\n\n        #Map canaical format to database format\n        db_data['domain_uuid'] = data['domain_id']\n        db_data['extension'] = data['extension']\n        db_data['password'] = data['password']\n        db_data['enabled'] = data['enabled']\n        db_data['accountcode'] = data['account_code']\n        db_data['outbound_caller_id_number'] = data['outbound_caller_number']\n        db_data['outbound_caller_id_name'] = data['outbound_caller_name']\n        db_data['call_timeout'] = data['call_timeout']\n\n        for element in db_data:\n            # Don't process Voice Mail Attributes\n            if \"vm_\" in element:\n                continue\n            query = query + \"{} = %s,\".format(element)\n            values.append(db_data[element])\n\n        # Remove the last comma\n        if query[len(query)-1] == ',':\n            query = query[:-1]\n\n        query = query + \" where extension_uuid = '{}'\".format(self.getExtensionID(db_data['extension']))\n\n        print(query)\n        print(values)\n        cur.execute(query,(values))\n\n\n        self.db.commit()\n        cur.close()\n\n    def getExtensionID(self,extension):\n        cur = self.db.cursor()\n        query = \"select extension_uuid from v_extensions where domain_uuid = %s and extension = %s\"\n        values = [self.domain_uuid,extension]\n\n        cur.execute(query,(values))\n        rows = cur.fetchall()\n        if rows is not None:\n            for row in rows:\n                return row[0]\n\n        cur.close()\n\n    def delete(self,extension):\n        #Todo: Check if the domain exists before creating it\n        # call it in any place of your program\n        # before working with UUID objects in PostgreSQL\n        psycopg2.extras.register_uuid()\n        domain_uuid = uuid.uuid4()\n        cur = self.db.cursor()\n\n\n        cur.execute(\"\"\"delete from v_extensions where domain_uuid = %s and extension = %s\"\"\",(self.domain_uuid,extension))\n        cur.execute(\"\"\"delete from v_voicemails where domain_uuid = %s and voicemail_id = %s\"\"\",(self.domain_uuid,extension))\n        self.db.commit()\n        cur.close()\n        return True\n"
  },
  {
    "path": "gui/modules/api/mediaserver/plugin/fusionpbx",
    "content": ""
  },
  {
    "path": "gui/modules/api/mediaserver/routes.py",
    "content": "import importlib.util, os\nfrom flask import Blueprint, jsonify\nfrom database import startSession, DummySession, dSIPMultiDomainMapping\nfrom shared import debugEndpoint,StatusCodes, getRequestData\nfrom modules.api.api_functions import showApiError, api_security\nfrom werkzeug import exceptions as http_exceptions\nfrom util.ipc import STATE_SHMEM_NAME, getSharedMemoryDict\nimport settings\n\n\n# TODO: standardize response payloads using new createApiResponse()\n#       marked for implementation in v0.74\n\n\nclass config():\n\n    hostname=''\n    port=''\n    username=''\n    password=''\n    dbname=''\n    type=''\n    auth_type=''\n    plugin_type=''\n    plugin=None\n\n\n\n    def __init__(self,config_id):\n\n        db = DummySession()\n\n        db = startSession()\n        try:\n            print(\"The config_id is {}\".format(config_id))\n            domainMapping = db.query(dSIPMultiDomainMapping).filter(dSIPMultiDomainMapping.pbx_id == config_id).first()\n            if domainMapping is None:\n                raise Exception(\"Configuration doesn't exist\")\n            else:\n                print(\"***In domain mapping***\")\n                self.hostname=domainMapping.db_host\n                #self.port=domainMapping.port if \"port\" in domainMapping else \"5432\"\n                self.port=\"5432\"\n                self.username=domainMapping.db_username\n                self.password=domainMapping.db_password\n                self.dbname=\"fusionpbx\"\n\n                #response_payload['msg'] = 'Domain Configuration Exists'\n\n                if domainMapping.type == int(dSIPMultiDomainMapping.FLAGS.TYPE_FUSIONPBX.value):\n                    self.plugin_type = FLAGS.FUSIONPBX_PLUGIN\n                elif domainMapping.type == dSIPMultiDomainMapping.FLAGS.TYPE_FUSIONPBX_CLUSTER.value:\n                    self.plugin_type = FLAGS.FUSIONPBX_PLUGIN\n                elif domainMapping.type == dSIPMultiDomainMapping.FLAGS.TYPE_FREEPBX.value:\n                    self.plugin_type = FLAGS.FREEPBX_PLUGIN;\n                    raise Exception(\"FreePBX Plugin is not supported yet\")\n                else:\n                    raise Exception(\"PBX plugin for config #{} can not be found\".format(config_id))\n\n                # Import plugin\n\n                # Returns the Base directory of this file\n                base_dir = os.path.dirname(__file__)\n\n                # Use the Base Dir to specify the location of the plugin required for this domain\n                spec = importlib.util.spec_from_file_location(\"plugin.{}\".format(self.plugin_type), \"{}/plugin/{}/interface.py\".format(base_dir,self.plugin_type))\n                self.plugin = importlib.util.module_from_spec(spec)\n                if spec.loader.exec_module(self.plugin):\n                    print(\"***Plugin was loaded***\")\n                return\n\n        except Exception as ex:\n            raise ex\n        finally:\n            db.close()\n\n\n    def getPlugin(self):\n        if self.plugin:\n            return self.plugin\n        else:\n            raise Exception(\"The plugin could not be loaded\")\n\nclass FLAGS():\n\n    FUSIONPBX_PLUGIN = \"fusion\"\n    FREEPBX_PLUGIN = \"freepbx\"\n\n\nmediaserver = Blueprint('mediaserver','__name__')\n\n@mediaserver.route('/api/v1/mediaserver/domain/',methods=['GET'])\n@mediaserver.route('/api/v1/mediaserver/domain/<string:config_id>',methods=['GET'])\n@mediaserver.route('/api/v1/mediaserver/domain/<string:config_id>/<string:domain_id>',methods=['GET'])\n@api_security\ndef getDomains(config_id=None,domain_id=None):\n    \"\"\"\n    List all of the domains on a PBX\\n\n    If the PBX only contains a single domain then it will return the hostname or ip address of the system.\n    If the PBX is multi-tenant then a list of all domains will be returned\n\n    ===============\n    Request Payload\n    ===============\n\n    .. code-block:: json\n\n\n    {}\n\n    ================\n    Response Payload\n    ================\n\n    .. code-block:: json\n\n        {\n            error: <string>,\n            msg: <string>,\n            kamreload: <bool>,\n            data: [\n                domains: [\n                    {\n                    domain_id: <int>,\n                    name: <string>,\n                    enabled: <string>,\n                    description: <string>\n                    }\n                ]\n            ]\n        }\n    \"\"\"\n\n    # Determine which plug-in to use\n    # Currently we only support FusionPBX\n    # Check if Configuration ID exists\n\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        if config_id != None:\n            config_info = config(config_id)\n            plugin = config_info.getPlugin()\n            # Create instance of Media Server Class\n            if plugin:\n                mediaserver = plugin.mediaserver(config_info)\n                if mediaserver:\n                    domains = plugin.domains(mediaserver)\n                    # Use plugin to get list of domains by calling plugin.<pbxtype>.getDomain()\n                    domain_list = domains.read(domain_id)\n                    response_payload['msg'] = '{} domains were found'.format(len(domain_list))\n                    response_payload['data'].append(domain_list)\n        else:\n            raise Exception(\"The configuration id must be provided\")\n\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except Exception as ex:\n        return showApiError(ex)\n\n\n\n\n@mediaserver.route('/api/v1/mediaserver/domain',methods=['POST'])\n@api_security\ndef postDomains():\n\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    VALID_REQUEST_DATA_ARGS = {\"name\": str, \"enabled\": bool, \"description\": str, \"config_id\": int, \"cos\": str, \"settings\": dict}\n\n    # ensure requred args are provided\n    REQUIRED_ARGS = {'name','config_id'}\n\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n\n        # get request data\n        data = getRequestData()\n\n        # sanity checks\n        for k, v in data.items():\n            if k not in VALID_REQUEST_DATA_ARGS.keys():\n                raise http_exceptions.BadRequest(\"Request data argument '{}' not recognized\".format(k))\n            if not type(v) == VALID_REQUEST_DATA_ARGS[k]:\n                raise http_exceptions.BadRequest(\"Request data argument '{}' not valid\".format(k))\n\n        for k in REQUIRED_ARGS:\n            if k not in VALID_REQUEST_DATA_ARGS.keys():\n                raise http_exceptions.BadRequest(\"Request argument '{}' is required\".format(k))\n\n        config_id = data['config_id']\n        cos = data['cos'] if 'cos' in data else None\n        domain_settings = data['settings'] if 'settings' in data else None\n\n        # Create instance of Media Server Class\n        if config_id != None:\n            config_info = config(config_id)\n            plugin = config_info.getPlugin()\n            # Create instance of Media Server Class\n            if plugin:\n                mediaserver = plugin.mediaserver(config_info)\n                if mediaserver:\n                    domains = plugin.domains(mediaserver)\n                    domain = domains.create(data)\n                    #Generate Close of Service\n                    if cos:\n                        cos_object = plugin.cos(domain,data)\n                        cos_object.create(cos)\n                    if domain_settings:\n                        cos_object.create(\"domain_settings\")\n\n                    response_payload['data'] = {\"domain_id\": domain.domain_id}\n\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except Exception as ex:\n        return showApiError(ex)\n\n\n@mediaserver.route('/api/v1/mediaserver/domain',methods=['PUT'])\n@api_security\ndef putDomains():\n\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    VALID_REQUEST_DATA_ARGS = {\"name\": str, \"enabled\": bool, \"description\": str, \"config_id\": int, \"domain_id\": str}\n\n    # ensure requred args are provided\n    REQUIRED_ARGS = {'domain_id','config_id'}\n\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n\n        # get request data\n        data = getRequestData()\n\n        # sanity checks\n        for k, v in data.items():\n            if k not in VALID_REQUEST_DATA_ARGS.keys():\n                raise http_exceptions.BadRequest(\"Request data argument '{}' not recognized\".format(k))\n            if not type(v) == VALID_REQUEST_DATA_ARGS[k]:\n                raise http_exceptions.BadRequest(\"Request data argument '{}' not valid\".format(k))\n\n        for k in REQUIRED_ARGS:\n            if k not in VALID_REQUEST_DATA_ARGS.keys():\n                raise http_exceptions.BadRequest(\"Request argument '{}' is required\".format(k))\n\n        config_id = data['config_id']\n        domain_id = data['domain_id']\n\n        # Create instance of Media Server Class\n        if config_id != None and domain_id != None:\n            config_info = config(config_id)\n            plugin = config_info.getPlugin()\n            # Create instance of Media Server Class\n            if plugin:\n                mediaserver = plugin.mediaserver(config_info)\n                if mediaserver:\n                    domains = plugin.domains(mediaserver)\n                    domain_id = domains.update(data)\n                    response_payload['msg'] = \"Success\"\n\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except Exception as ex:\n        return showApiError(ex)\n\n\n@mediaserver.route('/api/v1/mediaserver/domain',methods=['DELETE'])\n@api_security\ndef deleteDomains():\n\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    VALID_REQUEST_DATA_ARGS = {\"domain_id\": str, \"config_id\": int}\n\n    # ensure requred args are provided\n    REQUIRED_ARGS = {'domain_id','config_id'}\n\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n\n        # get request data\n        data = getRequestData()\n\n        # sanity checks\n        for k, v in data.items():\n            if k not in VALID_REQUEST_DATA_ARGS.keys():\n                raise http_exceptions.BadRequest(\"Request data argument '{}' not recognized\".format(k))\n            if not type(v) == VALID_REQUEST_DATA_ARGS[k]:\n                raise http_exceptions.BadRequest(\"Request data argument '{}' not valid\".format(k))\n\n        for k in REQUIRED_ARGS:\n            if k not in VALID_REQUEST_DATA_ARGS.keys():\n                raise http_exceptions.BadRequest(\"Request argument '{}' is required\".format(k))\n\n        config_id = data['config_id']\n        domain_id = data['domain_id']\n\n        # Create instance of Media Server Class\n        if config_id != None:\n            config_info = config(config_id)\n            plugin = config_info.getPlugin()\n            # Create instance of Media Server Class\n            if plugin:\n                mediaserver = plugin.mediaserver(config_info)\n                if mediaserver:\n                    domains = plugin.domains(mediaserver)\n                    # Delete the domain\n                    if domains.delete(domain_id):\n                        response_payload['msg'] = \"Success\"\n\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except Exception as ex:\n        return showApiError(ex)\n\n\n\n\n@mediaserver.route('/api/v1/mediaserver/extension',methods=['POST'])\n@api_security\ndef postExtensions():\n\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    VALID_REQUEST_DATA_ARGS = {\"domain_id\": str, \"account_code\": str, \"extension\": str, \"password\": str, \\\n                              \"outbound_caller_number\": str, \"outbound_caller_name\": str, \"vm_enabled\": bool, \\\n                              \"vm_password\": int, \"vm_notify_email\": str, \"enabled\": bool, \"call_timeout\": int, \\\n                              \"config_id\": int}\n\n    # ensure requred args are provided\n    REQUIRED_ARGS = {'domain_id','extension','password', 'enabled','config_id'}\n\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n\n        # get request data\n        data = getRequestData()\n        print(data)\n\n        # sanity checks\n        for k, v in data.items():\n            if k not in VALID_REQUEST_DATA_ARGS.keys():\n                raise http_exceptions.BadRequest(\"Request data argument '{}' not recognized\".format(k))\n            if not type(v) == VALID_REQUEST_DATA_ARGS[k]:\n                raise http_exceptions.BadRequest(\"Request data argument '{}' not valid\".format(k))\n\n        for k in REQUIRED_ARGS:\n            if k not in VALID_REQUEST_DATA_ARGS.keys():\n                raise http_exceptions.BadRequest(\"Request argument '{}' is required\".format(k))\n\n\n        # Create instance of Media Server Class\n        config_id = data['config_id']\n        domain_id = data['domain_id']\n\n        if config_id != None and domain_id != None:\n            config_info = config(config_id)\n            plugin = config_info.getPlugin()\n            # Create instance of Media Server Class\n            if plugin:\n                mediaserver = plugin.mediaserver(config_info)\n                if mediaserver:\n                    domains = plugin.domains(mediaserver)\n                    domain = domains.read(domain_id)\n                    print(domain_id)\n                    print(domain[0]['domain_id'])\n                    extensions = plugin.extensions(mediaserver,domain[0])\n                    ext = plugin.extension()\n                    ext.domain_id=data['domain_id']\n                    ext.extension=data['extension']\n                    ext.password=data['password']\n                    ext.enabled=data['enabled']\n                    ext.config_id=data['config_id']\n                    ext.outbound_caller_number=data['outbound_caller_number']\n                    ext.outbound_caller_name=data['outbound_caller_name']\n                    ext.vm_enabled=data['vm_enabled']\n                    ext.vm_password=data['vm_password']\n                    ext.vm_notify_email=data['vm_notify_email']\n                    ext.account_code=data['account_code']\n                    ext.call_timeout=data['call_timeout']\n                    extensions.create(ext)\n\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except Exception as ex:\n        return showApiError(ex)\n\n\n@mediaserver.route('/api/v1/mediaserver/extension',methods=['PUT'])\n@api_security\ndef putExtensions():\n\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    VALID_REQUEST_DATA_ARGS = {\"domain_id\": str, \"account_code\": str, \"extension\": str, \"password\": str, \\\n                              \"outbound_caller_number\": str, \"outbound_caller_name\": str, \"vm_enabled\": bool, \\\n                              \"vm_password\": int, \"vm_notify_email\": str, \"enabled\": bool, \"call_timeout\": int, \\\n                              \"config_id\": int}\n\n    # ensure requred args are provided\n    REQUIRED_ARGS = {'domain_id','extension','password', 'enabled','config_id'}\n\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n\n        # get request data\n        data = getRequestData()\n        print(data)\n\n        # sanity checks\n        for k, v in data.items():\n            if k not in VALID_REQUEST_DATA_ARGS.keys():\n                raise http_exceptions.BadRequest(\"Request data argument '{}' not recognized\".format(k))\n            if not type(v) == VALID_REQUEST_DATA_ARGS[k]:\n                raise http_exceptions.BadRequest(\"Request data argument '{}' not valid\".format(k))\n\n        for k in REQUIRED_ARGS:\n            if k not in VALID_REQUEST_DATA_ARGS.keys():\n                raise http_exceptions.BadRequest(\"Request argument '{}' is required\".format(k))\n\n\n        # Create instance of Media Server Class\n        config_id = data['config_id']\n        domain_id = data['domain_id']\n\n        if config_id != None and domain_id != None:\n            config_info = config(config_id)\n            plugin = config_info.getPlugin()\n            # Create instance of Media Server Class\n            if plugin:\n                mediaserver = plugin.mediaserver(config_info)\n                if mediaserver:\n                    domains = plugin.domains(mediaserver)\n                    domain = domains.read(domain_id)\n                    print(domain_id)\n                    print(domain[0]['domain_id'])\n                    extensions = plugin.extensions(mediaserver,domain[0])\n                    extensions.update(data)\n\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except Exception as ex:\n        return showApiError(ex)\n\n\n@mediaserver.route('/api/v1/mediaserver/extension/<string:config_id>/<string:domain_id>',methods=['GET'])\n@mediaserver.route('/api/v1/mediaserver/extension/<string:config_id>/<string:domain_id>/<string:extension_id>',methods=['GET'])\n@api_security\ndef getExtensions(config_id=None,domain_id=None,extension_id=None):\n    \"\"\"\n    List all of the domains on a PBX\\n\n    If the PBX only contains a single domain then it will return the hostname or ip address of the system.\n    If the PBX is multi-tenant then a list of all domains will be returned\n\n    ===============\n    Request Payload\n    ===============\n\n    .. code-block:: json\n\n\n    {}\n\n    ================\n    Response Payload\n    ================\n\n    .. code-block:: json\n    {\n         \"data\": [\n        [\n            {\n                \"call_timeout\": null,\n                \"domain_uuid\": \"51f66016-c2d5-4bd8-8117-29c8fc8ffa17\",\n                \"enabled\": \"true\",\n                \"extensions_id\": \"ae3cb4b8-f467-4a13-9bb8-9296226c1887\",\n                \"number\": \"504\",\n                \"user_context\": \"restaurant.detroitpbx.com\"\n            }\n        ]\n    }\n    \"\"\"\n\n    # Determine which plug-in to use\n    # Currently we only support FusionPBX\n    # Check if Configuration ID exists\n\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n    try:\n        if settings.DEBUG:\n            debugEndpoint()\n\n        if config_id != None and domain_id != None:\n            config_info = config(config_id)\n            plugin = config_info.getPlugin()\n            # Create instance of Media Server Class\n            if plugin:\n                mediaserver = plugin.mediaserver(config_info)\n                if mediaserver:\n                    domains = plugin.domains(mediaserver)\n                    domain = domains.read(domain_id)\n                    extensions = plugin.extensions(mediaserver,domain[0])\n                    extension_list = extensions.read(extension_id)\n                    response_payload['msg'] = '{} extensions were found'.format(len(extension_list))\n                    response_payload['data'].append(extension_list)\n        else:\n                raise Exception(\"The configuration id and the domain_id must be provided\")\n\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except Exception as ex:\n        return showApiError(ex)\n\n\n@mediaserver.route('/api/v1/mediaserver/extension',methods=['DELETE'])\n@api_security\ndef deleteExtensions():\n\n    # use a whitelist to avoid possible buffer overflow vulns or crashes\n    VALID_REQUEST_DATA_ARGS = {\"domain_id\": str, \"config_id\": int, \"extension\": str}\n\n    # ensure requred args are provided\n    REQUIRED_ARGS = VALID_REQUEST_DATA_ARGS\n\n    # defaults.. keep data returned separate from returned metadata\n    response_payload = {'error': '', 'msg': '', 'kamreload': getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'], 'data': []}\n\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n\n        # get request data\n        data = getRequestData()\n\n        # sanity checks\n        for k, v in data.items():\n            if k not in VALID_REQUEST_DATA_ARGS.keys():\n                raise http_exceptions.BadRequest(\"Request data argument '{}' not recognized\".format(k))\n            if not type(v) == VALID_REQUEST_DATA_ARGS[k]:\n                raise http_exceptions.BadRequest(\"Request data argument '{}' not valid\".format(k))\n\n        for k in REQUIRED_ARGS:\n            if k not in VALID_REQUEST_DATA_ARGS.keys():\n                raise http_exceptions.BadRequest(\"Request argument '{}' is required\".format(k))\n\n        config_id = data['config_id']\n        domain_id = data['domain_id']\n        extension = data['extension']\n\n        # Create instance of Media Server Class\n        if config_id != None:\n            config_info = config(config_id)\n            plugin = config_info.getPlugin()\n            # Create instance of Media Server Class\n            if plugin:\n                mediaserver = plugin.mediaserver(config_info)\n                if mediaserver:\n                    domains = plugin.domains(mediaserver)\n                    domain = domains.read(domain_id)\n                    if domain:\n                        extensions = plugin.extensions(mediaserver,domain[0])\n                        if extensions.delete(extension):\n                            response_payload['msg'] = \"Success\"\n\n        return jsonify(response_payload), StatusCodes.HTTP_OK\n\n    except Exception as ex:\n        return showApiError(ex)\n"
  },
  {
    "path": "gui/modules/api/sample_api.py",
    "content": "from flask import Blueprint\nfrom modules.api.api_functions import api_security\n\n\nnew_api = Blueprint('new_api','__name__')\n\n# Sample route.  Replace new_api and new_entity with the name of the api\n# and the name of the entity that it will manipulate\n@new_api.route('/api/v1/new_api/new_entity')\n@api_security\ndef getEntity():\n    result = \"{ \\\n        domain_id: 1012 \\\n        name: AprilandMackCo, \\\n        enabled: true, \\\n        description: 'April and Mack Co', \\\n        config_id: 64 \\\n        }\"\n    return result\n"
  },
  {
    "path": "gui/modules/cdr/cdrs.sql",
    "content": "-- MySQL dump 10.14  Distrib 5.5.52-MariaDB, for Linux (x86_64)\n--\n-- Host: localhost    Database: kamailio\n-- ------------------------------------------------------\n-- Server version\t5.5.52-MariaDB\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT = @@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS = @@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION = @@COLLATION_CONNECTION */;\n/*!40101 SET NAMES utf8 */;\n/*!40103 SET @OLD_TIME_ZONE = @@TIME_ZONE */;\n/*!40103 SET TIME_ZONE = '+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS = @@UNIQUE_CHECKS, UNIQUE_CHECKS = 0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS = 0 */;\n/*!40101 SET @OLD_SQL_MODE = @@SQL_MODE, SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES = @@SQL_NOTES, SQL_NOTES = 0 */;\n\n--\n-- Table structure for table `acc`\n--\n\nDROP TABLE IF EXISTS `acc`;\n/*!40101 SET @saved_cs_client = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `acc` (\n  `id`            int(10) UNSIGNED NOT NULL AUTO_INCREMENT,\n  `method`        varchar(16)      NOT NULL DEFAULT '',\n  `from_tag`      varchar(128)      NOT NULL DEFAULT '',\n  `to_tag`        varchar(128)      NOT NULL DEFAULT '',\n  `callid`        varchar(255)     NOT NULL DEFAULT '',\n  `sip_code`      char(3)          NOT NULL DEFAULT '',\n  `sip_reason`    varchar(255)      NOT NULL DEFAULT '',\n  `time`          datetime         NOT NULL DEFAULT NOW(),\n  `src_ip`        varchar(64)      NOT NULL DEFAULT '',\n  `dst_ouser`     varchar(128)      NOT NULL DEFAULT '',\n  `dst_user`      varchar(128)      NOT NULL DEFAULT '',\n  `dst_domain`    varchar(255)     NOT NULL DEFAULT '',\n  `src_user`      varchar(128)      NOT NULL DEFAULT '',\n  `src_domain`    varchar(255)     NOT NULL DEFAULT '',\n  `cdr_id`        int(10) UNSIGNED NOT NULL DEFAULT '0',\n  `calltype`      varchar(20)               DEFAULT NULL,\n  `src_gwgroupid` varchar(10)      NOT NULL DEFAULT '',\n  `dst_gwgroupid` varchar(10)      NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  KEY `acc_callid` (`callid`)\n  );\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `cdrs`\n--\n\nDROP TABLE IF EXISTS `cdrs`;\n/*!40101 SET @saved_cs_client = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `cdrs` (\n  `cdr_id`          bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,\n  `src_username`    varchar(128)      NOT NULL DEFAULT '',\n  `src_domain`      varchar(255)     NOT NULL DEFAULT '',\n  `dst_username`    varchar(128)      NOT NULL DEFAULT '',\n  `dst_domain`      varchar(255)     NOT NULL DEFAULT '',\n  `dst_ousername`   varchar(128)      NOT NULL DEFAULT '',\n  `call_start_time` datetime         NOT NULL,\n  `duration`        int(10) UNSIGNED NOT NULL DEFAULT '0',\n  `sip_call_id`     varchar(255)     NOT NULL DEFAULT '',\n  `sip_from_tag`    varchar(128)     NOT NULL DEFAULT '',\n  `sip_to_tag`      varchar(128)     NOT NULL DEFAULT '',\n  `src_ip`          varchar(64)      NOT NULL DEFAULT '',\n  `cost`            int(11)          NOT NULL DEFAULT '0',\n  `rated`           int(11)          NOT NULL DEFAULT '0',\n  `created`         datetime         NOT NULL DEFAULT NOW(),\n  `calltype`        varchar(20)               DEFAULT NULL,\n  `fraud`           bool             NOT NULL DEFAULT '0',\n  `src_gwgroupid`   varchar(10)      NOT NULL DEFAULT '',\n  `dst_gwgroupid`   varchar(10)      NOT NULL DEFAULT '',\n  PRIMARY KEY (`cdr_id`)\n  );\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping routines for database 'kamailio'\n--\n/*!50003 DROP PROCEDURE IF EXISTS `kamailio_cdrs` */;\n/*!50003 SET @saved_cs_client = @@character_set_client */;\n/*!50003 SET @saved_cs_results = @@character_set_results */;\n/*!50003 SET @saved_col_connection = @@collation_connection */;\n/*!50003 SET character_set_client = utf8 */;\n/*!50003 SET character_set_results = utf8 */;\n/*!50003 SET collation_connection = utf8_general_ci */;\n/*!50003 SET @saved_sql_mode = @@sql_mode */;\n/*!50003 SET sql_mode = '' */;\nDELIMITER ;;\nCREATE PROCEDURE `kamailio_cdrs`()\nBEGIN\n  DECLARE done int DEFAULT 0;\n  DECLARE bye_record int DEFAULT 0;\n  DECLARE v_src_user,v_src_domain,v_dst_user,v_dst_domain,v_callid,v_from_tag,\n    v_to_tag,v_src_ip,v_calltype varchar(255);\n  DECLARE v_src_gwgroupid, v_dst_gwgroupid int(11);\n  DECLARE v_inv_time, v_bye_time datetime;\n  DECLARE inv_cursor CURSOR FOR\n    SELECT src_user,\n           src_domain,\n           dst_user,\n           dst_domain,\n           time,\n           callid,\n           from_tag,\n           to_tag,\n           src_ip,\n           calltype,\n           src_gwgroupid,\n           dst_gwgroupid\n    FROM acc\n    WHERE method = 'INVITE'\n      AND cdr_id = '0';\n  DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;\n  OPEN inv_cursor;\n  REPEAT\n    FETCH inv_cursor INTO v_src_user, v_src_domain, v_dst_user, v_dst_domain,\n      v_inv_time, v_callid, v_from_tag, v_to_tag, v_src_ip, v_calltype,\n      v_src_gwgroupid, v_dst_gwgroupid;\n    IF NOT done THEN\n      SET bye_record = 0;\n      SELECT 1, time\n      INTO bye_record, v_bye_time\n      FROM acc\n      WHERE method = 'BYE'\n        AND callid = v_callid\n        AND ((from_tag = v_from_tag\n        AND to_tag = v_to_tag)\n        OR (from_tag = v_to_tag AND to_tag = v_from_tag))\n      ORDER BY time ASC\n      LIMIT 1;\n      IF bye_record = 1 THEN\n        INSERT INTO cdrs (src_username, src_domain, dst_username, dst_domain,\n                          call_start_time, duration, sip_call_id, sip_from_tag,\n                          sip_to_tag, src_ip, created, calltype, src_gwgroupid, dst_gwgroupid)\n        VALUES (v_src_user, v_src_domain, v_dst_user, v_dst_domain, v_inv_time,\n                UNIX_TIMESTAMP(v_bye_time) - UNIX_TIMESTAMP(v_inv_time),\n                v_callid, v_from_tag, v_to_tag, v_src_ip, NOW(), v_calltype,\n                v_src_gwgroupid, v_dst_gwgroupid);\n        UPDATE acc\n        SET cdr_id=last_insert_id()\n        WHERE callid = v_callid\n          AND from_tag = v_from_tag\n          AND to_tag = v_to_tag;\n      END IF;\n      SET done = 0;\n    END IF;\n  UNTIL done END REPEAT;\nEND ;;\nDELIMITER ;\n\n/*!50003 SET sql_mode = @saved_sql_mode */;\n/*!50003 SET character_set_client = @saved_cs_client */;\n/*!50003 SET character_set_results = @saved_cs_results */;\n/*!50003 SET collation_connection = @saved_col_connection */;\n/*!50003 DROP PROCEDURE IF EXISTS `kamailio_rating` */;\n/*!50003 SET @saved_cs_client = @@character_set_client */;\n/*!50003 SET @saved_cs_results = @@character_set_results */;\n/*!50003 SET @saved_col_connection = @@collation_connection */;\n/*!50003 SET character_set_client = utf8 */;\n/*!50003 SET character_set_results = utf8 */;\n/*!50003 SET collation_connection = utf8_general_ci */;\n/*!50003 SET @saved_sql_mode = @@sql_mode */;\n/*!50003 SET sql_mode = '' */;\nDELIMITER ;;\nCREATE PROCEDURE `kamailio_rating`(`rgroup` varchar(64))\nBEGIN\n  DECLARE done, rate_record, vx_cost int DEFAULT 0;\n  DECLARE v_cdr_id bigint DEFAULT 0;\n  DECLARE v_duration, v_rate_unit, v_time_unit int DEFAULT 0;\n  DECLARE v_dst_username varchar(255);\n  DECLARE cdrs_cursor CURSOR FOR SELECT cdr_id, dst_username, duration\n                                 FROM cdrs\n                                 WHERE rated = 0;\n  DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;\n  OPEN cdrs_cursor;\n  REPEAT\n    FETCH cdrs_cursor INTO v_cdr_id, v_dst_username, v_duration;\n    IF NOT done THEN\n      SET rate_record = 0;\n      SELECT 1, rate_unit, time_unit\n      INTO rate_record, v_rate_unit, v_time_unit\n      FROM billing_rates\n      WHERE rate_group = rgroup\n        AND v_dst_username LIKE concat(prefix, '%')\n      ORDER BY prefix DESC\n      LIMIT 1;\n      IF rate_record = 1 THEN\n        SET vx_cost = v_rate_unit * CEIL(v_duration / v_time_unit);\n        UPDATE cdrs SET rated=1, cost=vx_cost WHERE cdr_id = v_cdr_id;\n      END IF;\n      SET done = 0;\n    END IF;\n  UNTIL done END REPEAT;\nEND ;;\nDELIMITER ;\n/*!50003 SET sql_mode = @saved_sql_mode */;\n/*!50003 SET character_set_client = @saved_cs_client */;\n/*!50003 SET character_set_results = @saved_cs_results */;\n/*!50003 SET collation_connection = @saved_col_connection */;\n/*!40103 SET TIME_ZONE = @OLD_TIME_ZONE */;\n\n/*!40101 SET SQL_MODE = @OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS = @OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS = @OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT = @OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS = @OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION = @OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES = @OLD_SQL_NOTES */;\n\n-- Dump completed on 2017-10-07 11:57:33\n"
  },
  {
    "path": "gui/modules/cdr/cron_functions.py",
    "content": "from datetime import datetime\nfrom shared import debugException\nfrom database import startSession, DummySession, dSIPCDRInfo\nfrom modules.api.api_routes import generateCDRS\n\ndef sendCdrReport(gwgroupid):\n    db = DummySession()\n\n    try:\n        db = startSession()\n\n        now = datetime.now()\n        cdr_info = db.query(dSIPCDRInfo).filter(dSIPCDRInfo.gwgroupid == gwgroupid).first()\n        generateCDRS(gwgroupid=gwgroupid, report_type='csv', send_email=True, dtfilter=cdr_info.last_sent, run_standalone=True)\n        cdr_info.last_sent = now\n        db.commit()\n\n    except Exception as ex:\n        debugException(ex)\n        db.rollback()\n        db.flush()\n    finally:\n        db.close()\n"
  },
  {
    "path": "gui/modules/cdr/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# ENABLED=1 --> install, ENABLED=0 --> do nothing, ENABLED=-1 uninstall\nENABLED=1\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction installSQL {\n    # Check to see if the acc table or cdr tables are in use\n    MERGE_DATA=0\n    acc_row_count=$(withRootDBConn --db=\"$KAM_DB_NAME\" mysql -sN -e \"select count(*) from acc limit 10\" 2> /dev/null)\n    if [ ${acc_row_count:-0} -gt 0 ]; then\n        MERGE_DATA=1\n    fi\n\n    if [ ${MERGE_DATA} -eq 1 ]; then\n        printwarn \"The accounting table (acc) in Kamailio already exists. Merging table data\"\n        (\n            cat ${DSIP_PROJECT_DIR}/gui/modules/cdr/cdrs.sql;\n            withRootDBConn --db=\"$KAM_DB_NAME\" mysqldump --single-transaction --skip-triggers --skip-add-drop-table --no-create-info \\\n                --insert-ignore dsip_lcr\n        ) | withRootDBConn --db=\"$KAM_DB_NAME\" mysql\n    else\n        # Replace the CDR tables and add some Kamailio stored procedures\n        printwarn \"Adding/Replacing the tables needed for CDR's within dSIPRouter...\"\n        withRootDBConn --db=\"$KAM_DB_NAME\" mysql -sN <${DSIP_PROJECT_DIR}/gui/modules/cdr/cdrs.sql\n    fi\n}\n\nfunction install {\n    installSQL\n    enableKamailioConfigAttrib 'WITH_CDRS' ${DSIP_KAMAILIO_CONFIG_FILE}\n    printdbg \"CDR module installed\"\n}\n\nfunction uninstall {\n    disableKamailioConfigAttrib 'WITH_CDRS' ${DSIP_KAMAILIO_CONFIG_FILE}\n    printdbg \"CDR module uninstalled\"\n}\n\nfunction main {\n    if [[ ${ENABLED} -eq 1 ]]; then\n        install && exit 0 || exit 1\n    elif [[ ${ENABLED} -eq -1 ]]; then\n        uninstall && exit 0 || exit 1\n    else\n        exit 0\n    fi\n}\n\nmain\n"
  },
  {
    "path": "gui/modules/certificates/certificates.sql",
    "content": "DROP TABLE IF EXISTS `dsip_certificates`; \nCREATE TABLE IF NOT EXISTS `dsip_certificates` (\n  `id` INT NOT NULL AUTO_INCREMENT,\n  `domain` VARCHAR(128) NULL,\n  `type` VARCHAR(45) NULL,\n  `email` VARCHAR(128) NULL,\n  `cert` BLOB NULL,\n  `key` BLOB NULL,\n  PRIMARY KEY (`id`)\n);\n"
  },
  {
    "path": "gui/modules/certificates/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# ENABLED=1 --> install, ENABLED=0 --> do nothing, ENABLED=-1 uninstall\nENABLED=1\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction installSQL {\n    # Check to see if the acc table or cdr tables are in use\n    MERGE_DATA=0\n    count=$(withRootDBConn --db=\"$KAM_DB_NAME\" mysql -sN -e \"select count(*) from dsip_certificates limit 10\" 2> /dev/null)\n    if [ ${count:-0} -gt 0 ]; then\n        MERGE_DATA=1\n    fi\n\n    if [ ${MERGE_DATA} -eq 1 ]; then\n\t    printwarn \"The table already exists. Merging table data\"\n\t    (\n\t        cat ${DSIP_PROJECT_DIR}/gui/modules/certificates/certificates.sql;\n            withRootDBConn --db=\"$KAM_DB_NAME\" mysqldump --single-transaction --skip-triggers --skip-add-drop-table --no-create-info \\\n            --insert-ignore dsip_certificates\n        ) | withRootDBConn --db=\"$KAM_DB_NAME\" mysql\n    else\n        # Replace the api tables\n        printwarn \"Adding/Replacing the tables needed for Certificates module within dSIPRouter...\"\n        withRootDBConn --db=\"$KAM_DB_NAME\" mysql -sN <${DSIP_PROJECT_DIR}/gui/modules/certificates/certificates.sql\n    fi\n}\n\nfunction install {\n    installSQL\n    printdbg \"Certificates module installed\"\n}\n\nfunction uninstall {\n    printdbg \"Certificates module uninstalled\"\n}\n\nfunction main {\n    if [[ ${ENABLED} -eq 1 ]]; then\n        install && exit 0 || exit 1\n    elif [[ ${ENABLED} -eq -1 ]]; then\n        uninstall && exit 0 || exit 1\n    else\n        exit 0\n    fi\n}\n\nmain\n"
  },
  {
    "path": "gui/modules/custom_routing/custom_routing.sql",
    "content": "-- MySQL dump 10.13  Distrib 5.5.59, for debian-linux-gnu (x86_64)\n--\n-- Host: localhost    Database: kamailio\n-- ------------------------------------------------------\n-- Server version\t5.5.59-0+deb8u1\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!40101 SET NAMES utf8 */;\n/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n/*!40103 SET TIME_ZONE='+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n--\n-- Table structure for table `dr_custom_rules`\n--\n\nDROP TABLE IF EXISTS `dr_custom_rules`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dr_custom_rules` (\n  `dr_ruleid` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `locality` varchar(64) NOT NULL DEFAULT '',\n  `ppm` decimal(10,2) NOT NULL DEFAULT '0.00',\n  `description` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`dr_ruleid`),\n  CONSTRAINT `dr_custom_rules_ibfk_1` FOREIGN KEY (`dr_ruleid`) REFERENCES `dr_rules` (`ruleid`) ON DELETE CASCADE ON UPDATE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `locale_lookup`\n--\n\nDROP TABLE IF EXISTS `locale_lookup`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `locale_lookup` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `locale` varchar(64) NOT NULL DEFAULT '',\n  `fprefix` varchar(64) NOT NULL DEFAULT '0',\n  `tprefix` varchar(64) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;\n\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n\n-- Dump completed on 2018-03-31  0:10:33\n"
  },
  {
    "path": "gui/modules/custom_routing/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# ENABLED=1 --> install, ENABLED=0 --> do nothing, ENABLED=-1 uninstall\nENABLED=1\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction installSQL {\n    local TABLES=(dr_custom_rules locale_lookup)\n\n    printwarn \"Adding/Replacing the tables needed for Custom Routing  within dSIPRouter...\"\n\n    # Check to see if table exists\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql -sN -e \"select count(*) from ${TABLES[0]} limit 1\" > /dev/null 2>&1\n\n    if [ $? -eq 0 ]; then\n        printwarn \"The dSIPRouter tables ${TABLES[@]} already exists. Merging table data\"\n        (\n            cat ${DSIP_PROJECT_DIR}/gui/modules/custom_routing/custom_routing.sql;\n            withRootDBConn --db=\"$KAM_DB_NAME\" mysqldump --single-transaction --skip-triggers --skip-add-drop-table --no-create-info \\\n                --insert-ignore ${TABLES[@]};\n        ) | withRootDBConn --db=\"$KAM_DB_NAME\" mysql\n    else\n        echo -e \"Installing schema for custom routing\"\n        withRootDBConn --db=\"$KAM_DB_NAME\" mysql <${DSIP_PROJECT_DIR}/gui/modules/custom_routing/custom_routing.sql\n    fi\n}\n\nfunction install {\n    installSQL\n    printdbg \"Custom Routing module installed\"\n}\n\nfunction uninstall {\n    printdbg \"Custom Routing module uninstalled\"\n}\n\nfunction main {\n    if [[ ${ENABLED} -eq 1 ]]; then\n        install && exit 0 || exit 1\n    elif [[ ${ENABLED} -eq -1 ]]; then\n        uninstall && exit 0 || exit 1\n    else\n        exit 0\n    fi\n}\n\nmain\n"
  },
  {
    "path": "gui/modules/dnid_enrichment/dnid_enrichment.sql",
    "content": "DROP TABLE IF EXISTS dsip_dnid_enrich_lnp;\n/*!40101 SET @saved_cs_client = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE dsip_dnid_enrich_lnp (\n  id int(10) unsigned NOT NULL AUTO_INCREMENT,\n  dnid varchar(64) NOT NULL,\n  country_code varchar(64) NOT NULL DEFAULT '',\n  routing_number varchar(64) NOT NULL DEFAULT '',\n  description varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (id)\n) ENGINE = InnoDB\n  DEFAULT CHARSET = utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\nDROP TABLE IF EXISTS dsip_dnid_lnp_mapping;\nDROP VIEW IF EXISTS dsip_dnid_lnp_mapping;\n/*!40101 SET @saved_cs_client = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE VIEW dsip_dnid_lnp_mapping AS\n  SELECT dnid, CONCAT(country_code, routing_number) AS prefix, '0' AS key_type, '0' AS value_type\n  FROM dsip_dnid_enrich_lnp;\n/*!40101 SET character_set_client = @saved_cs_client */;\n"
  },
  {
    "path": "gui/modules/dnid_enrichment/install.sh",
    "content": "# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# ENABLED=1 --> install, ENABLED=0 --> do nothing, ENABLED=-1 uninstall\nENABLED=1\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction installSQL {\n    MERGE_DATA=0\n    count=$(withRootDBConn --db=\"$KAM_DB_NAME\" mysql -sN -e \"select count(*) from dsip_dnid_enrich_lnp limit 1\" 2> /dev/null)\n    if [ ${count:-0} -gt 0 ]; then\n        MERGE_DATA=1\n    fi\n\n    if [ ${MERGE_DATA} -eq 1 ]; then\n\t    printwarn \"The table already exists. Merging table data\"\n\t    (\n\t        cat ${DSIP_PROJECT_DIR}/gui/modules/dnid_enrichment/dnid_enrichment.sql;\n            withRootDBConn --db=\"$KAM_DB_NAME\" mysqldump --single-transaction --skip-triggers --skip-add-drop-table --no-create-info \\\n            --insert-ignore dsip_dnid_enrich_lnp\n        ) | withRootDBConn --db=\"$KAM_DB_NAME\" mysql\n    else\n        # Replace the api tables\n        printwarn \"Adding/Replacing the tables needed for DNID LNP Enrichment module within dSIPRouter...\"\n        withRootDBConn --db=\"$KAM_DB_NAME\" mysql <${DSIP_PROJECT_DIR}/gui/modules/dnid_enrichment/dnid_enrichment.sql\n    fi\n}\n\nfunction install {\n    installSQL\n    enableKamailioConfigAttrib 'WITH_DNID_LNP_ENRICHMENT' ${DSIP_KAMAILIO_CONFIG_FILE}\n    printdbg \"DNID LNP Enrichment module installed\"\n    return 0\n}\n\nfunction uninstall {\n    disableKamailioConfigAttrib 'WITH_DNID_LNP_ENRICHMENT' ${DSIP_KAMAILIO_CONFIG_FILE}\n    return 0\n}\n\nfunction main {\n    if [[ ${ENABLED} -eq 1 ]]; then\n        install && exit 0 || exit 1\n    elif [[ ${ENABLED} -eq -1 ]]; then\n        uninstall && exit 0 || exit 1\n    else\n        exit 0\n    fi\n}\n\nmain\n"
  },
  {
    "path": "gui/modules/domain/__init__.py",
    "content": ""
  },
  {
    "path": "gui/modules/domain/domain_mapping.sql",
    "content": "-- MySQL dump 10.16  Distrib 10.1.26-MariaDB, for debian-linux-gnu (x86_64)\n--\n-- Host: localhost    Database: kamailio\n-- ------------------------------------------------------\n-- Server version\t10.1.26-MariaDB-0+deb9u1\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!40101 SET NAMES utf8mb4 */;\n/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n/*!40103 SET TIME_ZONE='+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n--\n-- Table structure for table `dsip_multidomain_mapping`\n--\n\n-- TODO: db_password should be encrypted\nDROP TABLE IF EXISTS `dsip_multidomain_mapping`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_multidomain_mapping` (\n  `id` int(10) NOT NULL AUTO_INCREMENT,\n  `pbx_id` int(10) NOT NULL,\n  `db_host` varchar(255) NOT NULL,\n  `db_username` varchar(255) NOT NULL,\n  `db_password` varchar(255) NOT NULL,\n  `domain_list` varchar(255) NOT NULL DEFAULT '',\n  `domain_list_hash` varchar(255) NOT NULL DEFAULT '',\n  `attr_list` varchar(255) NOT NULL DEFAULT '',\n  `type` tinyint(3) NOT NULL DEFAULT '0',\n  `enabled` tinyint(1) NOT NULL DEFAULT '0',\n  `lastsync` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n  `syncstatus` tinyint(1) NOT NULL DEFAULT '0',\n  `syncerror` varchar(200) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `dsip_domain_mapping`\n--\n\nDROP TABLE IF EXISTS `dsip_domain_mapping`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_domain_mapping` (\n  `id` int(10) NOT NULL AUTO_INCREMENT,\n  `pbx_id` int(10) NOT NULL,\n  `domain_id` int(10) NOT NULL,\n  `attr_list` varchar(255) NOT NULL,\n  `type` tinyint(3) NOT NULL DEFAULT '0',\n  `enabled` tinyint(1) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;\n\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n\n-- Dump completed on 2018-08-28 22:04:51\n"
  },
  {
    "path": "gui/modules/domain/domain_routes.py",
    "content": "import sys\n\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nimport re\nfrom flask import request, Blueprint, render_template, redirect, session, url_for\nfrom sqlalchemy import exc as sql_exceptions\nfrom sqlalchemy.sql import text\nfrom werkzeug import exceptions as http_exceptions\nfrom modules.api.licensemanager.classes import WoocommerceError\nfrom modules.api.licensemanager.functions import getLicenseStatus\nfrom database import startSession, DummySession, Domain, DomainAttrs, Dispatcher, Gateways, Address\nfrom modules.api.api_routes import addEndpointGroups\nfrom shared import debugException, debugEndpoint, showError, strFieldsToDict, stripDictVals\nfrom util.ipc import STATE_SHMEM_NAME, getSharedMemoryDict\nimport settings\n\ndomains = Blueprint('domains', __name__)\n\n\n# Gateway to IP - A gateway can be a carrier or PBX\ndef gatewayIdToIP(pbx_id, db):\n    gw = db.query(Gateways).filter(Gateways.gwid == pbx_id).first()\n    if gw is not None:\n        return gw.address\n\n\ndef addDomain(domain, authtype, pbxs, notes, db):\n    # Create the domain because we need the domain id\n    PBXDomain = Domain(domain=domain, did=domain)\n    db.add(PBXDomain)\n    db.flush()\n\n    # Check if list of PBX's\n    if pbxs:\n        pbx_list = re.split(' |,', pbxs)\n    else:\n        pbx_list = []\n\n    # If list is found\n    if len(pbx_list) > 1 and authtype == \"passthru\":\n        pbx_id = pbx_list[0]\n    else:\n        # Else Single value was submitted\n        pbx_id = pbxs\n\n    # Implement Passthru authentication to the first PBX on the list.\n    # because Passthru Registration only works against one PBX\n    if authtype == \"passthru\":\n        PBXDomainAttr1 = DomainAttrs(did=domain, name='pbx_list', value=pbx_id)\n        PBXDomainAttr2 = DomainAttrs(did=domain, name='pbx_type', value=\"0\")\n        PBXDomainAttr3 = DomainAttrs(did=domain, name='created_by', value=\"0\")\n        PBXDomainAttr4 = DomainAttrs(did=domain, name='domain_auth', value=authtype)\n        PBXDomainAttr5 = DomainAttrs(did=domain, name='description', value=\"notes:{}\".format(notes))\n        PBXDomainAttr6 = DomainAttrs(did=domain, name='pbx_ip', value=gatewayIdToIP(pbx_id, db))\n\n        db.add(PBXDomainAttr1)\n        db.add(PBXDomainAttr2)\n        db.add(PBXDomainAttr3)\n        db.add(PBXDomainAttr4)\n        db.add(PBXDomainAttr5)\n        db.add(PBXDomainAttr6)\n\n    # MSTeams Support\n    elif authtype == \"msteams\":\n        # Set of MS Teams Proxies\n        msteams_endpoint_uris = ['{}:5061;transport=tls'.format(endpoint) for endpoint in settings.MSTEAMS_DNS_ENDPOINTS]\n\n        # Attributes to specify that the domain was created manually\n        PBXDomainAttr1 = DomainAttrs(did=domain, name='pbx_list', value=\"{}\".format(\",\".join(msteams_endpoint_uris)))\n        PBXDomainAttr2 = DomainAttrs(did=domain, name='pbx_type', value=\"3\")\n        PBXDomainAttr3 = DomainAttrs(did=domain, name='created_by', value=\"0\")\n        PBXDomainAttr4 = DomainAttrs(did=domain, name='domain_auth', value=authtype)\n        PBXDomainAttr5 = DomainAttrs(did=domain, name='description', value=\"notes:{}\".format(notes))\n        # Serial folking will be used to forward registration info to multiple PBX's\n        PBXDomainAttr6 = DomainAttrs(did=domain, name='dispatcher_alg_reg', value=\"4\")\n        PBXDomainAttr7 = DomainAttrs(did=domain, name='dispatcher_alg_in', value=\"4\")\n        # Create entry in dispatcher and set dispatcher_set_id in domain_attrs\n        PBXDomainAttr8 = DomainAttrs(did=domain, name='dispatcher_set_id', value=PBXDomain.id)\n\n        # Use the default MS Teams SIP Proxy List if one isn't defined\n        print(\"pbx list {}\".format(pbx_list))\n        if len(pbx_list) == 0 or pbx_list[0] == '':\n            # Logic to handle when dSIPRouter is behind NAT (aka servernat is enabled)\n            if settings.EXTERNAL_IP_ADDR != settings.INTERNAL_IP_ADDR:\n                socket_addr = settings.INTERNAL_IP_ADDR\n            else:\n                socket_addr = settings.EXTERNAL_IP_ADDR\n            for hostname, sipuri in zip(settings.MSTEAMS_DNS_ENDPOINTS,msteams_endpoint_uris):\n                dispatcher = Dispatcher(\n                    setid=PBXDomain.id,\n                    destination=sipuri,\n                    attrs=f'socket=tls:{socket_addr}:5061;ping_from=sip:{domain}',\n                    flags=Dispatcher.FLAGS['KEEP_ALIVE'],\n                    name=hostname\n                )\n                db.add(dispatcher)\n\n        db.add(PBXDomainAttr1)\n        db.add(PBXDomainAttr2)\n        db.add(PBXDomainAttr3)\n        db.add(PBXDomainAttr4)\n        db.add(PBXDomainAttr5)\n        db.add(PBXDomainAttr6)\n        db.add(PBXDomainAttr7)\n        db.add(PBXDomainAttr8)\n\n        # Check if the MSTeams IP(s) that send us OPTION messages is in the address table\n        for endpoint_ip in settings.MSTEAMS_IP_ENDPOINTS:\n            address_query = db.query(Address).filter(Address.ip_addr == endpoint_ip).first()\n            if address_query is None:\n                Addr = Address(\"msteams-sbc\", endpoint_ip, 32, settings.FLT_MSTEAMS, gwgroup=0)\n                db.add(Addr)\n\n        # Add Endpoint group to enable Inbound Mapping\n        endpointGroup = {\"name\": domain, \"endpoints\": None}\n        endpoints = []\n        for hostname in settings.MSTEAMS_DNS_ENDPOINTS:\n            address_query = db.query(Address).filter(Address.ip_addr == hostname).first()\n            if address_query is None:\n                Addr = Address(\"msteams-sbc\", hostname, 32, settings.FLT_MSTEAMS, gwgroup=0)\n                db.add(Addr)\n            hostname=\"{}:{};{}\".format(hostname,\"5061\",\"transport=tls\")\n            endpoints.append({\"host\": hostname, \"description\": \"msteams_endpoint\", \"maintmode\": False, \n            'keepalive': '1'})\n\n        endpointGroup['endpoints'] = endpoints\n        addEndpointGroups(endpointGroup, \"msteams\", domain)\n\n    # Implement external authentication to either Realtime DB or Local Subscriber table\n    else:\n        # Attributes to specify that the domain was created manually\n        PBXDomainAttr1 = DomainAttrs(did=domain, name='pbx_list', value=str(pbx_list))\n        PBXDomainAttr2 = DomainAttrs(did=domain, name='pbx_type', value=\"0\")\n        PBXDomainAttr3 = DomainAttrs(did=domain, name='created_by', value=\"0\")\n        PBXDomainAttr4 = DomainAttrs(did=domain, name='domain_auth', value=authtype)\n        PBXDomainAttr5 = DomainAttrs(did=domain, name='description', value=\"notes:{}\".format(notes))\n        # Serial folking will be used to forward registration info to multiple PBX's\n        PBXDomainAttr6 = DomainAttrs(did=domain, name='dispatcher_alg_reg', value=\"8\")\n        PBXDomainAttr7 = DomainAttrs(did=domain, name='dispatcher_alg_in', value=\"4\")\n        # Create entry in dispatcher and set dispatcher_set_id in domain_attrs\n        PBXDomainAttr8 = DomainAttrs(did=domain, name='dispatcher_set_id', value=PBXDomain.id)\n        for pbx_id in pbx_list:\n            dispatcher = Dispatcher(\n                setid=PBXDomain.id,\n                destination=gatewayIdToIP(pbx_id, db),\n                gwid=pbx_id\n            )\n            db.add(dispatcher)\n\n        db.add(PBXDomainAttr1)\n        db.add(PBXDomainAttr2)\n        db.add(PBXDomainAttr3)\n        db.add(PBXDomainAttr4)\n        db.add(PBXDomainAttr5)\n        db.add(PBXDomainAttr6)\n        db.add(PBXDomainAttr7)\n        db.add(PBXDomainAttr8)\n\n\n@domains.route(\"/domains/msteams/<int:id>\", methods=['GET'])\ndef configureMSTeams(id):\n    db = DummySession()\n\n    try:\n        if not session.get('logged_in'):\n            return redirect(url_for('index'))\n\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        license_status = getLicenseStatus(license_tag='DSIP_MSTEAMS')\n        if license_status == 0:\n            return render_template('license_required.html', msg=None)\n\n        if license_status == 1:\n            return render_template('license_required.html', msg='license is not valid, ensure your license is still active')\n\n        if license_status == 2:\n            return render_template('license_required.html', msg='license is associated with another machine, re-associate it with this machine first')\n\n        db = startSession()\n\n        domain_query = db.query(Domain).filter(Domain.id == id)\n        domain = domain_query.first()\n\n        return render_template('msteams.html', domainid=id, domain=domain)\n\n    except WoocommerceError as ex:\n        return render_template('license_required.html', msg=str(ex))\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        error = \"http\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except Exception as ex:\n        debugException(ex, log_ex=True, print_ex=True, showstack=True)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\n\n@domains.route(\"/domains\", methods=['GET'])\ndef displayDomains():\n    db = DummySession()\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        if not session.get('logged_in'):\n            return render_template('index.html', version=settings.VERSION)\n\n        # sql1 = \"select domain.id,domain.domain,dsip_domain_mapping.type,dsip_domain_mapping.pbx_id,dr_gateways.description from domain left join dsip_domain_mapping on domain.id = dsip_domain_mapping.domain_id left join dr_gateways on dsip_domain_mapping.pbx_id = dr_gateways.gwid;\"\n        sql1 = text(\"SELECT DISTINCT domain.did AS domain, domain.id, value as type FROM domain_attrs JOIN domain on domain.did = domain_attrs.did WHERE name='pbx_type'\")\n        res = db.execute(sql1)\n\n        sql2 = text(\"\"\"\n            SELECT distinct domain_attrs.did, pbx_list, domain_auth, creator, description FROM domain_attrs JOIN\n            ( SELECT did,value AS pbx_list FROM domain_attrs WHERE name='pbx_list' ) t1\n            ON t1.did=domain_attrs.did JOIN\n            ( SELECT did,value AS description FROM domain_attrs WHERE name='description' ) t2\n            ON t2.did=domain_attrs.did JOIN\n            ( SELECT did,value AS domain_auth FROM domain_attrs WHERE name='domain_auth' ) t3\n            ON t3.did=domain_attrs.did JOIN\n            ( SELECT did,description AS creator FROM domain_attrs LEFT JOIN dr_gw_lists ON domain_attrs.value = dr_gw_lists.id WHERE name='created_by') t4\n            ON t4.did=domain_attrs.did\n            \"\"\")\n        res2 = db.execute(sql2)\n\n        pbx_lookup = {}\n        for row in res2.mappings():\n            notes = strFieldsToDict(row[\"description\"])[\"notes\"] or ''\n            if row[\"creator\"] is not None:\n                name = strFieldsToDict(row[\"creator\"])[\"name\"] or ''\n            else:\n                name = \"Manually Created\"\n\n            pbx_lookup[row[\"did\"]] = {\n                'pbx_list': str(row[\"pbx_list\"].strip('[]')).replace(\"'\", \"\").replace(\",\", \", \"),\n                'domain_auth': row[\"domain_auth\"],\n                'name': name,\n                'notes': notes\n            }\n\n        license_status = getLicenseStatus(license_tag='DSIP_MSTEAMS')\n        if license_status == 0:\n            return render_template('domains.html', rows=res, pbxlookup=pbx_lookup, hc=False)\n\n        if license_status == 1:\n            return render_template('license_required.html', msg='license is not valid, ensure your license is still active')\n\n        if license_status == 2:\n            return render_template('license_required.html', msg='license is associated with another machine, re-associate it with this machine first')\n\n        return render_template('domains.html', rows=res, pbxlookup=pbx_lookup, hc=True)\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        error = \"http\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except Exception as ex:\n        debugException(ex, log_ex=True, print_ex=True, showstack=True)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\n\n@domains.route(\"/domains\", methods=['POST'])\ndef addUpdateDomain():\n    db = DummySession()\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        if not session.get('logged_in'):\n            return render_template('index.html')\n\n        form = stripDictVals(request.form.to_dict())\n\n        domain_id = form['domain_id'] if len(form['domain_id']) > 0 else ''\n        domainlist = form['domainlist'] if len(form['domainlist']) > 0 else ''\n        authtype = form['authtype'] if len(form['authtype']) > 0 else ''\n        pbxs = request.form['pbx_list'] if len(form['pbx_list']) > 0 else ''\n        notes = request.form['notes'] if len(form['notes']) > 0 else ''\n\n        # Adding\n        if len(domain_id) <= 0:\n            domains = [domain.strip() for domain in domainlist.split(\",\")]\n            for domain in domains:\n                addDomain(domain, authtype, pbxs, notes, db)\n        # Updating\n        else:\n            # remove old entries and add new ones\n            domain_query = db.query(Domain).filter(Domain.id == domain_id)\n            domain = domain_query.first()\n            if domain is not None:\n                db.query(DomainAttrs).filter(DomainAttrs.did == domain.did).delete(\n                    synchronize_session=False\n                )\n                db.query(Dispatcher).filter(Dispatcher.setid == domain.id).delete(\n                    synchronize_session=False\n                )\n                domain_query.delete(synchronize_session=False)\n                db.flush()\n\n            addDomain(domainlist.split(\",\")[0].strip(), authtype, pbxs, notes, db)\n\n        db.commit()\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return displayDomains()\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex, log_ex=True, print_ex=True, showstack=False)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        error = \"http\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except Exception as ex:\n        debugException(ex, log_ex=True, print_ex=True, showstack=False)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n\n\n@domains.route(\"/domainsdelete\", methods=['POST'])\ndef deleteDomain():\n    db = DummySession()\n\n    try:\n        if (settings.DEBUG):\n            debugEndpoint()\n\n        db = startSession()\n\n        if not session.get('logged_in'):\n            return render_template('index.html', version=settings.VERSION)\n\n        form = stripDictVals(request.form.to_dict())\n\n        domainid = form['domain_id'] if 'domain_id' in form else ''\n        domainname = form['domain_name'] if 'domain_name' in form else ''\n\n        dispatcherEntry = db.query(Dispatcher).filter(Dispatcher.setid == domainid)\n        domainAttrs = db.query(DomainAttrs).filter(DomainAttrs.did == domainname)\n        domainEntry = db.query(Domain).filter(Domain.did == domainname)\n\n        dispatcherEntry.delete(synchronize_session=False)\n        domainAttrs.delete(synchronize_session=False)\n        domainEntry.delete(synchronize_session=False)\n\n        db.commit()\n        getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True\n        return displayDomains()\n\n    except sql_exceptions.SQLAlchemyError as ex:\n        debugException(ex)\n        error = \"db\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except http_exceptions.HTTPException as ex:\n        debugException(ex)\n        error = \"http\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    except Exception as ex:\n        debugException(ex)\n        error = \"server\"\n        db.rollback()\n        db.flush()\n        return showError(type=error)\n    finally:\n        db.close()\n"
  },
  {
    "path": "gui/modules/domain/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# ENABLED=1 --> install, ENABLED=0 --> do nothing, ENABLED=-1 uninstall\nENABLED=1\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction installSQL {\n    local TABLES=(dsip_multidomain_mapping dsip_domain_mapping)\n\n    printwarn \"Adding/Replacing the tables needed for Domain Mapping within dSIPRouter...\"\n\n    # Check to see if table exists\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql -sN -e \"select count(*) from ${TABLES[0]} limit 1\" >/dev/null 2>&1\n\n    if [ $? -eq 0 ]; then\n        printwarn \"The dSIPRouter tables ${TABLES[@]} already exists. Merging table data\"\n        (\n            cat ${DSIP_PROJECT_DIR}/gui/modules/domain/domain_mapping.sql;\n            withRootDBConn --db=\"$KAM_DB_NAME\" mysqldump --single-transaction --skip-triggers --skip-add-drop-table --no-create-info \\\n            --insert-ignore ${TABLES[@]};\n        ) | withRootDBConn --db=\"$KAM_DB_NAME\" mysql\n    else\n        echo -e \"Installing schema for Domain Mapping\"\n        withRootDBConn --db=\"$KAM_DB_NAME\" mysql <${DSIP_PROJECT_DIR}/gui/modules/domain/domain_mapping.sql\n    fi\n}\n\nfunction install {\n    installSQL\n    printdbg \"Domain module installed\"\n}\n\nfunction uninstall {\n    printdbg \"Domain module uninstalled\"\n}\n\nfunction main {\n    if [[ ${ENABLED} -eq 1 ]]; then\n        install && exit 0 || exit 1\n    elif [[ ${ENABLED} -eq -1 ]]; then\n        uninstall && exit 0 || exit 1\n    else\n        exit 0\n    fi\n}\n\nmain\n"
  },
  {
    "path": "gui/modules/flowroute/__init__.py",
    "content": "import requests\nimport settings\n\n# TODO: automate route setup and ip auth config in flowroute\n\nclass Numbers():\n    \"\"\"\n    Contains methods for accessing the flowroute Numbers api\n    \"\"\"\n\n    def __init__(self):\n        self.auth = (settings.FLOWROUTE_ACCESS_KEY, settings.FLOWROUTE_SECRET_KEY)\n        self.api_url = settings.FLOWROUTE_API_ROOT_URL + \"/numbers\"\n\n    def __del__(self):\n        self.auth = None\n        self.api_url = None\n\n    def getNumbers(self, starts_with=None, contains=None, ends_with=None, limit=1000000, offset=None):\n        \"\"\"\n        Get flowroute DID's associated with accnt\n\n        .. seealso:: `flowroute list numbers <https://developer.flowroute.com/api/numbers/v2.0/list-account-phone-numbers/>`_\n        :param starts_with: match numbers starting with..\n        :param contains:    match numbers containing..\n        :param ends_with:   match numbers ending with..\n        :param limit:       limit of matched numbers\n        :param offset:      offsets list of numbers returned\n        :return:            list(*str)\n        \"\"\"\n        payload = {\n            'starts_with': starts_with,\n            'contains': contains,\n            'ends_with': ends_with,\n            'limit': limit,\n            'offset': offset\n        }\n        resp = requests.get(self.api_url, auth=self.auth, params=payload)\n        resp.raise_for_status()\n        return [num['attributes']['value'] for num in resp.json()['data']]\n"
  },
  {
    "path": "gui/modules/frauddetection/fraud.py",
    "content": "import sys\nfrom pyspark.sql import SQLContext\nfrom pyspark.sql import SparkSession\nfrom pyspark.ml.linalg import DenseVector\nfrom pyspark.sql import functions as F\nimport pyspark.sql.types as T\n\nspark = SparkSession \\\n        .builder \\\n        .appName(\"Python Spark SQL basic example\") \\\n        .config(\"spark.driver.extraClassPath\", \"/usr/share/java/mysql-connector-java.jar\") \\\n        .config(\"spark.executor.extraClassPath\", \"/usr/share/java/mysql-connector-java.jar\") \\\n\t.config(\"spark.pyspark.python\",\"/usr/bin/python3.6\") \\\n        .getOrCreate()\n\nsc = spark.sparkContext\n\nsqlContext = SQLContext(sc)\n\nurl = \"jdbc:mysql://localhost:3306/kamailio?user=kamailio;password=kamailiorw\"\n\ndf = sqlContext \\\n  .read \\\n  .format(\"jdbc\") \\\n  .option(\"url\", url) \\\n  .option(\"dbtable\", \"cdrs\") \\\n  .option(\"user\",\"kamailio\") \\\n  .option(\"password\", \"kamailiorw\") \\\n  .load()\n\ndf.printSchema()\n\ndf1 = df.select(\"fraud\",\"src_username\",\"dst_username\",\"call_start_time\")\n\ndf1.show()\n\ndef removeTechPrefix(col):\n   techprefix,dst_username = col.split(\"*\")\n   return dst_username\n\nmy_udf = F.UserDefinedFunction(removeTechPrefix, T.StringType())\n\ndf2=df1.withColumn(\"dst_username\",my_udf(df1.dst_username))\ndf2=df2.withColumn(\"call_start_time\",F.hour(df2.call_start_time))\n\ndf2.show()\n\ninput_data = df2.rdd.map(lambda x: (x[0],DenseVector(x[1:])))\n\ndf3 = spark.createDataFrame(input_data, [\"label\", \"features\"])\n\ndf3.show()\n"
  },
  {
    "path": "gui/modules/frauddetection/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# ENABLED=1 --> install, ENABLED=0 --> do nothing, ENABLED=-1 uninstall\nENABLED=0\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction installSQL {\n    echo \"\"\n}\n\nfunction install {\n    installSQL\n    printdbg \"Fraud Detection module installed\"\n}\n\nfunction uninstall {\n    printdbg \"Fraud Detection module uninstalled\"\n}\n\nfunction main {\n    if [[ ${ENABLED} -eq 1 ]]; then\n        install && exit 0 || exit 1\n    elif [[ ${ENABLED} -eq -1 ]]; then\n        uninstall && exit 0 || exit 1\n    else\n        exit 0\n    fi\n}\n\nmain\n"
  },
  {
    "path": "gui/modules/fusionpbx/dsiprouter-provisioner.conf",
    "content": "upstream fusionpbx {\n\tserver fusionpbx5.dsiprouter.net:443;\n\n}\n\n\n# handle the https requests\nserver {\n    # by default we listen on all interfaces\n    listen 80; \n    listen [::]:80;  \n    server_name _;\n\n    #ssl_certificate /etc/dsiprouter/certs/dsiprouter-cert.pem;\n    #ssl_certificate_key /etc/dsiprouter/certs/dsiprouter-key.pem;\n\n    location /provision/ {\n            proxy_pass http://fusionpbx;\n            proxy_redirect off;\n\t    proxy_set_header Host $host;\n            proxy_next_upstream error timeout http_404 http_403 http_500 http_502 http_503 http_504 non_idempotent;\n    }\n\t\n    location / {\n\n        error_page 404  /404.html;\n\n    }\n\n    location /images/ {\n\talias /etc/nginx/html/images/;\n    }\n\n    # enable the access log for debugging\n    # it doesn't log data between nginx and the upstream FusionPBX servers\n    # it only logs data between the phone and nginx\n    #access_log /var/log/nginx/dsiprouter-provisioner-access.log;\n}\n"
  },
  {
    "path": "gui/modules/fusionpbx/dsiprouter-provisioner.tpl",
    "content": "upstream fusionpbx {\n\t##SERVERLIST##\n}\n\n\n# handle the https requests\nserver {\n    # by default we listen on all interfaces\n    listen 443 ssl http2 so_keepalive=on; \n    listen [::]:443 ssl http2 so_keepalive=on;  \n    server_name _;\n\n    ssl_certificate /etc/dsiprouter/certs/dsiprouter-cert.pem;\n    ssl_certificate_key /etc/dsiprouter/certs/dsiprouter-key.pem;\n\n    location ~^(\\/app\\/provision\\/|\\/provision\\/) {\n            proxy_pass https://fusionpbx;\n            proxy_redirect off;\n\t    proxy_set_header Host $host;\n            proxy_next_upstream error timeout http_404 http_403 http_500 http_502 http_503 http_504 non_idempotent;\n    }\n\t\n    location / {\n\n        error_page 404  /404.html;\n\n    }\n\n    location /images/ {\n\talias /etc/nginx/html/images/;\n    }\n\n    # enable the access log for debugging\n    # it doesn't log data between nginx and the upstream FusionPBX servers\n    # it only logs data between the phone and nginx\n    #access_log /var/log/nginx/dsiprouter-provisioner-access.log;\n}\n"
  },
  {
    "path": "gui/modules/fusionpbx/dsiprouter.nginx",
    "content": "    upstream fusionpbx {\n\tserver fusionpbx5.dsiprouter.net:443;\n  \n  }\n\n\n    server {\n        listen 80;\n        listen 443 ssl;\n\n        ssl_certificate         /etc/ssl/certs/cert_combined.crt;\n        ssl_certificate_key     /etc/ssl/certs/cert.key;\n\n        location /provision/ {\n            proxy_pass https://fusionpbx;\n            proxy_redirect off;\n\t    proxy_next_upstream error timeout http_404 http_403 http_500 http_502 http_503 http_504 non_idempotent;\n        }\n\n\tlocation / {\n\t\n\terror_page 404\t/404.html;\n\n\t}\n\n\tlocation /images/ {\n\n\t    alias /etc/nginx/html/images/;\n\t}\n\t\n    }\n"
  },
  {
    "path": "gui/modules/fusionpbx/dsiprouter.nginx.tpl",
    "content": "    upstream fusionpbx {\n\t##SERVERLIST##  \n  }\n\n\n    server {\n        listen 80;\n        listen 443 ssl;\n\n        ssl_certificate         /etc/ssl/certs/cert_combined.crt;\n        ssl_certificate_key     /etc/ssl/certs/cert.key;\n\n        location /provision/ {\n            proxy_pass https://fusionpbx;\n            proxy_redirect off;\n\t    proxy_next_upstream error timeout http_404 http_403 http_500 http_502 http_503 http_504 non_idempotent;\n        }\n\n\tlocation / {\n\t\n\terror_page 404\t/404.html;\n\n\t}\n\n\tlocation /images/ {\n\n\t    alias /etc/nginx/html/images/;\n\t}\n\t\n    }\n"
  },
  {
    "path": "gui/modules/fusionpbx/fusionpbx_sync_functions.py",
    "content": "import os, psycopg2, hashlib, MySQLdb, shutil\nfrom util.security import AES_CTR\nfrom util.networking import safeUriToHost, hostToIP\nfrom modules.api.kamailio.functions import sendJsonRpcCmd\n\n# TODO: error handling here is pretty bad, we need to establish connection from main func and pass conn/cursors to sub funcs\n#       I implemented an exmaple in sync_needed() of proper connection / cursor handling, we need to move that to the entry func\n#       i.e. run_sync() should utilize the proper handling of the connections/cursors and pass them to sub functions\n\nFUSIONPBX_SYNC_LOCK = '/run/dsiprouter/fusionsync.lock'\n\n# Obtain a set of FusionPBX systems that contains domains that Kamailio will route traffic to.\ndef get_sources(db):\n    # Dictionary object to hold the set of source FusionPBX systems\n    sources = {}\n\n    # Kamailio Database Parameters\n    kam_hostname = db['hostname']\n    kam_username = db['username']\n    kam_password = db['password']\n    kam_database = db['database']\n\n    try:\n        db = MySQLdb.connect(host=kam_hostname, user=kam_username, passwd=kam_password, db=kam_database)\n        c = db.cursor()\n        c.execute(\n            \"\"\"select pbx_id,address as pbx_host,db_host,db_username,db_password,domain_list,domain_list_hash,attr_list,dsip_multidomain_mapping.type from dsip_multidomain_mapping join dr_gw_lists on dsip_multidomain_mapping.pbx_id=dr_gw_lists.id join dr_gateways on dr_gateways.gwid = dr_gw_lists.gwlist where enabled=1\"\"\")\n        results = c.fetchall()\n        db.close()\n        for row in results:\n            # Store the PBX_ID as the key and the entire row as the value\n            sources[row[1]] = row\n    except Exception as e:\n        print(str(e))\n\n    return sources\n\n\n# Will remove all of the fusionpbx domain data so that it can be rebuilt\ndef drop_fusionpbx_domains(source, dest):\n    # PBX Domain Mapping Parameters\n    pbx_domain_list = source[5]\n    pbx_attr_list = source[6]\n\n    # Kamailio Database Parameters\n    kam_hostname = dest['hostname']\n    kam_username = dest['username']\n    kam_password = dest['password']\n    kam_database = dest['database']\n\n    pbx_domain_list = list(map(int, filter(None, pbx_domain_list.split(\",\"))))\n    pbx_attr_list = list(map(int, filter(None, pbx_attr_list.split(\",\"))))\n\n    kam_conn = None\n    kam_curs = None\n\n    try:\n        kam_conn = MySQLdb.connect(host=kam_hostname, user=kam_username, passwd=kam_password, db=kam_database)\n        kam_curs = kam_conn.cursor()\n\n        if len(pbx_domain_list) > 0:\n            kam_curs.execute(\"\"\"DELETE FROM domain WHERE id IN({})\"\"\".format(pbx_domain_list))\n\n        if len(pbx_attr_list) > 0:\n            kam_curs.execute(\"\"\"DELETE FROM domain_attrs WHERE id IN({})\"\"\".format(pbx_attr_list))\n\n        kam_conn.commit()\n    except Exception as ex:\n        error = str(ex)\n        try:\n            kam_conn.rollback()\n            kam_curs.execute(\"update dsip_multidomain_mapping set syncstatus=4, lastsync=NOW(),syncerror='{}'\".format(error))\n            kam_conn.commit()\n        except:\n            pass\n        raise ex\n    finally:\n        if kam_curs is not None:\n            kam_curs.close()\n        if kam_conn is not None:\n            kam_conn.close()\n\ndef sync_db(source, dest):\n    # FusionPBX Database Parameters\n    pbx_id = source[0]\n    pbx_host = source[1]\n    if ':' in source[2]:\n        fpbx_hostname = source[2].split(':')[0]\n        fpbx_port = source[2].split(':')[1]\n    else:\n        fpbx_hostname = source[2]\n        fpbx_port = 5432\n    fpbx_username = source[3]\n    fpbx_password = source[4]\n    pbx_domain_list = source[5]\n    pbx_attr_list = source[6]\n    pbx_type = source[8]\n    fpbx_database = 'fusionpbx'\n\n    # Kamailio Database Parameters\n    kam_hostname = dest['hostname']\n    kam_username = dest['username']\n    kam_password = dest['password']\n    kam_database = dest['database']\n\n    domain_id_list = []\n    attr_id_list = []\n\n    fpbx_conn = None\n    fpbx_curs = None\n    kam_conn = None\n    kam_curs = None\n\n    try:\n        # Get a connection to Kamailio Server DB\n        kam_conn = MySQLdb.connect(host=kam_hostname, user=kam_username, passwd=kam_password, db=kam_database)\n        kam_curs = kam_conn.cursor()\n        # Delete existing domain for the pbx\n        pbx_domain_list_str = ''.join(str(e) for e in pbx_domain_list)\n        if len(pbx_domain_list_str) > 0:\n            query = \"delete from domain where id in ({})\".format(pbx_domain_list_str)\n            kam_curs.execute(query)\n            pbx_domain_list = ''\n\n        # Trying connecting to PostgresSQL database using a Trust releationship first\n        fpbx_conn = psycopg2.connect(dbname=fpbx_database, user=fpbx_username, host=fpbx_hostname, port=fpbx_port, password=fpbx_password)\n        if fpbx_conn is not None:\n            print(\"Connection to FusionPBX:{} database was successful\".format(fpbx_hostname))\n        fpbx_curs = fpbx_conn.cursor()\n        fpbx_curs.execute(\"\"\"select domain_name from v_domains where domain_enabled='true' and domain_name <> %s\"\"\",[hostToIP(fpbx_hostname)])\n        rows = fpbx_curs.fetchall()\n        if rows is not None:\n            counter = 0\n            domain_name_str = \"\"\n\n            for row in rows:\n                kam_curs.execute(\"\"\"insert ignore into domain (id,domain,did,last_modified) values (null,%s,%s,NOW())\"\"\",\n                          (row[0], row[0]))\n\n                if kam_curs.rowcount > 0:\n                    kam_curs.execute(\n                        \"\"\"SELECT AUTO_INCREMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN('domain') ORDER BY FIND_IN_SET(TABLE_NAME, 'domain')\"\"\")\n                    rows_left = kam_curs.fetchall()\n                    domain_id_list.append(str(rows_left[0][0] - 1))\n                # Delete all domain_attrs for the domain first\n                kam_curs.execute(\"\"\"delete from domain_attrs where did=%s\"\"\", [row[0]])\n                kam_curs.execute(\n                    \"\"\"insert ignore into domain_attrs (id,did,name,type,value,last_modified) values (null,%s,'pbx_ip',2,%s,NOW())\"\"\",\n                    (row[0], pbx_host))\n                kam_curs.execute(\n                    \"\"\"insert ignore into domain_attrs (id,did,name,type,value,last_modified) values (null,%s,'pbx_type',2,%s,NOW())\"\"\",\n\n                    (row[0], pbx_type))\n                kam_curs.execute(\n                    \"\"\"insert ignore into domain_attrs (id,did,name,type,value,last_modified) values (null,%s,'created_by',2,%s,NOW())\"\"\",\n                    (row[0], pbx_id))\n                kam_curs.execute(\n                    \"\"\"insert ignore into domain_attrs (id,did,name,type,value,last_modified) values (null,%s,'dispatcher_set_id',2,%s,NOW())\"\"\",\n                    (row[0], pbx_id))\n                kam_curs.execute(\n                    \"\"\"insert ignore into domain_attrs (id,did,name,type,value,last_modified) values (null,%s,'dispatcher_reg_alg',2,%s,NOW())\"\"\",\n                    (row[0], 4))\n                kam_curs.execute(\n\n                    \"\"\"insert ignore into domain_attrs (id,did,name,type,value,last_modified) values (null,%s,'domain_auth',2,%s,NOW())\"\"\",\n                    (row[0], 'passthru'))\n                kam_curs.execute(\n                    \"\"\"insert ignore into domain_attrs (id,did,name,type,value,last_modified) values (null,%s,'pbx_list',2,%s,NOW())\"\"\",\n                    (row[0], pbx_id))\n                kam_curs.execute(\n                    \"\"\"insert ignore into domain_attrs (id,did,name,type,value,last_modified) values (null,%s,'description',2,%s,NOW())\"\"\",\n                    (row[0], 'notes:'))\n                counter = counter + 1\n                domain_name_str += row[0]\n\n            # Convert to a string seperated by commas\n            domain_id_list = ','.join(domain_id_list)\n\n            if not pbx_domain_list:  # if empty string then this is the first set of domains\n                pbx_domain_list = domain_id_list\n            else:  # adding to an existing list of domains\n                pbx_domain_list = pbx_domain_list + \",\" + domain_id_list\n\n            print(\"[sync_db] String of domains: {}\".format(domain_name_str))\n\n            # Create Hash of the string\n            domain_name_str_hash = hashlib.md5(domain_name_str.encode('utf-8')).hexdigest()\n            print(\"[sync_db] Hashed String of domains: {}\".format(domain_name_str_hash))\n\n            kam_curs.execute(\n                \"\"\"update dsip_multidomain_mapping set domain_list=%s, domain_list_hash=%s,syncstatus=1, lastsync=NOW(),syncerror='' where pbx_id=%s\"\"\",\n                (pbx_domain_list, domain_name_str_hash, pbx_id))\n            kam_conn.commit()\n\n    except Exception as ex:\n        error = str(ex)\n        try:\n            kam_conn.rollback()\n            kam_curs.execute(\"update dsip_multidomain_mapping set syncstatus=4, lastsync=NOW(),syncerror='{}'\".format(error))\n            kam_conn.commit()\n        except:\n            pass\n        raise ex\n    finally:\n        if fpbx_conn is not None:\n            fpbx_conn.close()\n        if fpbx_curs is not None:\n            fpbx_curs.close()\n        if kam_curs is not None:\n            kam_curs.close()\n        if kam_conn is not None:\n            kam_conn.close()\n\n\ndef reloadkam():\n    try:\n        sendJsonRpcCmd('127.0.0.1', 'domain.reload')\n        return True\n    except:\n        return False\n\n\ndef update_nginx(sources):\n    print(\"Updating Nginx\")\n\n#    # Connect to docker\n#    client = docker.from_env()\n#    if client is not None:\n#        print(\"Got handle to docker\")\n#\n#    try:\n#        # If there isn't any FusionPBX sources then just shutdown the container\n#        if len(sources) < 1:\n#            containers = client.containers.list()\n#            for container in containers:\n#                if container.name == \"dsiprouter-nginx\":\n#                    # Stop the container\n#                    container.stop()\n#                    container.remove(force=True)\n#                    print(\"Stopped nginx container\")\n#            return\n#    except Exception as e:\n#        os.remove(FUSIONPBX_SYNC_LOCK)\n#        print(e)\n\n    # Create the Nginx file\n    try:\n        serverList = \"\"\n        for source in sources:\n            serverList += \"server \" + safeUriToHost(str(source)) + \":443;\\n\"\n\n        script_dir = os.path.dirname(os.path.abspath(__file__))\n    \n        # Build a config for the native Nginx instance\n\n        input = open(script_dir + \"/dsiprouter-provisioner.tpl\")\n        try:\n            output = open(\"/etc/nginx/sites-enabled/dsiprouter-provisioner.conf\", \"x\")\n        except FileExistsError:\n            output = open(\"/etc/nginx/sites-enabled/dsiprouter-provisioner.conf\", \"w\")\n        finally:\n            output.write(input.read().replace(\"##SERVERLIST##\", serverList))\n            output.close()\n\n        input.close()\n\n        # Restart Nginx\n        os.system('systemctl reload nginx')\n    \n    except Exception as e:\n        os.remove(FUSIONPBX_SYNC_LOCK)\n        print(e)\n\n#    Depricating the use of docker containers - logic will be removed during the next release\n#    # Check if dsiprouter-nginx is running. If so, reload nginx\n#\n#    containers = client.containers.list()\n#    print(\"past container list\")\n#    for container in containers:\n#        if container.name == \"dsiprouter-nginx\":\n#            # Execute a command to reload nginx\n#            container.exec_run(\"nginx -s reload\")\n#            print(\"Reloaded nginx\")\n#            return\n#\n#    # Start the container if one is not running\n#    try:\n#        print(\"trying to create a container\")\n#        host_volume_path = script_dir + \"/dsiprouter.nginx\"\n#        html_volume_path = script_dir + \"/html\"\n#        cert_volume_path = script_dir + \"/certs\"\n#        # host_volume_path = script_dir\n#        print(host_volume_path)\n#        # remove the container with a name of dsiprouter-nginx to avoid conflicts if it already exists\n#        containerList = client.containers.list('dsiprouter-nginx')\n#        containerFound = False\n#        for c in containerList:\n#            if c.name == \"dsiprouter-nginx\":\n#                containerFound = True\n#        if containerFound:\n#            print(\"dsiprouter-nginx found...about to remove it and recreate\")\n#            container = client.containers.get('dsiprouter-nginx')\n#            container.remove()\n#        client.containers.run(image='nginx:latest',\n#                              name=\"dsiprouter-nginx\",\n#                              ports={'80/tcp': '80/tcp', '443/tcp': '443/tcp'},\n#                              volumes={\n#                                  host_volume_path: {'bind': '/etc/nginx/conf.d/default.conf', 'mode': 'rw'},\n#                                  html_volume_path: {'bind': '/etc/nginx/html', 'mode': 'rw'},\n#                                  cert_volume_path: {'bind': '/etc/ssl/certs', 'mode': 'rw'}\n#                              },\n#                              detach=True)\n#        print(\"created a container\")\n#    except Exception as e:\n#        os.remove(FUSIONPBX_SYNC_LOCK)\n#        print(str(e))\n\n\ndef sync_needed(source, dest):\n    # FusionPBX Database Parameters\n    pbx_id = source[0]\n\n    pbx_host = source[1]\n    if ':' in source[2]:\n        fpbx_hostname = source[2].split(':')[0]\n        fpbx_port = source[2].split(':')[1]\n    else:\n        fpbx_hostname = source[2]\n        fpbx_port = 5432\n    fpbx_username = source[3]\n    fpbx_password = source[4]\n    pbx_domain_list = source[5]\n    pbx_domain_list_hash = source[6]\n    pbx_attr_list = source[7]\n    pbx_type = source[8]\n    fpbx_database = 'fusionpbx'\n\n    # Kamailio Database Parameters\n    kam_hostname = dest['hostname']\n    kam_username = dest['username']\n    kam_password = dest['password']\n    kam_database = dest['database']\n\n    domain_id_list = []\n    attr_id_list = []\n\n    need_sync = True\n\n    fpbx_conn = None\n    fpbx_curs = None\n    kam_conn = None\n    kam_curs = None\n\n    # Trying connecting to the databases\n    try:\n        # Get a connection to Kamailio Server DB\n        kam_conn = MySQLdb.connect(host=kam_hostname, user=kam_username, passwd=kam_password, db=kam_database)\n        kam_curs = kam_conn.cursor()\n        if kam_curs is not None:\n            print(\"[sync_needed] Connection to Kamailio DB: {} database was successful\".format(kam_hostname))\n\n        # Get a connection to the FusionPBX Server\n        fpbx_conn = psycopg2.connect(dbname=fpbx_database, user=fpbx_username, host=fpbx_hostname, port=fpbx_port, password=fpbx_password)\n        if fpbx_conn is not None:\n            print(\"[sync_needed] Connection to FusionPBX DB: {} database was successful\".format(fpbx_hostname))\n        fpbx_curs = fpbx_conn.cursor()\n        fpbx_curs.execute(\"\"\"select domain_name from v_domains where domain_enabled='true' and domain_name <> %s\"\"\",[hostToIP(fpbx_hostname)])\n        rows = fpbx_curs.fetchall()\n        if rows is not None:\n            domain_name_str = \"\"\n\n            # Build a string that contains all of the domains\n            for row in rows:\n                domain_name_str += row[0]\n\n            print(\"[sync_needed] String of domains: {}\".format(domain_name_str))\n\n            # Create Hash of the string\n            domain_name_str_hash = hashlib.md5(domain_name_str.encode('utf-8')).hexdigest()\n            print(\"[sync_needed] Hashed String of domains: {}\".format(domain_name_str_hash))\n            if domain_name_str_hash == pbx_domain_list_hash:\n                # Sync not needed.  Will update the syncstatus=2 to denote a domain change was not detected\n                kam_curs.execute(\"\"\"update dsip_multidomain_mapping set syncstatus=2, lastsync=NOW()\"\"\")\n                kam_conn.commit()\n                need_sync = False\n        else:\n            # No domains yet, so no need to sync\n            kam_curs.execute(\"\"\"update dsip_multidomain_mapping set syncstatus=3, lastsync=NOW()\"\"\")\n            kam_conn.commit()\n            need_sync = False\n\n        return need_sync\n\n    except Exception as e:\n        error = str(e)\n        print(error)\n        try:\n            kam_conn.rollback()\n            kam_curs.execute(\"update dsip_multidomain_mapping set syncstatus=4, lastsync=NOW(),syncerror='{}'\".format())\n            kam_conn.commit()\n        except:\n            pass\n    finally:\n        if fpbx_conn is not None:\n            fpbx_conn.close()\n        if fpbx_curs is not None:\n            fpbx_curs.close()\n        if kam_curs is not None:\n            kam_curs.close()\n        if kam_conn is not None:\n            kam_conn.close()\n\ndef run_sync(settings):\n    try:\n        # Set the system where sync'd data will be stored.\n        # The Kamailio DB in our case\n\n        # If already running - don't run\n        if os.path.exists(FUSIONPBX_SYNC_LOCK):\n            print(\"Already running\")\n            return\n        else:\n            f = open(FUSIONPBX_SYNC_LOCK, \"w+\")\n            f.close()\n\n        # need to decrypt password if encrypted\n        if isinstance(settings.KAM_DB_PASS, bytes):\n            kam_password = AES_CTR.decrypt(settings.KAM_DB_PASS)\n        else:\n            kam_password = settings.KAM_DB_PASS\n\n        dest = {}\n        dest['hostname'] = settings.KAM_DB_HOST\n        dest['username'] = settings.KAM_DB_USER\n        dest['password'] = kam_password\n        dest['database'] = settings.KAM_DB_NAME\n\n        # Get the list of FusionPBX's that needs to be sync'd\n        sources = get_sources(dest)\n\n        # Loop thru each FusionPBX system and start the sync\n        for key in sources:\n            if sync_needed(sources[key], dest):\n                #drop_fusionpbx_domains(sources[key], dest)\n                sync_db(sources[key], dest)\n            else:\n                print(\"[run_sync] No changes - no sync needed for source: {}\".format(sources[key][1]))\n\n        # Reload Kamailio\n        reloadkam()\n\n        # Update Nginx configuration file for HTTP Provisioning and start docker container if we have FusionPBX systems\n        # update_nginx(sources[key])\n        if sources is not None and len(sources) > 0:\n            sources = list(sources.keys())\n            update_nginx(sources)\n    except Exception as e:\n        print(str(e))\n    finally:\n        # Remove lock file\n        os.remove(FUSIONPBX_SYNC_LOCK)\n"
  },
  {
    "path": "gui/modules/fusionpbx/html/images/placeholder.txt",
    "content": ""
  },
  {
    "path": "gui/modules/fusionpbx/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# ENABLED=1 --> install, ENABLED=0 --> do nothing, ENABLED=-1 uninstall\nENABLED=1\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\n# TODO: replace docker workflow here by simply adding to default dsiprouter nginx configs\nfunction install {\n    local FUSIONPBX_DIR=\"${DSIP_PROJECT_DIR}/gui/modules/fusionpbx\"\n\n    case \"$DISTRO\" in\n        debian|ubuntu)\n            apt-get install -y apt-transport-https ca-certificates software-properties-common gnupg lsb-release\n\n            local DEB_ARCH=$(dpkg --print-architecture)\n            local DISTRO_CODENAME=$(lsb_release -cs)\n\n            mkdir -p /etc/apt/keyrings\n            curl -fsSL https://download.docker.com/linux/${DISTRO}/gpg | gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg\n            mkdir -p /etc/apt/sources.list.d\n            (cat <<EOF\ndeb [arch=${DEB_ARCH} signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${DISTRO} ${DISTRO_CODENAME} stable\n#deb-src [arch=${DEB_ARCH} signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${DISTRO} ${DISTRO_CODENAME} stable\nEOF\n            ) >/etc/apt/sources.list.d/docker.list\n            mkdir -p /etc/apt/preferences.d\n            (cat <<'EOF'\nPackage: *\nPin: origin download.docker.com\nPin-Priority: 1000\nEOF\n            ) >/etc/apt/preferences.d/docker.pref\n\n            apt-get update -y\n            apt-get install -y docker-ce\n\n            if (( $? != 0 )); then\n                printerr \"Failed installing Docker\"\n                return 1\n            fi\n            ;;\n        amzn)\n            yum install -y ca-certificates yum-utils device-mapper-persistent-data lvm2\n            amazon-linux-extras enable -y docker >/dev/null\n            yum clean -y metadata\n            yum install -y docker\n\n            if (( $? != 0 )); then\n                printerr \"Failed installing Docker\"\n                return 1\n            fi\n            ;;\n        rhel|almalinux|rocky)\n            dnf install -y dnf-utils device-mapper-persistent-data lvm2 ca-certificates\n    #        CA_CERT_DIR=$(dirname $(find / -name '*ca-bundle.crt'))\n    #        cp -f ${CA_CERT_DIR}/ca-bundle.crt ${CA_CERT_DIR}/ca-bundle.bak\n    #        curl http://curl.haxx.se/ca/cacert.pem -o ${CA_CERT_DIR}/ca-bundle.crt\n    #        update-ca-trust force-enable\n    #        update-ca-trust extract\n\n            dnf remove -y docker\\*\n            # docker.io does not provide support for x86_64 on rhel/alma/rocky\n            # instead we use the centos repo docker.io provides (binary compatible)\n            dnf config-manager -y --add-repo https://download.docker.com/linux/centos/docker-ce.repo\n            dnf config-manager -y --enable docker-ce-stable\n            dnf install -y docker-ce\n\n            if (( $? != 0 )); then\n                printerr \"Failed installing Docker\"\n                return 1\n            fi\n            ;;\n        *)\n            printerr \"Failed installing Docker, OS Distro not supported\"\n            return 1\n            ;;\n    esac\n\n    systemctl enable docker.service\n    systemctl start docker\n\n    firewall-cmd --permanent --zone=public --add-port=80/tcp\n    firewall-cmd --permanent --zone=public --add-port=443/tcp\n    firewall-cmd --reload\n\n    # Install Nginx container\n    docker create nginx\n    #docker run --name docker-nginx -p 80:80  -v ${FUSIONPBX_DIR}/dsiprouter.nginx:/etc/nginx/conf.d/default.conf  -d nginx\n\n    # Install a default self signed certificate for spinning up NGINX\n    mkdir -p ${DSIP_CERTS_DIR}/fusionpbx/ &&\n    openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 \\\n        -subj \"/C=US/ST=MI/L=Detroit/O=dopensource.com/CN=dSIPRouter\" \\\n        -keyout ${DSIP_CERTS_DIR}/fusionpbx/cert.key \\\n        -out ${DSIP_CERTS_DIR}/fusionpbx/cert_combined.crt\n\n    cronRemove -u dsiprouter 'dsiprouter_cron.py fusionpbx'\n    cronAppend -u dsiprouter \"*/1 * * * * ${PYTHON_CMD} ${DSIP_PROJECT_DIR}/gui/dsiprouter_cron.py fusionpbx sync\"\n\n    # Change to dsiprouter group so that the FusionPBX provisioning configuation can be written\n    chown root:dsiprouter /etc/nginx/sites-enabled\n    chmod 775 /etc/nginx/sites-enabled\n    \n    printdbg \"FusionPBX module installed\"\n    return 0\n}\n\nfunction uninstall {\n\t# Forcefully stop all docker containers and remove them\n\tdocker ps -a -q > /dev/null\n\tif [ $? == 1 ]; then\n\t\tdocker rm -f $(docker ps -a -q) > /dev/null\n\t\tprintdbg \"Stopped and removed all docker containers\"\n\telse\n\t\tprintwarn \"No docker containers to remove\"\n\tfi\n\n    case \"$DISTRO\" in\n        debian|ubuntu)\n            # Can't remove packages because it removes python3-pip package\n            #apt-get remove -y \\\n            #apt-transport-https \\\n            #ca-certificates\n            #software-properties-common\n\n            # Remove Docker Engine\n            apt-get remove -y docker-ce\n\n            # remove the docker repo\n            rm -f /etc/apt/keyrings/docker.gpg /etc/apt/sources.list.d/docker.list /etc/apt/preferences.d/docker.pref\n            apt-get update -y\n            ;;\n        amzn)\n            yum remove -y docker\n            amazon-linux-extras disable -y docker >/dev/null\n            yum clean -y metadata\n            ;;\n        rhel|almalinux|rocky)\n            #yum remove -y ca-certificates\n            #yum remove -y device-mapper-persistent-data lvm2\n            yum remove -y docker-ce\n\n            # Remove the repos\n            rm -f /etc/yum.repos.d/docker-ce*\n            yum clean all\n            ;;\n    esac\n\n    firewall-cmd --permanent --zone=public --remove-port=80/tcp\n    firewall-cmd --permanent --zone=public --remove-port=443/tcp\n    firewall-cmd --reload\n\n    rm -rf ${DSIP_CERTS_DIR}/fusionpbx/\n    cronRemove -u dsiprouter 'dsiprouter_cron.py fusionpbx'\n\n    printdbg \"FusionPBX module uninstalled\"\n    return 0\n}\n\nfunction main {\n    if (( ${ENABLED} == 1 )); then\n        install && exit 0 || exit 1\n    elif (( ${ENABLED} == -1 )); then\n        uninstall && exit 0 || exit 1\n    else\n        exit 0\n    fi\n}\n\nmain\n"
  },
  {
    "path": "gui/modules/upgrade/__init__.py",
    "content": "import re, requests\n\nimport settings\n\n\nclass UpdateUtils():\n    @staticmethod\n    def get_repo_version_list():\n        headers = {\n            'Accept': 'application/vnd.github+json'\n        }\n        params = {\n            \"per_page\": 100\n        }\n        r = requests.get(settings.GIT_RELEASE_URL, params=params, headers=headers)\n        return r.json()\n\n    @staticmethod\n    def get_latest_version():\n        latest = {\n            'tag_name': '',\n            'ver_num': 0\n        }\n        for rel in UpdateUtils.get_repo_version_list():\n            tag_name = rel['tag_name']\n            ver_num = float(re.sub(r'^v([0-9]+\\.[0-9]+).*?$', r'\\1', tag_name, flags=re.MULTILINE))\n            if ver_num > latest['ver_num']:\n                latest = {\n                    'tag_name': tag_name,\n                    'ver_num': ver_num\n                }\n        return latest\n"
  },
  {
    "path": "gui/requirements.txt",
    "content": "UltraDict\nacme\nansi2html\nbjoern\nbson\ncron_descriptor\ncryptography\ndnspython\ndocker\ndocutils<0.17,>=0.12\nFlask~=2.2.0\nFlask_WTF\nitsdangerous==2.0.1\nJinja2==3.1.3\njosepy\nmyst-parser\nmysqlclient\npem\npsycopg2\npsycopg2_binary\npycryptodome\npygtail\nPyMySQL\npyOpenSSL\npython-ldap==3.4.4\npython_crontab\npytz\nrecommonmark\nrequests\nsphinx\nsphinxcontrib-httpdomain\nsphinx-rtd-theme\npiccolo_theme\nSQLAlchemy~=2.0\ntwilio\nWerkzeug~=2.0\n"
  },
  {
    "path": "gui/settings.py",
    "content": "################ Database-Backed Settings ################\n# settings in this section are synced with the DB\n\n# dSIPRouter settings\n# dSIPRouter will need to be restarted for any changes to take effect - except settings that can be hot reloaded\n# for more information on hot reloading and shared memory via IPC see shared.updateConfig() and dsiprouter.syncSettings()\n\nDSIP_ID = None\nDSIP_CLUSTER_ID = 1\nDSIP_CLUSTER_SYNC = False\nDSIP_PROTO = 'https'\nDSIP_PORT = '5000'\nDSIP_USERNAME = 'admin'\nDSIP_PASSWORD = 'admin'\nDSIP_API_TOKEN = 'admin'\nDSIP_API_PROTO = 'https'\nDSIP_API_PORT = 5000\nDSIP_PRIV_KEY = '/etc/dsiprouter/privkey'\nDSIP_PID_FILE = '/run/dsiprouter/dsiprouter.pid'\nDSIP_UNIX_SOCK = '/run/dsiprouter/dsiprouter.sock'\nDSIP_IPC_SOCK = '/run/dsiprouter/ipc.sock'\nDSIP_IPC_PASS = 'admin'\n\n# dsiprouter logging settings\n# syslog level and facility values based on:\n# <http://www.nightmare.com/squirl/python-ext/misc/syslog.py>\n\nDSIP_LOG_LEVEL = 3\nDSIP_LOG_FACILITY = 18\n\n# dSIPRouter SSL settings\n# ssl key / cert are absolute paths\n# email for re-certification must match certs\nDSIP_SSL_KEY = '/etc/dsiprouter/certs/dsiprouter-key.pem'\nDSIP_SSL_CERT = '/etc/dsiprouter/certs/dsiprouter-cert.pem'\nDSIP_SSL_CA = '/etc/dsiprouter/certs/ca-list.pem'\nDSIP_SSL_EMAIL = 'admin@sbc4.customers.dsiprouter.net'\nDSIP_CERTS_DIR = '/etc/dsiprouter/certs'\n\n# dSIPRouter internal settings\n\nVERSION = '0.78'\nDEBUG = False\n# '' (default)  = handle inbound with domain mapping from endpoints, inbound from carriers and outbound to carriers\n# 'outbound'    = act as an outbound proxy only (no domain routing)\n# 'inout'       = inbound from carriers and outbound to carriers only (no domain routing)\nROLE = ''\nGUI_INACTIVE_TIMEOUT = 20\n\n# MySQL settings for kamailio\n\n# Database cluster\n#KAM_DB_HOST = ['64.129.84.11','64.129.84.12','50.237.20.11','50.237.20.12']\n# Single Host\nKAM_DB_HOST = 'localhost'\n# Database Engine Driver to connect with (leave empty for default)\n# supported drivers:    mysqldb | pymysql\n# see sqlalchemy docs for more info: <https://docs.sqlalchemy.org/en/latest/core/engines.html>\nKAM_DB_DRIVER = ''\nKAM_DB_TYPE = 'mysql'\nKAM_DB_PORT = '3306'\nKAM_DB_NAME = 'kamailio'\nKAM_DB_USER = 'kamailio'\nKAM_DB_PASS = 'kamailiorw'\n\nKAM_KAMCMD_PATH = '/usr/sbin/kamcmd'\nKAM_CFG_PATH = '/etc/kamailio/kamailio.cfg'\nKAM_TLSCFG_PATH = '/etc/kamailio/tls.cfg'\nRTP_CFG_PATH = '/etc/rtpengine/rtpengine.conf'\n\n# These constants shouldn't be modified\n# FLT_CARRIER/FLT_PBX/FLT_MSTEAMS:  type in dr_gateway table\n# FLT_OUTBOUND/FLT_INBOUND:         groupid in dr_rules table\n# FLT_LCR_MIN/FLT_FWD_MIN:          range of groupid in dr_rules table\nFLT_CARRIER = 8\nFLT_PBX = 9\nFLT_MSTEAMS = 17\nFLT_OUTBOUND = 8000\nFLT_INBOUND = 9000\nFLT_LCR_MIN = 10000\nFLT_FWD_MIN = 20000\n\n# The domain used to create user accounts for PBX and Endpoint registrations\nDEFAULT_AUTH_DOMAIN = 'sip.dsiprouter.org'\n\n# Teleblock Settings\nTELEBLOCK_GW_ENABLED = 0\nTELEBLOCK_GW_IP = '62.34.24.22'\nTELEBLOCK_GW_PORT = '5066'\nTELEBLOCK_MEDIA_IP = ''\nTELEBLOCK_MEDIA_PORT = ''\n\n# Flowroute API Settings\n# TODO: encrypt/decrypt these creds instead of storing in plaintext\nFLOWROUTE_ACCESS_KEY = ''\nFLOWROUTE_SECRET_KEY = ''\nFLOWROUTE_API_ROOT_URL = 'https://api.flowroute.com/v2'\n\n# Homer settings\nHOMER_ID = None\nHOMER_HEP_HOST = ''\nHOMER_HEP_PORT = 9060\n\n# Network Settings\n# possible network modes:\n# 0:    (full-auto)     dynamically update all network settings, updated on startup\n# 1:    (manual)        user sets all network settings manually, interfaces are ignored\n# 2:    (dmz)           internal/external IP/subnet resolved from public/private interfaces, all other settings as in mode 0\nNETWORK_MODE = 0\nIPV6_ENABLED = False\n# example: 192.168.0.1\nINTERNAL_IP_ADDR = ''\n# example: 192.168.0.1/24\nINTERNAL_IP_NET = ''\n# example: 2604:a880:400:d0::2048:3001\nINTERNAL_IP6_ADDR = ''\n# example: 2604:a880:400:d0::2048:3001/64\nINTERNAL_IP6_NET = ''\n# example: sip.dsiprouter.org\nINTERNAL_FQDN = ''\n# example: 1.1.1.1\nEXTERNAL_IP_ADDR = ''\n# example: 2604:a880:400:d0::2048:3001\nEXTERNAL_IP6_ADDR = ''\n# example: sip.dsiprouter.org\nEXTERNAL_FQDN = ''\n# example: eth0\nPUBLIC_IFACE = ''\n# example: eth1\nPRIVATE_IFACE = ''\n\n# upload folder for files\nUPLOAD_FOLDER = '/tmp'\n\n# email server config\nMAIL_SERVER = 'smtp.gmail.com'\nMAIL_PORT = 587\nMAIL_USE_TLS = True\nMAIL_USERNAME = ''\nMAIL_PASSWORD = ''\nMAIL_ASCII_ATTACHMENTS = False\nMAIL_DEFAULT_SENDER = 'dSIPRouter <donotreply@sip.dsiprouter.org>'\nMAIL_DEFAULT_SUBJECT = 'dSIPRouter System Notification'\n\n# dSIPRouter licenses associated with this node\n# stored as hash_str: key_combo\nDSIP_LICENSE_STORE = {}\n\n# rtpengine settings\nRTPENGINE_URI = 'udp:localhost:7722'\n\n################# End DB-Backed Settings #################\n\n################# Local-Only Settings ####################\n# settings in this section are not stored on the DB\n\n# the key used by the flask session manager\nDSIP_SESSION_KEY = None\n\n# the fqdn / ip address used by uac/nathelper modules when contacting other servers\nUAC_REG_ADDR = ''\n\n# Cloud Platform\n# The cloud platform the dSIPRouter is installed on\n# The installer will update this\n# '' = other or bare metal install\n# AWS = Amazon Web Services, GCP = Google Cloud Platform, AZURE = Microsoft Azure, DO = Digital Ocean, VULTR = Vultr Cloud\nCLOUD_PLATFORM = ''\n\n# backup settings\nBACKUP_FOLDER = '/var/backups/dsiprouter'\n\n# TransNexus Settings\n# TODO: marked for review, these settings should be synced across cluster in the DB\nTRANSNEXUS_AUTHSERVICE_ENABLED = 0\nTRANSNEXUS_AUTHSERVICE_HOST = 'outbound.sip.clearip.com:5060'\nTRANSNEXUS_VERIFYSERVICE_ENABLED = 0\nTRANSNEXUS_VERIFYSERVICE_HOST = 'inbound.sip.clearip.com:5060'\n\n# STIR/SHAKEN Settings\n# TODO: marked for review, these settings should be synced across cluster in the DB\nSTIR_SHAKEN_ENABLED = 0\nSTIR_SHAKEN_PREFIX_A = ''\nSTIR_SHAKEN_PREFIX_B = ''\nSTIR_SHAKEN_PREFIX_C = ''\nSTIR_SHAKEN_PREFIX_INVALID = ''\nSTIR_SHAKEN_BLOCK_INVALID = 0\nSTIR_SHAKEN_CERT_URL = ''\nSTIR_SHAKEN_KEY_PATH = ''\n\n# where the project was installed\nDSIP_PROJECT_DIR = '/opt/dsiprouter'\n# where the dsip docs are served from\nDSIP_DOCS_DIR = '/opt/dsiprouter/docs/build/html'\n\n# dr_routing prefix matching supported characters\n# refer to: <https://kamailio.org/docs/modules/5.1.x/modules/drouting.html#idp26708356>\nDID_PREFIX_ALLOWED_CHARS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '#', '*'}\n\n# micosoft teams settings\n# pick one of the\nMSTEAMS_DNS_ENDPOINTS = [\"sip.pstnhub.microsoft.com\",\"sip2.pstnhub.microsoft.com\",\"sip3.pstnhub.microsoft.com\"]\n#MSTEAMS_DNS_ENDPOINTS = [\"sip.pstnhub.dod.teams.microsoft.us\",\"sip.pstnhub.gov.teams.microsoft.us\"]\nMSTEAMS_IP_ENDPOINTS = [\"52.114.148.0\",\"52.114.132.46\",\"52.114.75.24\",\"52.114.76.76\",\"52.114.7.24\",\"52.114.14.70\",\"52.114.32.169\"]\n#MSTEAMS_IP_ENDPOINTS = [\"52.127.64.33\",\"52.127.88.59\",\"52.127.64.34\",\"52.127.92.64\"]\n\n# kamailio jsonrpc settings\nKAM_JSONRPC_ROOTPATH = '/api/kamailio'\nKAM_JSONRPC_VERSION = '2.0'\nKAM_JSONRPC_ID = 1\nKAM_JSONRPC_TIMEOUT = 15\nKAM_JSONRPC_RETRYIVAL = 1.0\n\n# root DB credentials\nROOT_DB_HOST = 'localhost'\nROOT_DB_PORT = ''\nROOT_DB_USER = 'root'\nROOT_DB_PASS = ''\nROOT_DB_NAME = 'mysql'\n\n# Where to sync settings from\n# file  - load from setting.py file\n# db    - load from dsip_settings table\nLOAD_SETTINGS_FROM = 'file'\n\n# where upgrades will be pulled from\nGIT_REPO_URL = 'https://github.com/dOpensource/dsiprouter.git'\nGIT_RELEASE_URL = 'https://api.github.com/repos/dOpensource/dsiprouter/releases'\n\n# auth modules\n# a dictionary of authentication modules to load and their corresponding settings\n# example for ldap module:\n# AUTH_MODULES = {\"ldap\": {\"LDAP_HOST\":\"ldap://ldap.dopensource.com\", \"USER_SEARCH_BASE\":\"ou=People,dc=dopensource,dc=com\", \"GROUP_SEARCH_BASE\":\"dc=dopensource,dc=com\", \"GROUP_MEMBER_ATTRIBUTE\":\"memberUid\", \"REQUIRED_GROUP\":\"support\", \"USER_ATTRIBUTE\":\"uid\"}}\nAUTH_MODULES = {}\n############### End Local-Only Settings ##################\n"
  },
  {
    "path": "gui/shared.py",
    "content": "# make sure the generated source files are imported instead of the template ones\nimport sys\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nimport os, re, json, socket, logging, traceback, inspect, ssl\nfrom calendar import monthrange\nfrom importlib import reload\nfrom flask import request, render_template, make_response\nfrom werkzeug.utils import escape\nfrom werkzeug.urls import iri_to_uri\nimport settings\n\n\ndef isCertValid(hostname, externalip, port=5061):\n    \"\"\" Returns true if the hostname has a valid cert\"\"\"\n\n    result = {\"tls_cert_valid\": False, \"tls_cert_details\": \"\", \"tls_error\": \"\"}\n\n    try:\n        context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=settings.DSIP_SSL_CA)\n        context.load_cert_chain(certfile=settings.DSIP_SSL_CERT, keyfile=settings.DSIP_SSL_KEY)\n\n        conn = context.wrap_socket(socket.socket(socket.AF_INET), server_hostname=hostname)\n        conn.connect((externalip, port))\n        # print(\"SSL established. Peer: {}\".format(conn.getpeercert()))\n        cert = conn.getpeercert()\n        result['tls_cert_details'] = cert\n        #ssl.match_hostname(cert, hostname)\n        result['tls_cert_valid'] = True\n        return result\n    except Exception as ex:\n        result['tls_error'] = str(ex)\n        # Return valid even if the cert can't be validated.  We just want to validate\n        # the ability to connect using the cert.\n        return result\n\ndef objToDict(obj):\n    \"\"\"\n    converts an arbitrary object to dict\n    \"\"\"\n    if isinstance(obj, dict):\n        return obj\n\n    vals = []\n    for attr in dir(obj):\n        if attr.startswith('__'):\n            continue\n        val = getattr(obj, attr)\n        if inspect.ismethod(val) or inspect.isfunction(val) or inspect.isbuiltin(val):\n            continue\n        vals.append((attr, val))\n    return dict(vals)\n\ndef rowToDict(row):\n    \"\"\"\n    converts sqlalchemy row object to python dict\n    does not recurse through relationships\n    tries table data, then _asdict() method, then objToDict()\n    \"\"\"\n    d = {}\n\n    if hasattr(row, '__table__'):\n        for column in row.__table__.columns:\n            d[column.name] = str(getattr(row, column.name))\n    elif hasattr(row, '_asdict'):\n        d = row._asdict()\n    elif hasattr(row, '__dict__'):\n        d = row.__dict__\n        d.pop('_sa_instance_state', None)\n    else:\n        d = objToDict(row)\n\n    return d\n\n# TODO: we are letting these functions error out until the DB relations are refactored to support these delimiters\ndef strFieldsToDict(fields_str, delims=(',', ':')):\n    return dict(\n        item.split(delims[1], maxsplit=1) for item in fields_str.split(delims[0])\n    )\n\ndef dictToStrFields(fields_dict, delims=(',', ':')):\n    return delims[0].join(\n        f'{k}{delims[1]}{v}' for k, v in fields_dict.items()\n    )\n\ndef updateConfig(config_obj, field_dict, hot_reload=False):\n    \"\"\"\n    Update a python config module\n    :param config_obj:\n    :param field_dict:\n    :return:\n    \"\"\"\n    config_file = \"<no filepath available>\"\n    try:\n        config_file = config_obj.__file__\n\n        with open(config_file, 'r+') as config:\n            config_str = config.read()\n            for key, val in field_dict.items():\n                regex = r\"^(?!#)(?:\" + re.escape(key) + \\\n                        r\")[ \\t]*=[ \\t]*(?:\\w+\\(.*\\)[ \\t\\v]*$|[\\w\\d\\.]+[ \\t]*$|\\{.*\\}|\\[.*\\][ \\t]*$|\\(.*\\)[ \\t]*$|b?\\\"\\\"\\\".*\\\"\\\"\\\"[ \\t]*$|b?'''.*'''[ \\v]*$|b?\\\".*\\\"[ \\t]*$|b?'.*')\"\n                replace_str = \"{} = {}\".format(key, repr(val))\n                config_str = re.sub(regex, replace_str, config_str, flags=re.MULTILINE)\n\n            config.seek(0)\n            config.write(config_str)\n            config.truncate()\n\n        if hot_reload:\n            globals()[config_obj.__name__] = reload(config_obj)\n    except:\n        IO.logerr('Problem updating the {0} configuration file'.format(config_file))\n\ndef stripDictVals(d):\n    for key, val in d.items():\n        if isinstance(val, str):\n            d[key] = val.strip()\n        elif isinstance(val, int):\n            d[key] = int(str(val).strip())\n    return d\n\ndef getCustomRoutes():\n    \"\"\" Return custom kamailio routes from config file \"\"\"\n    custom_routes = []\n    with open(settings.KAM_CFG_PATH, 'rb') as kamcfg_file:\n        kamcfg_bytes = kamcfg_file.read()\n\n        regex = rb'''CUSTOM_ROUTING_START.*CUSTOM_ROUTING_END'''\n        custom_routes_bytes = re.search(regex, kamcfg_bytes, flags=re.MULTILINE | re.DOTALL).group(0)\n\n        regex = rb'''^route\\[(\\w+)\\]'''\n        matches = re.finditer(regex, custom_routes_bytes, flags=re.MULTILINE)\n\n        for matchnum, match in enumerate(matches):\n            if len(match.groups()) > 0:\n                custom_routes.append(match.group(1).decode('utf-8'))\n\n    return custom_routes\n\ndef monthdelta(dt, delta):\n    \"\"\" Return dt with a delta month change (neg or pos) \"\"\"\n    m, y = (dt.month + delta) % 12, dt.year + (dt.month + delta - 1) // 12\n    if m == 0:\n        m = 12\n    d = min(dt.day, monthrange(y, m)[1])\n    return dt.replace(day=d, month=m, year=y)\n\n# modified method from Python cookbook, #475186\ndef supportsColor(stream):\n    \"\"\" Return True if terminal supports ASCII color codes  \"\"\"\n    if not hasattr(stream, \"isatty\") or not stream.isatty():\n        # auto color only on TTYs\n        return False\n    try:\n        import curses\n        curses.setupterm()\n        return curses.tigetnum(\"colors\") > 2\n    except:\n        # guess false in case of error\n        return False\n\nclass IO():\n    \"\"\" Contains static methods for handling i/o operations \"\"\"\n\n    if supportsColor(sys.stdout):\n        @staticmethod\n        def printerr(message):\n            print('\\x1b[1;31m' + str(message).strip() + '\\x1b[0m')\n\n        @staticmethod\n        def printinfo(message):\n            print('\\x1b[1;32m' + str(message).strip() + '\\x1b[0m')\n\n        @staticmethod\n        def printwarn(message):\n            print('\\x1b[1;33m' + str(message).strip() + '\\x1b[0m')\n\n        @staticmethod\n        def printdbg(message):\n            print('\\x1b[1;34m' + str(message).strip() + '\\x1b[0m')\n\n        @staticmethod\n        def printbold(message):\n            print('\\x1b[1;37m' + str(message).strip() + '\\x1b[0m')\n\n        @staticmethod\n        def logcrit(message):\n            logging.getLogger().log(logging.CRITICAL, '\\x1b[1;31m' + str(message).strip() + '\\x1b[0m')\n\n        @staticmethod\n        def logerr(message):\n            logging.getLogger().log(logging.ERROR, '\\x1b[1;31m' + str(message).strip() + '\\x1b[0m')\n\n        @staticmethod\n        def loginfo(message):\n            logging.getLogger().log(logging.INFO, '\\x1b[1;32m' + str(message).strip() + '\\x1b[0m')\n\n        @staticmethod\n        def logwarn(message):\n            logging.getLogger().log(logging.WARNING, '\\x1b[1;33m' + str(message).strip() + '\\x1b[0m')\n\n        @staticmethod\n        def logdbg(message):\n            logging.getLogger().log(logging.DEBUG, '\\x1b[1;34m' + str(message).strip() + '\\x1b[0m')\n\n        @staticmethod\n        def lognolvl(message):\n            logging.getLogger().log(logging.NOTSET, '\\x1b[1;37m' + str(message).strip() + '\\x1b[0m')\n\n    else:\n        @staticmethod\n        def printerr(message):\n            print(str(message).strip())\n\n        @staticmethod\n        def printinfo(message):\n            print(str(message).strip())\n\n        @staticmethod\n        def printwarn(message):\n            print(str(message).strip())\n\n        @staticmethod\n        def printdbg(message):\n            print(str(message).strip())\n\n        @staticmethod\n        def printbold(message):\n            print(str(message).strip())\n\n        @staticmethod\n        def logcrit(message):\n            logging.getLogger().log(logging.CRITICAL, str(message).strip())\n\n        @staticmethod\n        def logerr(message):\n            logging.getLogger().log(logging.ERROR, str(message).strip())\n\n        @staticmethod\n        def loginfo(message):\n            logging.getLogger().log(logging.INFO, str(message).strip())\n\n        @staticmethod\n        def logwarn(message):\n            logging.getLogger().log(logging.WARNING, str(message).strip())\n\n        @staticmethod\n        def logdbg(message):\n            logging.getLogger().log(logging.DEBUG, str(message).strip())\n\n        @staticmethod\n        def lognolvl(message):\n            logging.getLogger().log(logging.NOTSET, str(message).strip())\n\ndef debugException(ex=None, log_ex=True, print_ex=True, showstack=True):\n    \"\"\"\n    Debugging of an exception: print and/or log frame and/or stacktrace\n    :param ex:          The exception object\n    :param log_ex:      True | False\n    :param print_ex:    True | False\n    :param showstack:   True | False\n    \"\"\"\n\n    # get basic info and the stack\n    exc_type, exc_value, exc_tb = sys.exc_info()\n\n    text = \"((( EXCEPTION )))\\n[CLASS]: {}\\n[VALUE]: {}\\n\".format(exc_type, exc_value)\n    # get detailed exception info\n    if ex is None:\n        ex = exc_value\n    for k, v in vars(ex).items():\n        text += \"[{}]: {}\\n\".format(k.upper(), str(v))\n\n    # determine how far we trace it back\n    tb_list = None\n    if showstack:\n        tb_list = traceback.extract_tb(exc_tb)\n    else:\n        tb_list = traceback.extract_tb(exc_tb, limit=1)\n\n    # ensure a backtrace exists first\n    if tb_list is not None and len(tb_list) > 0:\n        text += \"((( BACKTRACE )))\\n\"\n\n        for tb_info in tb_list:\n            filename, linenum, funcname, source = tb_info\n\n            if funcname != '<module>':\n                funcname = funcname + '()'\n            text += \"[FILE]: {}\\n[LINE NUM]: {}\\n[FUNCTION]: {}\\n[SOURCE]: {}\\n\".format(filename, linenum, funcname,\n                source)\n    if log_ex:\n        IO.logerr(text)\n    if print_ex:\n        IO.printerr(text)\n\ndef debugEndpoint(log_out=True, print_out=True, **kwargs):\n    \"\"\"\n    Debug an endpoint\\n\n    Must be run within request context\n\n    :param log_out:       True | False\n    :param print_out:     True | False\n    :param kwargs:        Any args to print / log (<key=value> key word pairs)\n    \"\"\"\n\n    calling_chain = []\n\n    frame = sys._getframe().f_back if sys._getframe().f_back is not None else sys._getframe()\n    # parent module\n    if hasattr(frame.f_code, 'co_filename'):\n        calling_chain.append(os.path.abspath(frame.f_code.co_filename))\n    # parent class\n    if 'self' in frame.f_locals:\n        calling_chain.append(frame.f_locals[\"self\"].__class__)\n    else:\n        for k, v in frame.f_globals.items():\n            if not k.startswith('__') and frame.f_code.co_name in dir(v):\n                calling_chain.append(k)\n                break\n    # parent func\n    if frame.f_code.co_name != '<module>':\n        calling_chain.append(frame.f_code.co_name)\n\n    text = \"((( [DEBUG ENDPOINT]: {} )))\\n\".format(' -> '.join(calling_chain))\n    text += '\\n'.join((\n        '{}: {}'.format('accept_charsets', str(request.accept_charsets).strip()),\n        '{}: {}'.format('accept_encodings', str(request.accept_encodings).strip()),\n        '{}: {}'.format('accept_languages', str(request.accept_languages).strip()),\n        '{}: {}'.format('accept_mimetypes', str(request.accept_mimetypes).strip()),\n        '{}: {}'.format('access_control_request_headers', str(request.access_control_request_headers).strip()),\n        '{}: {}'.format('access_control_request_method', str(request.access_control_request_method).strip()),\n        '{}: {}'.format('access_route', str(request.access_route).strip()),\n        '{}: {}'.format('args', str(request.args).strip()),\n        '{}: {}'.format('authorization', str(request.authorization).strip()),\n        '{}: {}'.format('base_url', str(request.base_url).strip()),\n        '{}: {}'.format('blueprint', str(request.blueprint).strip()),\n        '{}: {}'.format('cache_control', str(request.cache_control).strip()),\n        '{}: {}'.format('charset', str(request.charset).strip()),\n        '{}: {}'.format('content_encoding', str(request.content_encoding).strip()),\n        '{}: {}'.format('content_length', str(request.content_length).strip()),\n        '{}: {}'.format('content_md5', str(request.content_md5).strip()),\n        '{}: {}'.format('content_type', str(request.content_type).strip()),\n        '{}: {}'.format('cookies', str(request.cookies).strip()),\n        '{}: {}'.format('files', str(request.files).strip()),\n        '{}: {}'.format('form', str(request.form).strip()),\n        '{}: {}'.format('headers', str(request.headers).strip()),\n        '{}: {}'.format('json', str(request.get_json(force=True, silent=True)).strip()),\n        '{}: {}'.format('method', str(request.method).strip()),\n        '{}: {}'.format('query_string', str(request.query_string).strip()),\n        '{}: {}'.format('referrer', str(request.referrer).strip()),\n        '{}: {}'.format('remote_addr', str(request.remote_addr).strip()),\n        '{}: {}'.format('remote_user', str(request.remote_user).strip()),\n        '{}: {}'.format('url', str(request.url).strip()),\n        '{}: {}'.format('user_agent', str(request.user_agent).strip()),\n        '{}: {}'.format('values', str(request.values).strip()),\n        '{}: {}'.format('view_args', str(request.view_args).strip()),\n    ))\n\n    if len(kwargs) > 0:\n        for k, v in sorted(kwargs):\n            text += \"{}: {}\\n\".format(k, str(v).strip())\n\n    if log_out:\n        IO.logdbg(text)\n\n    if print_out:\n        IO.printdbg(text)\n\ndef allowed_file(filename, ALLOWED_EXTENSIONS={'csv', 'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}):\n    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS\n\ndef showError(type=\"\", code=None, msg=None):\n    code = int(code) if code is not None else StatusCodes.HTTP_INTERNAL_SERVER_ERROR\n    return render_template('error.html', type=type, msg=msg), code\n\ndef redirectCustom(location, *render_args, code=302, response_cb=None, force_redirect=False):\n    \"\"\"\n    =======\n    Summary\n    =======\n\n    Combines functionality of :func:`werkzeug.utils.redirect` with :func:`flask.templating.render_template`\n    to allow for virtual redirection to an endpoint with any HTTP status code.\\n\n\n    =========\n    Use Cases\n    =========\n\n    1. render template at a custom url (doesn't have to be an endpoint)\n\n        def index():\n            return redirectCustom('http://localhost:5000/notfound', render_template('not_found.html'), code=404)\n\n        def index():\n            html = '<h1>Are you lost {{ username }}?</h1>'\n            return redirectCustom(url_for('notfound'), render_template_string(html), username='john', code=404)\n\n    2. customizing the response before sending to the client\n\n        def index():\n            def addAuthHeaders(response):\n                response.headers['Access-Control-Allow-Origin' = '*'\n                response.headers['Access-Control-Allow-Headers'] = 'origin, x-requested-with, content-type'\n                response.headers['Access-Control-Allow-Methods'] = 'PUT, GET, POST, DELETE, OPTIONS'\n            return redirectCustom(url_for('login'), render_template('login_custom.html'), code=403, response_cb=addAuthHeaders)\n\n    3. redirecting with a custom HTTP status code (normally restricted to 300's)\n\n        def index():\n            return redirectCustom(url_for('http://localhost:5000/error'), showError())\n\n    4. forcing redirection when client normally would ignore location header\n\n        def index():\n            return redirectCustom(url_for(showError), showError(), code=500, force_redirect=True)\n\n\n    :note: the endpoint logic is only executed when a the view function is passed in render_args\n    :param location: the location the response should virtually redirect to\n    :param render_args: the return value from a view / template rendering function\n    :param code: the return HTTP status code, defaults to 302 like a normal redirect\n    :param response_cb: callback function taking :class:`werkzeug.wrappers.Response` object as arg and returning edited response\n    :param force_redirect: whether to force redirection on client, only needed when client ignores location header\n    :return: client viewable :class:`werkzeug.wrappers.Response` object\n    \"\"\"\n\n    display_location = escape(location)\n    if isinstance(location, str):\n        # Safe conversion as stated in :func:`werkzeug.utils.redirect`\n        location = iri_to_uri(location, safe_conversion=True)\n\n    # create a response object, from rendered data (accepts None)\n    response = make_response(*render_args)\n\n    # if no render_args given fill response with default redirect html\n    if len(render_args) <= 0:\n        response.response = '<!DOCTYPE HTML\">\\n' \\\n                            '<html><head><meta charset=\"utf-8\"><meta http-equiv=\"refresh\" content=\"0; URL={location}\">' \\\n                            '<title>Redirecting</title></head>\\n' \\\n                            '<body><script type=\"text/javascript\">window.location.href={location};</script></body></html>' if force_redirect else \\\n            '<body><h1>Redirecting...</h1>\\n' \\\n            '<p>You should be redirected automatically to target URL: ' \\\n            '<a href=\"{location}\">{display_location}</a>.  If not click the link.</p></body></html>' \\\n                .format(location=escape(location), display_location=display_location)\n\n        response.mimetype = 'text/html'\n\n    # customize response if needed\n    if response_cb is not None:\n        response = response_cb(response)\n\n    # override return code if set from render args\n    # if len(render_args) == 3:\n    response.status = code\n\n    # change response location\n    response.headers['Location'] = location\n\n    return response\n\ndef getRequestData():\n    \"\"\"\n    Get data from request formatted as dict\\n\n    Must be run within request context\n\n    -----\n\n    The following data formats are supported:\n\n    - multipart forms\n    - url encoded forms\n    - JSON body\n\n    :return:        request data\n    :rtype:         dict\n    \"\"\"\n\n    content_type = str.lower(request.headers.get('Content-Type', ''))\n    if 'multipart/form-data' in content_type:\n        data = request.form.to_dict(flat=False)\n    elif 'application/x-www-form-urlencoded' in content_type:\n        data = request.form.to_dict(flat=False)\n    elif 'application/json' in content_type:\n        data = request.get_json()\n    else:\n        data = request.get_json(force=True, silent=True)\n\n    # fix data if client is sloppy (http_async_client)\n    if request.headers.get('User-Agent') == 'http_async_client':\n        data = json.loads(list(data)[0])\n\n    return data\n\nclass StatusCodes():\n    \"\"\"\n    Namespace for descriptive status codes, for code readability\n\n    -- HTTP Status Codes --\n    Original Idea From: `flask_api <https://www.flaskapi.org/>`_\n    :ref RFC2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html\n    :ref RFC6585: http://tools.ietf.org/html/rfc6585\n\n    ... examples of possible future sections ...\n    -- MYSQL Error Codes --\n    -- SERVER Status Codes --\n    -- KAMAILIO Error Codes --\n    -- RTPENGINE Error Codes --\n    \"\"\"\n\n    HTTP_CONTINUE = 100\n    HTTP_SWITCHING_PROTOCOLS = 101\n    HTTP_OK = 200\n    HTTP_CREATED = 201\n    HTTP_ACCEPTED = 202\n    HTTP_NON_AUTHORITATIVE_INFORMATION = 203\n    HTTP_NO_CONTENT = 204\n    HTTP_RESET_CONTENT = 205\n    HTTP_PARTIAL_CONTENT = 206\n    HTTP_MULTI_STATUS = 207\n    HTTP_MULTIPLE_CHOICES = 300\n    HTTP_MOVED_PERMANENTLY = 301\n    HTTP_FOUND = 302\n    HTTP_SEE_OTHER = 303\n    HTTP_NOT_MODIFIED = 304\n    HTTP_USE_PROXY = 305\n    HTTP_RESERVED = 306\n    HTTP_TEMPORARY_REDIRECT = 307\n    HTTP_PERMANENT_REDIRECT = 308\n    HTTP_BAD_REQUEST = 400\n    HTTP_UNAUTHORIZED = 401\n    HTTP_PAYMENT_REQUIRED = 402\n    HTTP_FORBIDDEN = 403\n    HTTP_NOT_FOUND = 404\n    HTTP_METHOD_NOT_ALLOWED = 405\n    HTTP_NOT_ACCEPTABLE = 406\n    HTTP_PROXY_AUTHENTICATION_REQUIRED = 407\n    HTTP_REQUEST_TIMEOUT = 408\n    HTTP_CONFLICT = 409\n    HTTP_GONE = 410\n    HTTP_LENGTH_REQUIRED = 411\n    HTTP_PRECONDITION_FAILED = 412\n    HTTP_REQUEST_ENTITY_TOO_LARGE = 413\n    HTTP_REQUEST_URI_TOO_LONG = 414\n    HTTP_UNSUPPORTED_MEDIA_TYPE = 415\n    HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416\n    HTTP_EXPECTATION_FAILED = 417\n    HTTP_PRECONDITION_REQUIRED = 428\n    HTTP_TOO_MANY_REQUESTS = 429\n    HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431\n    HTTP_CONNECTION_CLOSED_WITHOUT_RESPONSE = 444\n    HTTP_INTERNAL_SERVER_ERROR = 500\n    HTTP_NOT_IMPLEMENTED = 501\n    HTTP_BAD_GATEWAY = 502\n    HTTP_SERVICE_UNAVAILABLE = 503\n    HTTP_GATEWAY_TIMEOUT = 504\n    HTTP_HTTP_VERSION_NOT_SUPPORTED = 505\n    HTTP_LOOP_DETECTED = 508\n    HTTP_NOT_EXTENDED = 510\n    HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511\n"
  },
  {
    "path": "gui/static/css/bootstrap-theme.css",
    "content": "/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n  text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);\n  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);\n          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);\n}\n.btn-default:active,\n.btn-primary:active,\n.btn-success:active,\n.btn-info:active,\n.btn-warning:active,\n.btn-danger:active,\n.btn-default.active,\n.btn-primary.active,\n.btn-success.active,\n.btn-info.active,\n.btn-warning.active,\n.btn-danger.active {\n  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);\n          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);\n}\n.btn-default.disabled,\n.btn-primary.disabled,\n.btn-success.disabled,\n.btn-info.disabled,\n.btn-warning.disabled,\n.btn-danger.disabled,\n.btn-default[disabled],\n.btn-primary[disabled],\n.btn-success[disabled],\n.btn-info[disabled],\n.btn-warning[disabled],\n.btn-danger[disabled],\nfieldset[disabled] .btn-default,\nfieldset[disabled] .btn-primary,\nfieldset[disabled] .btn-success,\nfieldset[disabled] .btn-info,\nfieldset[disabled] .btn-warning,\nfieldset[disabled] .btn-danger {\n  -webkit-box-shadow: none;\n          box-shadow: none;\n}\n.btn-default .badge,\n.btn-primary .badge,\n.btn-success .badge,\n.btn-info .badge,\n.btn-warning .badge,\n.btn-danger .badge {\n  text-shadow: none;\n}\n.btn:active,\n.btn.active {\n  background-image: none;\n}\n.btn-default {\n  text-shadow: 0 1px 0 #fff;\n  background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n  background-image:      -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));\n  background-image:         linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  background-repeat: repeat-x;\n  border-color: #dbdbdb;\n  border-color: #ccc;\n}\n.btn-default:hover,\n.btn-default:focus {\n  background-color: #e0e0e0;\n  background-position: 0 -15px;\n}\n.btn-default:active,\n.btn-default.active {\n  background-color: #e0e0e0;\n  border-color: #dbdbdb;\n}\n.btn-default.disabled,\n.btn-default[disabled],\nfieldset[disabled] .btn-default,\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus,\n.btn-default.disabled:active,\n.btn-default[disabled]:active,\nfieldset[disabled] .btn-default:active,\n.btn-default.disabled.active,\n.btn-default[disabled].active,\nfieldset[disabled] .btn-default.active {\n  background-color: #e0e0e0;\n  background-image: none;\n}\n.btn-primary {\n  background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);\n  background-image:      -o-linear-gradient(top, #337ab7 0%, #265a88 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));\n  background-image:         linear-gradient(to bottom, #337ab7 0%, #265a88 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  background-repeat: repeat-x;\n  border-color: #245580;\n}\n.btn-primary:hover,\n.btn-primary:focus {\n  background-color: #265a88;\n  background-position: 0 -15px;\n}\n.btn-primary:active,\n.btn-primary.active {\n  background-color: #265a88;\n  border-color: #245580;\n}\n.btn-primary.disabled,\n.btn-primary[disabled],\nfieldset[disabled] .btn-primary,\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus,\n.btn-primary.disabled:active,\n.btn-primary[disabled]:active,\nfieldset[disabled] .btn-primary:active,\n.btn-primary.disabled.active,\n.btn-primary[disabled].active,\nfieldset[disabled] .btn-primary.active {\n  background-color: #265a88;\n  background-image: none;\n}\n.btn-success {\n  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);\n  background-image:      -o-linear-gradient(top, #5cb85c 0%, #419641 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));\n  background-image:         linear-gradient(to bottom, #5cb85c 0%, #419641 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  background-repeat: repeat-x;\n  border-color: #3e8f3e;\n}\n.btn-success:hover,\n.btn-success:focus {\n  background-color: #419641;\n  background-position: 0 -15px;\n}\n.btn-success:active,\n.btn-success.active {\n  background-color: #419641;\n  border-color: #3e8f3e;\n}\n.btn-success.disabled,\n.btn-success[disabled],\nfieldset[disabled] .btn-success,\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus,\n.btn-success.disabled:active,\n.btn-success[disabled]:active,\nfieldset[disabled] .btn-success:active,\n.btn-success.disabled.active,\n.btn-success[disabled].active,\nfieldset[disabled] .btn-success.active {\n  background-color: #419641;\n  background-image: none;\n}\n.btn-info {\n  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n  background-image:      -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));\n  background-image:         linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  background-repeat: repeat-x;\n  border-color: #28a4c9;\n}\n.btn-info:hover,\n.btn-info:focus {\n  background-color: #2aabd2;\n  background-position: 0 -15px;\n}\n.btn-info:active,\n.btn-info.active {\n  background-color: #2aabd2;\n  border-color: #28a4c9;\n}\n.btn-info.disabled,\n.btn-info[disabled],\nfieldset[disabled] .btn-info,\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus,\n.btn-info.disabled:active,\n.btn-info[disabled]:active,\nfieldset[disabled] .btn-info:active,\n.btn-info.disabled.active,\n.btn-info[disabled].active,\nfieldset[disabled] .btn-info.active {\n  background-color: #2aabd2;\n  background-image: none;\n}\n.btn-warning {\n  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n  background-image:      -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));\n  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  background-repeat: repeat-x;\n  border-color: #e38d13;\n}\n.btn-warning:hover,\n.btn-warning:focus {\n  background-color: #eb9316;\n  background-position: 0 -15px;\n}\n.btn-warning:active,\n.btn-warning.active {\n  background-color: #eb9316;\n  border-color: #e38d13;\n}\n.btn-warning.disabled,\n.btn-warning[disabled],\nfieldset[disabled] .btn-warning,\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus,\n.btn-warning.disabled:active,\n.btn-warning[disabled]:active,\nfieldset[disabled] .btn-warning:active,\n.btn-warning.disabled.active,\n.btn-warning[disabled].active,\nfieldset[disabled] .btn-warning.active {\n  background-color: #eb9316;\n  background-image: none;\n}\n.btn-danger {\n  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n  background-image:      -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));\n  background-image:         linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  background-repeat: repeat-x;\n  border-color: #b92c28;\n}\n.btn-danger:hover,\n.btn-danger:focus {\n  background-color: #c12e2a;\n  background-position: 0 -15px;\n}\n.btn-danger:active,\n.btn-danger.active {\n  background-color: #c12e2a;\n  border-color: #b92c28;\n}\n.btn-danger.disabled,\n.btn-danger[disabled],\nfieldset[disabled] .btn-danger,\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus,\n.btn-danger.disabled:active,\n.btn-danger[disabled]:active,\nfieldset[disabled] .btn-danger:active,\n.btn-danger.disabled.active,\n.btn-danger[disabled].active,\nfieldset[disabled] .btn-danger.active {\n  background-color: #c12e2a;\n  background-image: none;\n}\n.thumbnail,\n.img-thumbnail {\n  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);\n          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n  background-color: #e8e8e8;\n  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n  background-image:      -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));\n  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n  background-repeat: repeat-x;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n  background-color: #2e6da4;\n  background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n  background-image:      -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));\n  background-image:         linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n  background-repeat: repeat-x;\n}\n.navbar-default {\n  background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);\n  background-image:      -o-linear-gradient(top, #fff 0%, #f8f8f8 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8));\n  background-image:         linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  background-repeat: repeat-x;\n  border-radius: 4px;\n  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);\n          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .active > a {\n  background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n  background-image:      -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));\n  background-image:         linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);\n  background-repeat: repeat-x;\n  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);\n          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);\n}\n.navbar-brand,\n.navbar-nav > li > a {\n  text-shadow: 0 1px 0 rgba(255, 255, 255, .25);\n}\n.navbar-inverse {\n  background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);\n  background-image:      -o-linear-gradient(top, #3c3c3c 0%, #222 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222));\n  background-image:         linear-gradient(to bottom, #3c3c3c 0%, #222 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  background-repeat: repeat-x;\n  border-radius: 4px;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .active > a {\n  background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n  background-image:      -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f));\n  background-image:         linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);\n  background-repeat: repeat-x;\n  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);\n          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);\n}\n.navbar-inverse .navbar-brand,\n.navbar-inverse .navbar-nav > li > a {\n  text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);\n}\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n  border-radius: 0;\n}\n@media (max-width: 767px) {\n  .navbar .navbar-nav .open .dropdown-menu > .active > a,\n  .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,\n  .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {\n    color: #fff;\n    background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n    background-image:      -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n    background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));\n    background-image:         linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n    background-repeat: repeat-x;\n  }\n}\n.alert {\n  text-shadow: 0 1px 0 rgba(255, 255, 255, .2);\n  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);\n          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);\n}\n.alert-success {\n  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n  background-image:      -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc));\n  background-image:         linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);\n  background-repeat: repeat-x;\n  border-color: #b2dba1;\n}\n.alert-info {\n  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n  background-image:      -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0));\n  background-image:         linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);\n  background-repeat: repeat-x;\n  border-color: #9acfea;\n}\n.alert-warning {\n  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n  background-image:      -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0));\n  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);\n  background-repeat: repeat-x;\n  border-color: #f5e79e;\n}\n.alert-danger {\n  background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n  background-image:      -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3));\n  background-image:         linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);\n  background-repeat: repeat-x;\n  border-color: #dca7a7;\n}\n.progress {\n  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n  background-image:      -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));\n  background-image:         linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);\n  background-repeat: repeat-x;\n}\n.progress-bar {\n  background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);\n  background-image:      -o-linear-gradient(top, #337ab7 0%, #286090 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090));\n  background-image:         linear-gradient(to bottom, #337ab7 0%, #286090 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);\n  background-repeat: repeat-x;\n}\n.progress-bar-success {\n  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n  background-image:      -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44));\n  background-image:         linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);\n  background-repeat: repeat-x;\n}\n.progress-bar-info {\n  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n  background-image:      -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5));\n  background-image:         linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);\n  background-repeat: repeat-x;\n}\n.progress-bar-warning {\n  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n  background-image:      -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f));\n  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);\n  background-repeat: repeat-x;\n}\n.progress-bar-danger {\n  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n  background-image:      -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c));\n  background-image:         linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);\n  background-repeat: repeat-x;\n}\n.progress-bar-striped {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:      -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n}\n.list-group {\n  border-radius: 4px;\n  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);\n          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n  text-shadow: 0 -1px 0 #286090;\n  background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n  background-image:      -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a));\n  background-image:         linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);\n  background-repeat: repeat-x;\n  border-color: #2b669a;\n}\n.list-group-item.active .badge,\n.list-group-item.active:hover .badge,\n.list-group-item.active:focus .badge {\n  text-shadow: none;\n}\n.panel {\n  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);\n          box-shadow: 0 1px 2px rgba(0, 0, 0, .05);\n}\n.panel-default > .panel-heading {\n  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n  background-image:      -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));\n  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n  background-repeat: repeat-x;\n}\n.panel-primary > .panel-heading {\n  background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n  background-image:      -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));\n  background-image:         linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n  background-repeat: repeat-x;\n}\n.panel-success > .panel-heading {\n  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n  background-image:      -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6));\n  background-image:         linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);\n  background-repeat: repeat-x;\n}\n.panel-info > .panel-heading {\n  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n  background-image:      -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3));\n  background-image:         linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);\n  background-repeat: repeat-x;\n}\n.panel-warning > .panel-heading {\n  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n  background-image:      -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc));\n  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);\n  background-repeat: repeat-x;\n}\n.panel-danger > .panel-heading {\n  background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n  background-image:      -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc));\n  background-image:         linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);\n  background-repeat: repeat-x;\n}\n.well {\n  background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n  background-image:      -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));\n  background-image:         linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);\n  background-repeat: repeat-x;\n  border-color: #dcdcdc;\n  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);\n          box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);\n}\n/*# sourceMappingURL=bootstrap-theme.css.map */\n"
  },
  {
    "path": "gui/static/css/bootstrap-toggle.css",
    "content": "/*! ========================================================================\n * Bootstrap Toggle: bootstrap-toggle.css v2.2.0\n * http://www.bootstraptoggle.com\n * ========================================================================\n * Copyright 2014 Min Hur, The New York Times Company\n * Licensed under MIT\n * ======================================================================== */\n\n\n.checkbox label .toggle,\n.checkbox-inline .toggle {\n\tmargin-left: -20px;\n\tmargin-right: 5px;\n}\n\n.toggle {\n\tposition: relative;\n\toverflow: hidden;\n}\n.toggle input[type=\"checkbox\"] {\n\tdisplay: none;\n}\n.toggle-group {\n\tposition: absolute;\n\twidth: 200%;\n\ttop: 0;\n\tbottom: 0;\n\tleft: 0;\n\ttransition: left 0.35s;\n\t-webkit-transition: left 0.35s;\n\t-moz-user-select: none;\n\t-webkit-user-select: none;\n}\n.toggle.off .toggle-group {\n\tleft: -100%;\n}\n.toggle-on {\n\tposition: absolute;\n\ttop: 0;\n\tbottom: 0;\n\tleft: 0;\n\tright: 50%;\n\tmargin: 0;\n\tborder: 0;\n\tborder-radius: 0;\n}\n.toggle-off {\n\tposition: absolute;\n\ttop: 0;\n\tbottom: 0;\n\tleft: 50%;\n\tright: 0;\n\tmargin: 0;\n\tborder: 0;\n\tborder-radius: 0;\n}\n.toggle-handle {\n\tposition: relative;\n\tmargin: 0 auto;\n\tpadding-top: 0px;\n\tpadding-bottom: 0px;\n\theight: 100%;\n\twidth: 0px;\n\tborder-width: 0 1px;\n}\n\n.toggle.btn { min-width: 59px; min-height: 34px; }\n.toggle-on.btn { padding-right: 24px; }\n.toggle-off.btn { padding-left: 24px; }\n\n.toggle.btn-lg { min-width: 79px; min-height: 45px; }\n.toggle-on.btn-lg { padding-right: 31px; }\n.toggle-off.btn-lg { padding-left: 31px; }\n.toggle-handle.btn-lg { width: 40px; }\n\n.toggle.btn-sm { min-width: 50px; min-height: 30px;}\n.toggle-on.btn-sm { padding-right: 20px; }\n.toggle-off.btn-sm { padding-left: 20px; }\n\n.toggle.btn-xs { min-width: 35px; min-height: 22px;}\n.toggle-on.btn-xs { padding-right: 12px; }\n.toggle-off.btn-xs { padding-left: 12px; }\n\n"
  },
  {
    "path": "gui/static/css/bootstrap.css",
    "content": "/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\nhtml {\n  font-family: sans-serif;\n  -webkit-text-size-adjust: 100%;\n      -ms-text-size-adjust: 100%;\n}\nbody {\n  margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n  display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n  display: inline-block;\n  vertical-align: baseline;\n}\naudio:not([controls]) {\n  display: none;\n  height: 0;\n}\n[hidden],\ntemplate {\n  display: none;\n}\na {\n  background-color: transparent;\n}\na:active,\na:hover {\n  outline: 0;\n}\nabbr[title] {\n  border-bottom: 1px dotted;\n}\nb,\nstrong {\n  font-weight: bold;\n}\ndfn {\n  font-style: italic;\n}\nh1 {\n  margin: .67em 0;\n  font-size: 2em;\n}\nmark {\n  color: #000;\n  background: #ff0;\n}\nsmall {\n  font-size: 80%;\n}\nsub,\nsup {\n  position: relative;\n  font-size: 75%;\n  line-height: 0;\n  vertical-align: baseline;\n}\nsup {\n  top: -.5em;\n}\nsub {\n  bottom: -.25em;\n}\nimg {\n  border: 0;\n}\nsvg:not(:root) {\n  overflow: hidden;\n}\nfigure {\n  margin: 1em 40px;\n}\nhr {\n  height: 0;\n  -webkit-box-sizing: content-box;\n     -moz-box-sizing: content-box;\n          box-sizing: content-box;\n}\npre {\n  overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n  font-family: monospace, monospace;\n  font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  margin: 0;\n  font: inherit;\n  color: inherit;\n}\nbutton {\n  overflow: visible;\n}\nbutton,\nselect {\n  text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n  -webkit-appearance: button;\n  cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n  cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n  padding: 0;\n  border: 0;\n}\ninput {\n  line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n  -webkit-box-sizing: border-box;\n     -moz-box-sizing: border-box;\n          box-sizing: border-box;\n  padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n  height: auto;\n}\ninput[type=\"search\"] {\n  -webkit-box-sizing: content-box;\n     -moz-box-sizing: content-box;\n          box-sizing: content-box;\n  -webkit-appearance: textfield;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\nfieldset {\n  padding: .35em .625em .75em;\n  margin: 0 2px;\n  border: 1px solid #c0c0c0;\n}\nlegend {\n  padding: 0;\n  border: 0;\n}\ntextarea {\n  overflow: auto;\n}\noptgroup {\n  font-weight: bold;\n}\ntable {\n  border-spacing: 0;\n  border-collapse: collapse;\n}\ntd,\nth {\n  padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n  *,\n  *:before,\n  *:after {\n    color: #000 !important;\n    text-shadow: none !important;\n    background: transparent !important;\n    -webkit-box-shadow: none !important;\n            box-shadow: none !important;\n  }\n  a,\n  a:visited {\n    text-decoration: underline;\n  }\n  a[href]:after {\n    content: \" (\" attr(href) \")\";\n  }\n  abbr[title]:after {\n    content: \" (\" attr(title) \")\";\n  }\n  a[href^=\"#\"]:after,\n  a[href^=\"javascript:\"]:after {\n    content: \"\";\n  }\n  pre,\n  blockquote {\n    border: 1px solid #999;\n\n    page-break-inside: avoid;\n  }\n  thead {\n    display: table-header-group;\n  }\n  tr,\n  img {\n    page-break-inside: avoid;\n  }\n  img {\n    max-width: 100% !important;\n  }\n  p,\n  h2,\n  h3 {\n    orphans: 3;\n    widows: 3;\n  }\n  h2,\n  h3 {\n    page-break-after: avoid;\n  }\n  .navbar {\n    display: none;\n  }\n  .btn > .caret,\n  .dropup > .btn > .caret {\n    border-top-color: #000 !important;\n  }\n  .label {\n    border: 1px solid #000;\n  }\n  .table {\n    border-collapse: collapse !important;\n  }\n  .table td,\n  .table th {\n    background-color: #fff !important;\n  }\n  .table-bordered th,\n  .table-bordered td {\n    border: 1px solid #ddd !important;\n  }\n}\n@font-face {\n  font-family: 'Glyphicons Halflings';\n\n  src: url('../fonts/glyphicons-halflings-regular.eot');\n  src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');\n}\n.glyphicon {\n  position: relative;\n  top: 1px;\n  display: inline-block;\n  font-family: 'Glyphicons Halflings';\n  font-style: normal;\n  font-weight: normal;\n  line-height: 1;\n\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n  content: \"\\002a\";\n}\n.glyphicon-plus:before {\n  content: \"\\002b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n  content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n  content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n  content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n  content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n  content: \"\\270f\";\n}\n.glyphicon-glass:before {\n  content: \"\\e001\";\n}\n.glyphicon-music:before {\n  content: \"\\e002\";\n}\n.glyphicon-search:before {\n  content: \"\\e003\";\n}\n.glyphicon-heart:before {\n  content: \"\\e005\";\n}\n.glyphicon-star:before {\n  content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n  content: \"\\e007\";\n}\n.glyphicon-user:before {\n  content: \"\\e008\";\n}\n.glyphicon-film:before {\n  content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n  content: \"\\e010\";\n}\n.glyphicon-th:before {\n  content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n  content: \"\\e012\";\n}\n.glyphicon-ok:before {\n  content: \"\\e013\";\n}\n.glyphicon-remove:before {\n  content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n  content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n  content: \"\\e016\";\n}\n.glyphicon-off:before {\n  content: \"\\e017\";\n}\n.glyphicon-signal:before {\n  content: \"\\e018\";\n}\n.glyphicon-cog:before {\n  content: \"\\e019\";\n}\n.glyphicon-trash:before {\n  content: \"\\e020\";\n}\n.glyphicon-home:before {\n  content: \"\\e021\";\n}\n.glyphicon-file:before {\n  content: \"\\e022\";\n}\n.glyphicon-time:before {\n  content: \"\\e023\";\n}\n.glyphicon-road:before {\n  content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n  content: \"\\e025\";\n}\n.glyphicon-download:before {\n  content: \"\\e026\";\n}\n.glyphicon-upload:before {\n  content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n  content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n  content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n  content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n  content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n  content: \"\\e032\";\n}\n.glyphicon-lock:before {\n  content: \"\\e033\";\n}\n.glyphicon-flag:before {\n  content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n  content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n  content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n  content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n  content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n  content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n  content: \"\\e040\";\n}\n.glyphicon-tag:before {\n  content: \"\\e041\";\n}\n.glyphicon-tags:before {\n  content: \"\\e042\";\n}\n.glyphicon-book:before {\n  content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n  content: \"\\e044\";\n}\n.glyphicon-print:before {\n  content: \"\\e045\";\n}\n.glyphicon-camera:before {\n  content: \"\\e046\";\n}\n.glyphicon-font:before {\n  content: \"\\e047\";\n}\n.glyphicon-bold:before {\n  content: \"\\e048\";\n}\n.glyphicon-italic:before {\n  content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n  content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n  content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n  content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n  content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n  content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n  content: \"\\e055\";\n}\n.glyphicon-list:before {\n  content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n  content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n  content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n  content: \"\\e059\";\n}\n.glyphicon-picture:before {\n  content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n  content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n  content: \"\\e063\";\n}\n.glyphicon-tint:before {\n  content: \"\\e064\";\n}\n.glyphicon-edit:before {\n  content: \"\\e065\";\n}\n.glyphicon-share:before {\n  content: \"\\e066\";\n}\n.glyphicon-check:before {\n  content: \"\\e067\";\n}\n.glyphicon-move:before {\n  content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n  content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n  content: \"\\e070\";\n}\n.glyphicon-backward:before {\n  content: \"\\e071\";\n}\n.glyphicon-play:before {\n  content: \"\\e072\";\n}\n.glyphicon-pause:before {\n  content: \"\\e073\";\n}\n.glyphicon-stop:before {\n  content: \"\\e074\";\n}\n.glyphicon-forward:before {\n  content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n  content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n  content: \"\\e077\";\n}\n.glyphicon-eject:before {\n  content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n  content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n  content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n  content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n  content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n  content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n  content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n  content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n  content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n  content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n  content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n  content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n  content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n  content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n  content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n  content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n  content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n  content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n  content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n  content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n  content: \"\\e101\";\n}\n.glyphicon-gift:before {\n  content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n  content: \"\\e103\";\n}\n.glyphicon-fire:before {\n  content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n  content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n  content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n  content: \"\\e107\";\n}\n.glyphicon-plane:before {\n  content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n  content: \"\\e109\";\n}\n.glyphicon-random:before {\n  content: \"\\e110\";\n}\n.glyphicon-comment:before {\n  content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n  content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n  content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n  content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n  content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n  content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n  content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n  content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n  content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n  content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n  content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n  content: \"\\e122\";\n}\n.glyphicon-bell:before {\n  content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n  content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n  content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n  content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n  content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n  content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n  content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n  content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n  content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n  content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n  content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n  content: \"\\e134\";\n}\n.glyphicon-globe:before {\n  content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n  content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n  content: \"\\e137\";\n}\n.glyphicon-filter:before {\n  content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n  content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n  content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n  content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n  content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n  content: \"\\e143\";\n}\n.glyphicon-link:before {\n  content: \"\\e144\";\n}\n.glyphicon-phone:before {\n  content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n  content: \"\\e146\";\n}\n.glyphicon-usd:before {\n  content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n  content: \"\\e149\";\n}\n.glyphicon-sort:before {\n  content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n  content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n  content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n  content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n  content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n  content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n  content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n  content: \"\\e157\";\n}\n.glyphicon-expand:before {\n  content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n  content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n  content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n  content: \"\\e161\";\n}\n.glyphicon-flash:before {\n  content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n  content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n  content: \"\\e164\";\n}\n.glyphicon-record:before {\n  content: \"\\e165\";\n}\n.glyphicon-save:before {\n  content: \"\\e166\";\n}\n.glyphicon-open:before {\n  content: \"\\e167\";\n}\n.glyphicon-saved:before {\n  content: \"\\e168\";\n}\n.glyphicon-import:before {\n  content: \"\\e169\";\n}\n.glyphicon-export:before {\n  content: \"\\e170\";\n}\n.glyphicon-send:before {\n  content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n  content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n  content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n  content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n  content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n  content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n  content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n  content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n  content: \"\\e179\";\n}\n.glyphicon-header:before {\n  content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n  content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n  content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n  content: \"\\e183\";\n}\n.glyphicon-tower:before {\n  content: \"\\e184\";\n}\n.glyphicon-stats:before {\n  content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n  content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n  content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n  content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n  content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n  content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n  content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n  content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n  content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n  content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n  content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n  content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n  content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n  content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n  content: \"\\e200\";\n}\n.glyphicon-cd:before {\n  content: \"\\e201\";\n}\n.glyphicon-save-file:before {\n  content: \"\\e202\";\n}\n.glyphicon-open-file:before {\n  content: \"\\e203\";\n}\n.glyphicon-level-up:before {\n  content: \"\\e204\";\n}\n.glyphicon-copy:before {\n  content: \"\\e205\";\n}\n.glyphicon-paste:before {\n  content: \"\\e206\";\n}\n.glyphicon-alert:before {\n  content: \"\\e209\";\n}\n.glyphicon-equalizer:before {\n  content: \"\\e210\";\n}\n.glyphicon-king:before {\n  content: \"\\e211\";\n}\n.glyphicon-queen:before {\n  content: \"\\e212\";\n}\n.glyphicon-pawn:before {\n  content: \"\\e213\";\n}\n.glyphicon-bishop:before {\n  content: \"\\e214\";\n}\n.glyphicon-knight:before {\n  content: \"\\e215\";\n}\n.glyphicon-baby-formula:before {\n  content: \"\\e216\";\n}\n.glyphicon-tent:before {\n  content: \"\\26fa\";\n}\n.glyphicon-blackboard:before {\n  content: \"\\e218\";\n}\n.glyphicon-bed:before {\n  content: \"\\e219\";\n}\n.glyphicon-apple:before {\n  content: \"\\f8ff\";\n}\n.glyphicon-erase:before {\n  content: \"\\e221\";\n}\n.glyphicon-hourglass:before {\n  content: \"\\231b\";\n}\n.glyphicon-lamp:before {\n  content: \"\\e223\";\n}\n.glyphicon-duplicate:before {\n  content: \"\\e224\";\n}\n.glyphicon-piggy-bank:before {\n  content: \"\\e225\";\n}\n.glyphicon-scissors:before {\n  content: \"\\e226\";\n}\n.glyphicon-bitcoin:before {\n  content: \"\\e227\";\n}\n.glyphicon-btc:before {\n  content: \"\\e227\";\n}\n.glyphicon-xbt:before {\n  content: \"\\e227\";\n}\n.glyphicon-yen:before {\n  content: \"\\00a5\";\n}\n.glyphicon-jpy:before {\n  content: \"\\00a5\";\n}\n.glyphicon-ruble:before {\n  content: \"\\20bd\";\n}\n.glyphicon-rub:before {\n  content: \"\\20bd\";\n}\n.glyphicon-scale:before {\n  content: \"\\e230\";\n}\n.glyphicon-ice-lolly:before {\n  content: \"\\e231\";\n}\n.glyphicon-ice-lolly-tasted:before {\n  content: \"\\e232\";\n}\n.glyphicon-education:before {\n  content: \"\\e233\";\n}\n.glyphicon-option-horizontal:before {\n  content: \"\\e234\";\n}\n.glyphicon-option-vertical:before {\n  content: \"\\e235\";\n}\n.glyphicon-menu-hamburger:before {\n  content: \"\\e236\";\n}\n.glyphicon-modal-window:before {\n  content: \"\\e237\";\n}\n.glyphicon-oil:before {\n  content: \"\\e238\";\n}\n.glyphicon-grain:before {\n  content: \"\\e239\";\n}\n.glyphicon-sunglasses:before {\n  content: \"\\e240\";\n}\n.glyphicon-text-size:before {\n  content: \"\\e241\";\n}\n.glyphicon-text-color:before {\n  content: \"\\e242\";\n}\n.glyphicon-text-background:before {\n  content: \"\\e243\";\n}\n.glyphicon-object-align-top:before {\n  content: \"\\e244\";\n}\n.glyphicon-object-align-bottom:before {\n  content: \"\\e245\";\n}\n.glyphicon-object-align-horizontal:before {\n  content: \"\\e246\";\n}\n.glyphicon-object-align-left:before {\n  content: \"\\e247\";\n}\n.glyphicon-object-align-vertical:before {\n  content: \"\\e248\";\n}\n.glyphicon-object-align-right:before {\n  content: \"\\e249\";\n}\n.glyphicon-triangle-right:before {\n  content: \"\\e250\";\n}\n.glyphicon-triangle-left:before {\n  content: \"\\e251\";\n}\n.glyphicon-triangle-bottom:before {\n  content: \"\\e252\";\n}\n.glyphicon-triangle-top:before {\n  content: \"\\e253\";\n}\n.glyphicon-console:before {\n  content: \"\\e254\";\n}\n.glyphicon-superscript:before {\n  content: \"\\e255\";\n}\n.glyphicon-subscript:before {\n  content: \"\\e256\";\n}\n.glyphicon-menu-left:before {\n  content: \"\\e257\";\n}\n.glyphicon-menu-right:before {\n  content: \"\\e258\";\n}\n.glyphicon-menu-down:before {\n  content: \"\\e259\";\n}\n.glyphicon-menu-up:before {\n  content: \"\\e260\";\n}\n* {\n  -webkit-box-sizing: border-box;\n     -moz-box-sizing: border-box;\n          box-sizing: border-box;\n}\n*:before,\n*:after {\n  -webkit-box-sizing: border-box;\n     -moz-box-sizing: border-box;\n          box-sizing: border-box;\n}\nhtml {\n  font-size: 10px;\n\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-size: 14px;\n  line-height: 1.42857143;\n  color: #333;\n  background-color: #fff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n  font-family: inherit;\n  font-size: inherit;\n  line-height: inherit;\n}\na {\n  color: #337ab7;\n  text-decoration: none;\n}\na:hover,\na:focus {\n  color: #23527c;\n  text-decoration: underline;\n}\na:focus {\n  outline: 5px auto -webkit-focus-ring-color;\n  outline-offset: -2px;\n}\nfigure {\n  margin: 0;\n}\nimg {\n  vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n  display: block;\n  max-width: 100%;\n  height: auto;\n}\n.img-rounded {\n  border-radius: 6px;\n}\n.img-thumbnail {\n  display: inline-block;\n  max-width: 100%;\n  height: auto;\n  padding: 4px;\n  line-height: 1.42857143;\n  background-color: #fff;\n  border: 1px solid #ddd;\n  border-radius: 4px;\n  -webkit-transition: all .2s ease-in-out;\n       -o-transition: all .2s ease-in-out;\n          transition: all .2s ease-in-out;\n}\n.img-circle {\n  border-radius: 50%;\n}\nhr {\n  margin-top: 20px;\n  margin-bottom: 20px;\n  border: 0;\n  border-top: 1px solid #eee;\n}\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n  position: static;\n  width: auto;\n  height: auto;\n  margin: 0;\n  overflow: visible;\n  clip: auto;\n}\n[role=\"button\"] {\n  cursor: pointer;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n  font-family: inherit;\n  font-weight: 500;\n  line-height: 1.1;\n  color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n  font-weight: normal;\n  line-height: 1;\n  color: #777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n  margin-top: 20px;\n  margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n  font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n  font-size: 75%;\n}\nh1,\n.h1 {\n  font-size: 36px;\n}\nh2,\n.h2 {\n  font-size: 30px;\n}\nh3,\n.h3 {\n  font-size: 24px;\n}\nh4,\n.h4 {\n  font-size: 18px;\n}\nh5,\n.h5 {\n  font-size: 14px;\n}\nh6,\n.h6 {\n  font-size: 12px;\n}\np {\n  margin: 0 0 10px;\n}\n.lead {\n  margin-bottom: 20px;\n  font-size: 16px;\n  font-weight: 300;\n  line-height: 1.4;\n}\n@media (min-width: 768px) {\n  .lead {\n    font-size: 21px;\n  }\n}\nsmall,\n.small {\n  font-size: 85%;\n}\nmark,\n.mark {\n  padding: .2em;\n  background-color: #fcf8e3;\n}\n.text-left {\n  text-align: left;\n}\n.text-right {\n  text-align: right;\n}\n.text-center {\n  text-align: center;\n}\n.text-justify {\n  text-align: justify;\n}\n.text-nowrap {\n  white-space: nowrap;\n}\n.text-lowercase {\n  text-transform: lowercase;\n}\n.text-uppercase {\n  text-transform: uppercase;\n}\n.text-capitalize {\n  text-transform: capitalize;\n}\n.text-muted {\n  color: #777;\n}\n.text-primary {\n  color: #337ab7;\n}\na.text-primary:hover,\na.text-primary:focus {\n  color: #286090;\n}\n.text-success {\n  color: #3c763d;\n}\na.text-success:hover,\na.text-success:focus {\n  color: #2b542c;\n}\n.text-info {\n  color: #31708f;\n}\na.text-info:hover,\na.text-info:focus {\n  color: #245269;\n}\n.text-warning {\n  color: #8a6d3b;\n}\na.text-warning:hover,\na.text-warning:focus {\n  color: #66512c;\n}\n.text-danger {\n  color: #a94442;\n}\na.text-danger:hover,\na.text-danger:focus {\n  color: #843534;\n}\n.bg-primary {\n  color: #fff;\n  background-color: #337ab7;\n}\na.bg-primary:hover,\na.bg-primary:focus {\n  background-color: #286090;\n}\n.bg-success {\n  background-color: #dff0d8;\n}\na.bg-success:hover,\na.bg-success:focus {\n  background-color: #c1e2b3;\n}\n.bg-info {\n  background-color: #d9edf7;\n}\na.bg-info:hover,\na.bg-info:focus {\n  background-color: #afd9ee;\n}\n.bg-warning {\n  background-color: #fcf8e3;\n}\na.bg-warning:hover,\na.bg-warning:focus {\n  background-color: #f7ecb5;\n}\n.bg-danger {\n  background-color: #f2dede;\n}\na.bg-danger:hover,\na.bg-danger:focus {\n  background-color: #e4b9b9;\n}\n.page-header {\n  padding-bottom: 9px;\n  margin: 40px 0 20px;\n  border-bottom: 1px solid #eee;\n}\nul,\nol {\n  margin-top: 0;\n  margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n  margin-bottom: 0;\n}\n.list-unstyled {\n  padding-left: 0;\n  list-style: none;\n}\n.list-inline {\n  padding-left: 0;\n  margin-left: -5px;\n  list-style: none;\n}\n.list-inline > li {\n  display: inline-block;\n  padding-right: 5px;\n  padding-left: 5px;\n}\ndl {\n  margin-top: 0;\n  margin-bottom: 20px;\n}\ndt,\ndd {\n  line-height: 1.42857143;\n}\ndt {\n  font-weight: bold;\n}\ndd {\n  margin-left: 0;\n}\n@media (min-width: 768px) {\n  .dl-horizontal dt {\n    float: left;\n    width: 160px;\n    overflow: hidden;\n    clear: left;\n    text-align: right;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .dl-horizontal dd {\n    margin-left: 180px;\n  }\n}\nabbr[title],\nabbr[data-original-title] {\n  cursor: help;\n  border-bottom: 1px dotted #777;\n}\n.initialism {\n  font-size: 90%;\n  text-transform: uppercase;\n}\nblockquote {\n  padding: 10px 20px;\n  margin: 0 0 20px;\n  font-size: 17.5px;\n  border-left: 5px solid #eee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n  margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n  display: block;\n  font-size: 80%;\n  line-height: 1.42857143;\n  color: #777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n  content: '\\2014 \\00A0';\n}\n.blockquote-reverse,\nblockquote.pull-right {\n  padding-right: 15px;\n  padding-left: 0;\n  text-align: right;\n  border-right: 5px solid #eee;\n  border-left: 0;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n  content: '';\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n  content: '\\00A0 \\2014';\n}\naddress {\n  margin-bottom: 20px;\n  font-style: normal;\n  line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n  font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n  padding: 2px 4px;\n  font-size: 90%;\n  color: #c7254e;\n  background-color: #f9f2f4;\n  border-radius: 4px;\n}\nkbd {\n  padding: 2px 4px;\n  font-size: 90%;\n  color: #fff;\n  background-color: #333;\n  border-radius: 3px;\n  -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25);\n          box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25);\n}\nkbd kbd {\n  padding: 0;\n  font-size: 100%;\n  font-weight: bold;\n  -webkit-box-shadow: none;\n          box-shadow: none;\n}\npre {\n  display: block;\n  padding: 9.5px;\n  margin: 0 0 10px;\n  font-size: 13px;\n  line-height: 1.42857143;\n  color: #333;\n  word-break: break-all;\n  word-wrap: break-word;\n  background-color: #f5f5f5;\n  border: 1px solid #ccc;\n  border-radius: 4px;\n}\npre code {\n  padding: 0;\n  font-size: inherit;\n  color: inherit;\n  white-space: pre-wrap;\n  background-color: transparent;\n  border-radius: 0;\n}\n.pre-scrollable {\n  max-height: 340px;\n  overflow-y: scroll;\n}\n.container {\n  padding-right: 15px;\n  padding-left: 15px;\n  margin-right: auto;\n  margin-left: auto;\n}\n@media (min-width: 768px) {\n  .container {\n    width: 750px;\n  }\n}\n@media (min-width: 992px) {\n  .container {\n    width: 970px;\n  }\n}\n@media (min-width: 1200px) {\n  .container {\n    width: 1170px;\n  }\n}\n.container-fluid {\n  padding-right: 15px;\n  padding-left: 15px;\n  margin-right: auto;\n  margin-left: auto;\n}\n.row {\n  margin-right: -15px;\n  margin-left: -15px;\n}\n.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {\n  position: relative;\n  min-height: 1px;\n  padding-right: 15px;\n  padding-left: 15px;\n}\n.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {\n  float: left;\n}\n.col-xs-12 {\n  width: 100%;\n}\n.col-xs-11 {\n  width: 91.66666667%;\n}\n.col-xs-10 {\n  width: 83.33333333%;\n}\n.col-xs-9 {\n  width: 75%;\n}\n.col-xs-8 {\n  width: 66.66666667%;\n}\n.col-xs-7 {\n  width: 58.33333333%;\n}\n.col-xs-6 {\n  width: 50%;\n}\n.col-xs-5 {\n  width: 41.66666667%;\n}\n.col-xs-4 {\n  width: 33.33333333%;\n}\n.col-xs-3 {\n  width: 25%;\n}\n.col-xs-2 {\n  width: 16.66666667%;\n}\n.col-xs-1 {\n  width: 8.33333333%;\n}\n.col-xs-pull-12 {\n  right: 100%;\n}\n.col-xs-pull-11 {\n  right: 91.66666667%;\n}\n.col-xs-pull-10 {\n  right: 83.33333333%;\n}\n.col-xs-pull-9 {\n  right: 75%;\n}\n.col-xs-pull-8 {\n  right: 66.66666667%;\n}\n.col-xs-pull-7 {\n  right: 58.33333333%;\n}\n.col-xs-pull-6 {\n  right: 50%;\n}\n.col-xs-pull-5 {\n  right: 41.66666667%;\n}\n.col-xs-pull-4 {\n  right: 33.33333333%;\n}\n.col-xs-pull-3 {\n  right: 25%;\n}\n.col-xs-pull-2 {\n  right: 16.66666667%;\n}\n.col-xs-pull-1 {\n  right: 8.33333333%;\n}\n.col-xs-pull-0 {\n  right: auto;\n}\n.col-xs-push-12 {\n  left: 100%;\n}\n.col-xs-push-11 {\n  left: 91.66666667%;\n}\n.col-xs-push-10 {\n  left: 83.33333333%;\n}\n.col-xs-push-9 {\n  left: 75%;\n}\n.col-xs-push-8 {\n  left: 66.66666667%;\n}\n.col-xs-push-7 {\n  left: 58.33333333%;\n}\n.col-xs-push-6 {\n  left: 50%;\n}\n.col-xs-push-5 {\n  left: 41.66666667%;\n}\n.col-xs-push-4 {\n  left: 33.33333333%;\n}\n.col-xs-push-3 {\n  left: 25%;\n}\n.col-xs-push-2 {\n  left: 16.66666667%;\n}\n.col-xs-push-1 {\n  left: 8.33333333%;\n}\n.col-xs-push-0 {\n  left: auto;\n}\n.col-xs-offset-12 {\n  margin-left: 100%;\n}\n.col-xs-offset-11 {\n  margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n  margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n  margin-left: 75%;\n}\n.col-xs-offset-8 {\n  margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n  margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n  margin-left: 50%;\n}\n.col-xs-offset-5 {\n  margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n  margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n  margin-left: 25%;\n}\n.col-xs-offset-2 {\n  margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n  margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n  margin-left: 0;\n}\n@media (min-width: 768px) {\n  .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {\n    float: left;\n  }\n  .col-sm-12 {\n    width: 100%;\n  }\n  .col-sm-11 {\n    width: 91.66666667%;\n  }\n  .col-sm-10 {\n    width: 83.33333333%;\n  }\n  .col-sm-9 {\n    width: 75%;\n  }\n  .col-sm-8 {\n    width: 66.66666667%;\n  }\n  .col-sm-7 {\n    width: 58.33333333%;\n  }\n  .col-sm-6 {\n    width: 50%;\n  }\n  .col-sm-5 {\n    width: 41.66666667%;\n  }\n  .col-sm-4 {\n    width: 33.33333333%;\n  }\n  .col-sm-3 {\n    width: 25%;\n  }\n  .col-sm-2 {\n    width: 16.66666667%;\n  }\n  .col-sm-1 {\n    width: 8.33333333%;\n  }\n  .col-sm-pull-12 {\n    right: 100%;\n  }\n  .col-sm-pull-11 {\n    right: 91.66666667%;\n  }\n  .col-sm-pull-10 {\n    right: 83.33333333%;\n  }\n  .col-sm-pull-9 {\n    right: 75%;\n  }\n  .col-sm-pull-8 {\n    right: 66.66666667%;\n  }\n  .col-sm-pull-7 {\n    right: 58.33333333%;\n  }\n  .col-sm-pull-6 {\n    right: 50%;\n  }\n  .col-sm-pull-5 {\n    right: 41.66666667%;\n  }\n  .col-sm-pull-4 {\n    right: 33.33333333%;\n  }\n  .col-sm-pull-3 {\n    right: 25%;\n  }\n  .col-sm-pull-2 {\n    right: 16.66666667%;\n  }\n  .col-sm-pull-1 {\n    right: 8.33333333%;\n  }\n  .col-sm-pull-0 {\n    right: auto;\n  }\n  .col-sm-push-12 {\n    left: 100%;\n  }\n  .col-sm-push-11 {\n    left: 91.66666667%;\n  }\n  .col-sm-push-10 {\n    left: 83.33333333%;\n  }\n  .col-sm-push-9 {\n    left: 75%;\n  }\n  .col-sm-push-8 {\n    left: 66.66666667%;\n  }\n  .col-sm-push-7 {\n    left: 58.33333333%;\n  }\n  .col-sm-push-6 {\n    left: 50%;\n  }\n  .col-sm-push-5 {\n    left: 41.66666667%;\n  }\n  .col-sm-push-4 {\n    left: 33.33333333%;\n  }\n  .col-sm-push-3 {\n    left: 25%;\n  }\n  .col-sm-push-2 {\n    left: 16.66666667%;\n  }\n  .col-sm-push-1 {\n    left: 8.33333333%;\n  }\n  .col-sm-push-0 {\n    left: auto;\n  }\n  .col-sm-offset-12 {\n    margin-left: 100%;\n  }\n  .col-sm-offset-11 {\n    margin-left: 91.66666667%;\n  }\n  .col-sm-offset-10 {\n    margin-left: 83.33333333%;\n  }\n  .col-sm-offset-9 {\n    margin-left: 75%;\n  }\n  .col-sm-offset-8 {\n    margin-left: 66.66666667%;\n  }\n  .col-sm-offset-7 {\n    margin-left: 58.33333333%;\n  }\n  .col-sm-offset-6 {\n    margin-left: 50%;\n  }\n  .col-sm-offset-5 {\n    margin-left: 41.66666667%;\n  }\n  .col-sm-offset-4 {\n    margin-left: 33.33333333%;\n  }\n  .col-sm-offset-3 {\n    margin-left: 25%;\n  }\n  .col-sm-offset-2 {\n    margin-left: 16.66666667%;\n  }\n  .col-sm-offset-1 {\n    margin-left: 8.33333333%;\n  }\n  .col-sm-offset-0 {\n    margin-left: 0;\n  }\n}\n@media (min-width: 992px) {\n  .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {\n    float: left;\n  }\n  .col-md-12 {\n    width: 100%;\n  }\n  .col-md-11 {\n    width: 91.66666667%;\n  }\n  .col-md-10 {\n    width: 83.33333333%;\n  }\n  .col-md-9 {\n    width: 75%;\n  }\n  .col-md-8 {\n    width: 66.66666667%;\n  }\n  .col-md-7 {\n    width: 58.33333333%;\n  }\n  .col-md-6 {\n    width: 50%;\n  }\n  .col-md-5 {\n    width: 41.66666667%;\n  }\n  .col-md-4 {\n    width: 33.33333333%;\n  }\n  .col-md-3 {\n    width: 25%;\n  }\n  .col-md-2 {\n    width: 16.66666667%;\n  }\n  .col-md-1 {\n    width: 8.33333333%;\n  }\n  .col-md-pull-12 {\n    right: 100%;\n  }\n  .col-md-pull-11 {\n    right: 91.66666667%;\n  }\n  .col-md-pull-10 {\n    right: 83.33333333%;\n  }\n  .col-md-pull-9 {\n    right: 75%;\n  }\n  .col-md-pull-8 {\n    right: 66.66666667%;\n  }\n  .col-md-pull-7 {\n    right: 58.33333333%;\n  }\n  .col-md-pull-6 {\n    right: 50%;\n  }\n  .col-md-pull-5 {\n    right: 41.66666667%;\n  }\n  .col-md-pull-4 {\n    right: 33.33333333%;\n  }\n  .col-md-pull-3 {\n    right: 25%;\n  }\n  .col-md-pull-2 {\n    right: 16.66666667%;\n  }\n  .col-md-pull-1 {\n    right: 8.33333333%;\n  }\n  .col-md-pull-0 {\n    right: auto;\n  }\n  .col-md-push-12 {\n    left: 100%;\n  }\n  .col-md-push-11 {\n    left: 91.66666667%;\n  }\n  .col-md-push-10 {\n    left: 83.33333333%;\n  }\n  .col-md-push-9 {\n    left: 75%;\n  }\n  .col-md-push-8 {\n    left: 66.66666667%;\n  }\n  .col-md-push-7 {\n    left: 58.33333333%;\n  }\n  .col-md-push-6 {\n    left: 50%;\n  }\n  .col-md-push-5 {\n    left: 41.66666667%;\n  }\n  .col-md-push-4 {\n    left: 33.33333333%;\n  }\n  .col-md-push-3 {\n    left: 25%;\n  }\n  .col-md-push-2 {\n    left: 16.66666667%;\n  }\n  .col-md-push-1 {\n    left: 8.33333333%;\n  }\n  .col-md-push-0 {\n    left: auto;\n  }\n  .col-md-offset-12 {\n    margin-left: 100%;\n  }\n  .col-md-offset-11 {\n    margin-left: 91.66666667%;\n  }\n  .col-md-offset-10 {\n    margin-left: 83.33333333%;\n  }\n  .col-md-offset-9 {\n    margin-left: 75%;\n  }\n  .col-md-offset-8 {\n    margin-left: 66.66666667%;\n  }\n  .col-md-offset-7 {\n    margin-left: 58.33333333%;\n  }\n  .col-md-offset-6 {\n    margin-left: 50%;\n  }\n  .col-md-offset-5 {\n    margin-left: 41.66666667%;\n  }\n  .col-md-offset-4 {\n    margin-left: 33.33333333%;\n  }\n  .col-md-offset-3 {\n    margin-left: 25%;\n  }\n  .col-md-offset-2 {\n    margin-left: 16.66666667%;\n  }\n  .col-md-offset-1 {\n    margin-left: 8.33333333%;\n  }\n  .col-md-offset-0 {\n    margin-left: 0;\n  }\n}\n@media (min-width: 1200px) {\n  .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {\n    float: left;\n  }\n  .col-lg-12 {\n    width: 100%;\n  }\n  .col-lg-11 {\n    width: 91.66666667%;\n  }\n  .col-lg-10 {\n    width: 83.33333333%;\n  }\n  .col-lg-9 {\n    width: 75%;\n  }\n  .col-lg-8 {\n    width: 66.66666667%;\n  }\n  .col-lg-7 {\n    width: 58.33333333%;\n  }\n  .col-lg-6 {\n    width: 50%;\n  }\n  .col-lg-5 {\n    width: 41.66666667%;\n  }\n  .col-lg-4 {\n    width: 33.33333333%;\n  }\n  .col-lg-3 {\n    width: 25%;\n  }\n  .col-lg-2 {\n    width: 16.66666667%;\n  }\n  .col-lg-1 {\n    width: 8.33333333%;\n  }\n  .col-lg-pull-12 {\n    right: 100%;\n  }\n  .col-lg-pull-11 {\n    right: 91.66666667%;\n  }\n  .col-lg-pull-10 {\n    right: 83.33333333%;\n  }\n  .col-lg-pull-9 {\n    right: 75%;\n  }\n  .col-lg-pull-8 {\n    right: 66.66666667%;\n  }\n  .col-lg-pull-7 {\n    right: 58.33333333%;\n  }\n  .col-lg-pull-6 {\n    right: 50%;\n  }\n  .col-lg-pull-5 {\n    right: 41.66666667%;\n  }\n  .col-lg-pull-4 {\n    right: 33.33333333%;\n  }\n  .col-lg-pull-3 {\n    right: 25%;\n  }\n  .col-lg-pull-2 {\n    right: 16.66666667%;\n  }\n  .col-lg-pull-1 {\n    right: 8.33333333%;\n  }\n  .col-lg-pull-0 {\n    right: auto;\n  }\n  .col-lg-push-12 {\n    left: 100%;\n  }\n  .col-lg-push-11 {\n    left: 91.66666667%;\n  }\n  .col-lg-push-10 {\n    left: 83.33333333%;\n  }\n  .col-lg-push-9 {\n    left: 75%;\n  }\n  .col-lg-push-8 {\n    left: 66.66666667%;\n  }\n  .col-lg-push-7 {\n    left: 58.33333333%;\n  }\n  .col-lg-push-6 {\n    left: 50%;\n  }\n  .col-lg-push-5 {\n    left: 41.66666667%;\n  }\n  .col-lg-push-4 {\n    left: 33.33333333%;\n  }\n  .col-lg-push-3 {\n    left: 25%;\n  }\n  .col-lg-push-2 {\n    left: 16.66666667%;\n  }\n  .col-lg-push-1 {\n    left: 8.33333333%;\n  }\n  .col-lg-push-0 {\n    left: auto;\n  }\n  .col-lg-offset-12 {\n    margin-left: 100%;\n  }\n  .col-lg-offset-11 {\n    margin-left: 91.66666667%;\n  }\n  .col-lg-offset-10 {\n    margin-left: 83.33333333%;\n  }\n  .col-lg-offset-9 {\n    margin-left: 75%;\n  }\n  .col-lg-offset-8 {\n    margin-left: 66.66666667%;\n  }\n  .col-lg-offset-7 {\n    margin-left: 58.33333333%;\n  }\n  .col-lg-offset-6 {\n    margin-left: 50%;\n  }\n  .col-lg-offset-5 {\n    margin-left: 41.66666667%;\n  }\n  .col-lg-offset-4 {\n    margin-left: 33.33333333%;\n  }\n  .col-lg-offset-3 {\n    margin-left: 25%;\n  }\n  .col-lg-offset-2 {\n    margin-left: 16.66666667%;\n  }\n  .col-lg-offset-1 {\n    margin-left: 8.33333333%;\n  }\n  .col-lg-offset-0 {\n    margin-left: 0;\n  }\n}\ntable {\n  background-color: transparent;\n}\ncaption {\n  padding-top: 8px;\n  padding-bottom: 8px;\n  color: #777;\n  text-align: left;\n}\nth {\n  text-align: left;\n}\n.table {\n  width: 100%;\n  max-width: 100%;\n  margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n  padding: 8px;\n  line-height: 1.42857143;\n  vertical-align: top;\n  border-top: 1px solid #ddd;\n}\n.table > thead > tr > th {\n  vertical-align: bottom;\n  border-bottom: 2px solid #ddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n  border-top: 0;\n}\n.table > tbody + tbody {\n  border-top: 2px solid #ddd;\n}\n.table .table {\n  background-color: #fff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n  padding: 5px;\n}\n.table-bordered {\n  border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n  border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n  border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-of-type(odd) {\n  background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n  background-color: #f5f5f5;\n}\ntable col[class*=\"col-\"] {\n  position: static;\n  display: table-column;\n  float: none;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n  position: static;\n  display: table-cell;\n  float: none;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n  background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n  background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n  background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n  background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n  background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n  background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n  background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n  background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n  background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n  background-color: #ebcccc;\n}\n.table-responsive {\n  min-height: .01%;\n  overflow-x: auto;\n}\n@media screen and (max-width: 767px) {\n  .table-responsive {\n    width: 100%;\n    margin-bottom: 15px;\n    overflow-y: hidden;\n    -ms-overflow-style: -ms-autohiding-scrollbar;\n    border: 1px solid #ddd;\n  }\n  .table-responsive > .table {\n    margin-bottom: 0;\n  }\n  .table-responsive > .table > thead > tr > th,\n  .table-responsive > .table > tbody > tr > th,\n  .table-responsive > .table > tfoot > tr > th,\n  .table-responsive > .table > thead > tr > td,\n  .table-responsive > .table > tbody > tr > td,\n  .table-responsive > .table > tfoot > tr > td {\n    white-space: nowrap;\n  }\n  .table-responsive > .table-bordered {\n    border: 0;\n  }\n  .table-responsive > .table-bordered > thead > tr > th:first-child,\n  .table-responsive > .table-bordered > tbody > tr > th:first-child,\n  .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n  .table-responsive > .table-bordered > thead > tr > td:first-child,\n  .table-responsive > .table-bordered > tbody > tr > td:first-child,\n  .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n    border-left: 0;\n  }\n  .table-responsive > .table-bordered > thead > tr > th:last-child,\n  .table-responsive > .table-bordered > tbody > tr > th:last-child,\n  .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n  .table-responsive > .table-bordered > thead > tr > td:last-child,\n  .table-responsive > .table-bordered > tbody > tr > td:last-child,\n  .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n    border-right: 0;\n  }\n  .table-responsive > .table-bordered > tbody > tr:last-child > th,\n  .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n  .table-responsive > .table-bordered > tbody > tr:last-child > td,\n  .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n    border-bottom: 0;\n  }\n}\nfieldset {\n  min-width: 0;\n  padding: 0;\n  margin: 0;\n  border: 0;\n}\nlegend {\n  display: block;\n  width: 100%;\n  padding: 0;\n  margin-bottom: 20px;\n  font-size: 21px;\n  line-height: inherit;\n  color: #333;\n  border: 0;\n  border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n  display: inline-block;\n  max-width: 100%;\n  margin-bottom: 5px;\n  font-weight: bold;\n}\ninput[type=\"search\"] {\n  -webkit-box-sizing: border-box;\n     -moz-box-sizing: border-box;\n          box-sizing: border-box;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n  margin: 4px 0 0;\n  margin-top: 1px \\9;\n  line-height: normal;\n}\ninput[type=\"file\"] {\n  display: block;\n}\ninput[type=\"range\"] {\n  display: block;\n  width: 100%;\n}\nselect[multiple],\nselect[size] {\n  height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n  outline: 5px auto -webkit-focus-ring-color;\n  outline-offset: -2px;\n}\noutput {\n  display: block;\n  padding-top: 7px;\n  font-size: 14px;\n  line-height: 1.42857143;\n  color: #555;\n}\n.form-control {\n  display: block;\n  width: 100%;\n  height: 34px;\n  padding: 6px 12px;\n  font-size: 14px;\n  line-height: 1.42857143;\n  color: #555;\n  background-color: #fff;\n  background-image: none;\n  border: 1px solid #ccc;\n  border-radius: 4px;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n  -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;\n       -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n          transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n  border-color: #66afe9;\n  outline: 0;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);\n          box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);\n}\n.form-control::-moz-placeholder {\n  color: #999;\n  opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n  color: #999;\n}\n.form-control::-webkit-input-placeholder {\n  color: #999;\n}\n.form-control::-ms-expand {\n  background-color: transparent;\n  border: 0;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n  background-color: #eee;\n  opacity: 1;\n}\n.form-control[disabled],\nfieldset[disabled] .form-control {\n  cursor: not-allowed;\n}\ntextarea.form-control {\n  height: auto;\n}\ninput[type=\"search\"] {\n  -webkit-appearance: none;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n  input[type=\"date\"].form-control,\n  input[type=\"time\"].form-control,\n  input[type=\"datetime-local\"].form-control,\n  input[type=\"month\"].form-control {\n    line-height: 34px;\n  }\n  input[type=\"date\"].input-sm,\n  input[type=\"time\"].input-sm,\n  input[type=\"datetime-local\"].input-sm,\n  input[type=\"month\"].input-sm,\n  .input-group-sm input[type=\"date\"],\n  .input-group-sm input[type=\"time\"],\n  .input-group-sm input[type=\"datetime-local\"],\n  .input-group-sm input[type=\"month\"] {\n    line-height: 30px;\n  }\n  input[type=\"date\"].input-lg,\n  input[type=\"time\"].input-lg,\n  input[type=\"datetime-local\"].input-lg,\n  input[type=\"month\"].input-lg,\n  .input-group-lg input[type=\"date\"],\n  .input-group-lg input[type=\"time\"],\n  .input-group-lg input[type=\"datetime-local\"],\n  .input-group-lg input[type=\"month\"] {\n    line-height: 46px;\n  }\n}\n.form-group {\n  margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n  position: relative;\n  display: block;\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\n.radio label,\n.checkbox label {\n  min-height: 20px;\n  padding-left: 20px;\n  margin-bottom: 0;\n  font-weight: normal;\n  cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n  position: absolute;\n  margin-top: 4px \\9;\n  margin-left: -20px;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n  margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n  position: relative;\n  display: inline-block;\n  padding-left: 20px;\n  margin-bottom: 0;\n  font-weight: normal;\n  vertical-align: middle;\n  cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n  margin-top: 0;\n  margin-left: 10px;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n  cursor: not-allowed;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n  cursor: not-allowed;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n  cursor: not-allowed;\n}\n.form-control-static {\n  min-height: 34px;\n  padding-top: 7px;\n  padding-bottom: 7px;\n  margin-bottom: 0;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n  padding-right: 0;\n  padding-left: 0;\n}\n.input-sm {\n  height: 30px;\n  padding: 5px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n  border-radius: 3px;\n}\nselect.input-sm {\n  height: 30px;\n  line-height: 30px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n  height: auto;\n}\n.form-group-sm .form-control {\n  height: 30px;\n  padding: 5px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n  border-radius: 3px;\n}\n.form-group-sm select.form-control {\n  height: 30px;\n  line-height: 30px;\n}\n.form-group-sm textarea.form-control,\n.form-group-sm select[multiple].form-control {\n  height: auto;\n}\n.form-group-sm .form-control-static {\n  height: 30px;\n  min-height: 32px;\n  padding: 6px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n}\n.input-lg {\n  height: 46px;\n  padding: 10px 16px;\n  font-size: 18px;\n  line-height: 1.3333333;\n  border-radius: 6px;\n}\nselect.input-lg {\n  height: 46px;\n  line-height: 46px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n  height: auto;\n}\n.form-group-lg .form-control {\n  height: 46px;\n  padding: 10px 16px;\n  font-size: 18px;\n  line-height: 1.3333333;\n  border-radius: 6px;\n}\n.form-group-lg select.form-control {\n  height: 46px;\n  line-height: 46px;\n}\n.form-group-lg textarea.form-control,\n.form-group-lg select[multiple].form-control {\n  height: auto;\n}\n.form-group-lg .form-control-static {\n  height: 46px;\n  min-height: 38px;\n  padding: 11px 16px;\n  font-size: 18px;\n  line-height: 1.3333333;\n}\n.has-feedback {\n  position: relative;\n}\n.has-feedback .form-control {\n  padding-right: 42.5px;\n}\n.form-control-feedback {\n  position: absolute;\n  top: 0;\n  right: 0;\n  z-index: 2;\n  display: block;\n  width: 34px;\n  height: 34px;\n  line-height: 34px;\n  text-align: center;\n  pointer-events: none;\n}\n.input-lg + .form-control-feedback,\n.input-group-lg + .form-control-feedback,\n.form-group-lg .form-control + .form-control-feedback {\n  width: 46px;\n  height: 46px;\n  line-height: 46px;\n}\n.input-sm + .form-control-feedback,\n.input-group-sm + .form-control-feedback,\n.form-group-sm .form-control + .form-control-feedback {\n  width: 30px;\n  height: 30px;\n  line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n  color: #3c763d;\n}\n.has-success .form-control {\n  border-color: #3c763d;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n}\n.has-success .form-control:focus {\n  border-color: #2b542c;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168;\n          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n  color: #3c763d;\n  background-color: #dff0d8;\n  border-color: #3c763d;\n}\n.has-success .form-control-feedback {\n  color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n  color: #8a6d3b;\n}\n.has-warning .form-control {\n  border-color: #8a6d3b;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n}\n.has-warning .form-control:focus {\n  border-color: #66512c;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b;\n          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n  color: #8a6d3b;\n  background-color: #fcf8e3;\n  border-color: #8a6d3b;\n}\n.has-warning .form-control-feedback {\n  color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n  color: #a94442;\n}\n.has-error .form-control {\n  border-color: #a94442;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n}\n.has-error .form-control:focus {\n  border-color: #843534;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483;\n          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n  color: #a94442;\n  background-color: #f2dede;\n  border-color: #a94442;\n}\n.has-error .form-control-feedback {\n  color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n  top: 25px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n  top: 0;\n}\n.help-block {\n  display: block;\n  margin-top: 5px;\n  margin-bottom: 10px;\n  color: #737373;\n}\n@media (min-width: 768px) {\n  .form-inline .form-group {\n    display: inline-block;\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .form-inline .form-control {\n    display: inline-block;\n    width: auto;\n    vertical-align: middle;\n  }\n  .form-inline .form-control-static {\n    display: inline-block;\n  }\n  .form-inline .input-group {\n    display: inline-table;\n    vertical-align: middle;\n  }\n  .form-inline .input-group .input-group-addon,\n  .form-inline .input-group .input-group-btn,\n  .form-inline .input-group .form-control {\n    width: auto;\n  }\n  .form-inline .input-group > .form-control {\n    width: 100%;\n  }\n  .form-inline .control-label {\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .form-inline .radio,\n  .form-inline .checkbox {\n    display: inline-block;\n    margin-top: 0;\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .form-inline .radio label,\n  .form-inline .checkbox label {\n    padding-left: 0;\n  }\n  .form-inline .radio input[type=\"radio\"],\n  .form-inline .checkbox input[type=\"checkbox\"] {\n    position: relative;\n    margin-left: 0;\n  }\n  .form-inline .has-feedback .form-control-feedback {\n    top: 0;\n  }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n  padding-top: 7px;\n  margin-top: 0;\n  margin-bottom: 0;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n  min-height: 27px;\n}\n.form-horizontal .form-group {\n  margin-right: -15px;\n  margin-left: -15px;\n}\n@media (min-width: 768px) {\n  .form-horizontal .control-label {\n    padding-top: 7px;\n    margin-bottom: 0;\n    text-align: right;\n  }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n  right: 15px;\n}\n@media (min-width: 768px) {\n  .form-horizontal .form-group-lg .control-label {\n    padding-top: 11px;\n    font-size: 18px;\n  }\n}\n@media (min-width: 768px) {\n  .form-horizontal .form-group-sm .control-label {\n    padding-top: 6px;\n    font-size: 12px;\n  }\n}\n.btn {\n  display: inline-block;\n  padding: 6px 12px;\n  margin-bottom: 0;\n  font-size: 14px;\n  font-weight: normal;\n  line-height: 1.42857143;\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: middle;\n  -ms-touch-action: manipulation;\n      touch-action: manipulation;\n  cursor: pointer;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n      -ms-user-select: none;\n          user-select: none;\n  background-image: none;\n  border: 1px solid transparent;\n  border-radius: 4px;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n  outline: 5px auto -webkit-focus-ring-color;\n  outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n  color: #333;\n  text-decoration: none;\n}\n.btn:active,\n.btn.active {\n  background-image: none;\n  outline: 0;\n  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);\n          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n  cursor: not-allowed;\n  filter: alpha(opacity=65);\n  -webkit-box-shadow: none;\n          box-shadow: none;\n  opacity: .65;\n}\na.btn.disabled,\nfieldset[disabled] a.btn {\n  pointer-events: none;\n}\n.btn-default {\n  color: #333;\n  background-color: #fff;\n  border-color: #ccc;\n}\n.btn-default:focus,\n.btn-default.focus {\n  color: #333;\n  background-color: #e6e6e6;\n  border-color: #8c8c8c;\n}\n.btn-default:hover {\n  color: #333;\n  background-color: #e6e6e6;\n  border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n  color: #333;\n  background-color: #e6e6e6;\n  border-color: #adadad;\n}\n.btn-default:active:hover,\n.btn-default.active:hover,\n.open > .dropdown-toggle.btn-default:hover,\n.btn-default:active:focus,\n.btn-default.active:focus,\n.open > .dropdown-toggle.btn-default:focus,\n.btn-default:active.focus,\n.btn-default.active.focus,\n.open > .dropdown-toggle.btn-default.focus {\n  color: #333;\n  background-color: #d4d4d4;\n  border-color: #8c8c8c;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n  background-image: none;\n}\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus {\n  background-color: #fff;\n  border-color: #ccc;\n}\n.btn-default .badge {\n  color: #fff;\n  background-color: #333;\n}\n.btn-primary {\n  color: #fff;\n  background-color: #337ab7;\n  border-color: #2e6da4;\n}\n.btn-primary:focus,\n.btn-primary.focus {\n  color: #fff;\n  background-color: #286090;\n  border-color: #122b40;\n}\n.btn-primary:hover {\n  color: #fff;\n  background-color: #286090;\n  border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n  color: #fff;\n  background-color: #286090;\n  border-color: #204d74;\n}\n.btn-primary:active:hover,\n.btn-primary.active:hover,\n.open > .dropdown-toggle.btn-primary:hover,\n.btn-primary:active:focus,\n.btn-primary.active:focus,\n.open > .dropdown-toggle.btn-primary:focus,\n.btn-primary:active.focus,\n.btn-primary.active.focus,\n.open > .dropdown-toggle.btn-primary.focus {\n  color: #fff;\n  background-color: #204d74;\n  border-color: #122b40;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n  background-image: none;\n}\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus {\n  background-color: #337ab7;\n  border-color: #2e6da4;\n}\n.btn-primary .badge {\n  color: #337ab7;\n  background-color: #fff;\n}\n.btn-success {\n  color: #fff;\n  background-color: #5cb85c;\n  border-color: #4cae4c;\n}\n.btn-success:focus,\n.btn-success.focus {\n  color: #fff;\n  background-color: #449d44;\n  border-color: #255625;\n}\n.btn-success:hover {\n  color: #fff;\n  background-color: #449d44;\n  border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n  color: #fff;\n  background-color: #449d44;\n  border-color: #398439;\n}\n.btn-success:active:hover,\n.btn-success.active:hover,\n.open > .dropdown-toggle.btn-success:hover,\n.btn-success:active:focus,\n.btn-success.active:focus,\n.open > .dropdown-toggle.btn-success:focus,\n.btn-success:active.focus,\n.btn-success.active.focus,\n.open > .dropdown-toggle.btn-success.focus {\n  color: #fff;\n  background-color: #398439;\n  border-color: #255625;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n  background-image: none;\n}\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus {\n  background-color: #5cb85c;\n  border-color: #4cae4c;\n}\n.btn-success .badge {\n  color: #5cb85c;\n  background-color: #fff;\n}\n.btn-info {\n  color: #fff;\n  background-color: #5bc0de;\n  border-color: #46b8da;\n}\n.btn-info:focus,\n.btn-info.focus {\n  color: #fff;\n  background-color: #31b0d5;\n  border-color: #1b6d85;\n}\n.btn-info:hover {\n  color: #fff;\n  background-color: #31b0d5;\n  border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n  color: #fff;\n  background-color: #31b0d5;\n  border-color: #269abc;\n}\n.btn-info:active:hover,\n.btn-info.active:hover,\n.open > .dropdown-toggle.btn-info:hover,\n.btn-info:active:focus,\n.btn-info.active:focus,\n.open > .dropdown-toggle.btn-info:focus,\n.btn-info:active.focus,\n.btn-info.active.focus,\n.open > .dropdown-toggle.btn-info.focus {\n  color: #fff;\n  background-color: #269abc;\n  border-color: #1b6d85;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n  background-image: none;\n}\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus {\n  background-color: #5bc0de;\n  border-color: #46b8da;\n}\n.btn-info .badge {\n  color: #5bc0de;\n  background-color: #fff;\n}\n.btn-warning {\n  color: #fff;\n  background-color: #f0ad4e;\n  border-color: #eea236;\n}\n.btn-warning:focus,\n.btn-warning.focus {\n  color: #fff;\n  background-color: #ec971f;\n  border-color: #985f0d;\n}\n.btn-warning:hover {\n  color: #fff;\n  background-color: #ec971f;\n  border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n  color: #fff;\n  background-color: #ec971f;\n  border-color: #d58512;\n}\n.btn-warning:active:hover,\n.btn-warning.active:hover,\n.open > .dropdown-toggle.btn-warning:hover,\n.btn-warning:active:focus,\n.btn-warning.active:focus,\n.open > .dropdown-toggle.btn-warning:focus,\n.btn-warning:active.focus,\n.btn-warning.active.focus,\n.open > .dropdown-toggle.btn-warning.focus {\n  color: #fff;\n  background-color: #d58512;\n  border-color: #985f0d;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n  background-image: none;\n}\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus {\n  background-color: #f0ad4e;\n  border-color: #eea236;\n}\n.btn-warning .badge {\n  color: #f0ad4e;\n  background-color: #fff;\n}\n.btn-danger {\n  color: #fff;\n  background-color: #d9534f;\n  border-color: #d43f3a;\n}\n.btn-danger:focus,\n.btn-danger.focus {\n  color: #fff;\n  background-color: #c9302c;\n  border-color: #761c19;\n}\n.btn-danger:hover {\n  color: #fff;\n  background-color: #c9302c;\n  border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n  color: #fff;\n  background-color: #c9302c;\n  border-color: #ac2925;\n}\n.btn-danger:active:hover,\n.btn-danger.active:hover,\n.open > .dropdown-toggle.btn-danger:hover,\n.btn-danger:active:focus,\n.btn-danger.active:focus,\n.open > .dropdown-toggle.btn-danger:focus,\n.btn-danger:active.focus,\n.btn-danger.active.focus,\n.open > .dropdown-toggle.btn-danger.focus {\n  color: #fff;\n  background-color: #ac2925;\n  border-color: #761c19;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n  background-image: none;\n}\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus {\n  background-color: #d9534f;\n  border-color: #d43f3a;\n}\n.btn-danger .badge {\n  color: #d9534f;\n  background-color: #fff;\n}\n.btn-link {\n  font-weight: normal;\n  color: #337ab7;\n  border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n  background-color: transparent;\n  -webkit-box-shadow: none;\n          box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n  border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n  color: #23527c;\n  text-decoration: underline;\n  background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n  color: #777;\n  text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n  padding: 10px 16px;\n  font-size: 18px;\n  line-height: 1.3333333;\n  border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n  padding: 5px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n  border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n  padding: 1px 5px;\n  font-size: 12px;\n  line-height: 1.5;\n  border-radius: 3px;\n}\n.btn-block {\n  display: block;\n  width: 100%;\n}\n.btn-block + .btn-block {\n  margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n  width: 100%;\n}\n.fade {\n  opacity: 0;\n  -webkit-transition: opacity .15s linear;\n       -o-transition: opacity .15s linear;\n          transition: opacity .15s linear;\n}\n.fade.in {\n  opacity: 1;\n}\n.collapse {\n  display: none;\n}\n.collapse.in {\n  display: block;\n}\ntr.collapse.in {\n  display: table-row;\n}\ntbody.collapse.in {\n  display: table-row-group;\n}\n.collapsing {\n  position: relative;\n  height: 0;\n  overflow: hidden;\n  -webkit-transition-timing-function: ease;\n       -o-transition-timing-function: ease;\n          transition-timing-function: ease;\n  -webkit-transition-duration: .35s;\n       -o-transition-duration: .35s;\n          transition-duration: .35s;\n  -webkit-transition-property: height, visibility;\n       -o-transition-property: height, visibility;\n          transition-property: height, visibility;\n}\n.caret {\n  display: inline-block;\n  width: 0;\n  height: 0;\n  margin-left: 2px;\n  vertical-align: middle;\n  border-top: 4px dashed;\n  border-top: 4px solid \\9;\n  border-right: 4px solid transparent;\n  border-left: 4px solid transparent;\n}\n.dropup,\n.dropdown {\n  position: relative;\n}\n.dropdown-toggle:focus {\n  outline: 0;\n}\n.dropdown-menu {\n  position: absolute;\n  top: 100%;\n  left: 0;\n  z-index: 1000;\n  display: none;\n  float: left;\n  min-width: 160px;\n  padding: 5px 0;\n  margin: 2px 0 0;\n  font-size: 14px;\n  text-align: left;\n  list-style: none;\n  background-color: #fff;\n  -webkit-background-clip: padding-box;\n          background-clip: padding-box;\n  border: 1px solid #ccc;\n  border: 1px solid rgba(0, 0, 0, .15);\n  border-radius: 4px;\n  -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);\n          box-shadow: 0 6px 12px rgba(0, 0, 0, .175);\n}\n.dropdown-menu.pull-right {\n  right: 0;\n  left: auto;\n}\n.dropdown-menu .divider {\n  height: 1px;\n  margin: 9px 0;\n  overflow: hidden;\n  background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n  display: block;\n  padding: 3px 20px;\n  clear: both;\n  font-weight: normal;\n  line-height: 1.42857143;\n  color: #333;\n  white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n  color: #262626;\n  text-decoration: none;\n  background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n  color: #fff;\n  text-decoration: none;\n  background-color: #337ab7;\n  outline: 0;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n  color: #777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n  text-decoration: none;\n  cursor: not-allowed;\n  background-color: transparent;\n  background-image: none;\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n}\n.open > .dropdown-menu {\n  display: block;\n}\n.open > a {\n  outline: 0;\n}\n.dropdown-menu-right {\n  right: 0;\n  left: auto;\n}\n.dropdown-menu-left {\n  right: auto;\n  left: 0;\n}\n.dropdown-header {\n  display: block;\n  padding: 3px 20px;\n  font-size: 12px;\n  line-height: 1.42857143;\n  color: #777;\n  white-space: nowrap;\n}\n.dropdown-backdrop {\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 990;\n}\n.pull-right > .dropdown-menu {\n  right: 0;\n  left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n  content: \"\";\n  border-top: 0;\n  border-bottom: 4px dashed;\n  border-bottom: 4px solid \\9;\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n  top: auto;\n  bottom: 100%;\n  margin-bottom: 2px;\n}\n@media (min-width: 768px) {\n  .navbar-right .dropdown-menu {\n    right: 0;\n    left: auto;\n  }\n  .navbar-right .dropdown-menu-left {\n    right: auto;\n    left: 0;\n  }\n}\n.btn-group,\n.btn-group-vertical {\n  position: relative;\n  display: inline-block;\n  vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n  position: relative;\n  float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n  z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n  margin-left: -1px;\n}\n.btn-toolbar {\n  margin-left: -5px;\n}\n.btn-toolbar .btn,\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n  float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n  margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n  border-radius: 0;\n}\n.btn-group > .btn:first-child {\n  margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n  border-top-left-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.btn-group > .btn-group {\n  float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n  border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n  border-top-left-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n  outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n  padding-right: 8px;\n  padding-left: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n  padding-right: 12px;\n  padding-left: 12px;\n}\n.btn-group.open .dropdown-toggle {\n  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);\n          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n  -webkit-box-shadow: none;\n          box-shadow: none;\n}\n.btn .caret {\n  margin-left: 0;\n}\n.btn-lg .caret {\n  border-width: 5px 5px 0;\n  border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n  border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n  display: block;\n  float: none;\n  width: 100%;\n  max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n  float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n  margin-top: -1px;\n  margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n  border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n  border-top-left-radius: 4px;\n  border-top-right-radius: 4px;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 4px;\n  border-bottom-left-radius: 4px;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n  border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n}\n.btn-group-justified {\n  display: table;\n  width: 100%;\n  table-layout: fixed;\n  border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n  display: table-cell;\n  float: none;\n  width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n  width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n  left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n  position: absolute;\n  clip: rect(0, 0, 0, 0);\n  pointer-events: none;\n}\n.input-group {\n  position: relative;\n  display: table;\n  border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n  float: none;\n  padding-right: 0;\n  padding-left: 0;\n}\n.input-group .form-control {\n  position: relative;\n  z-index: 2;\n  float: left;\n  width: 100%;\n  margin-bottom: 0;\n}\n.input-group .form-control:focus {\n  z-index: 3;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n  height: 46px;\n  padding: 10px 16px;\n  font-size: 18px;\n  line-height: 1.3333333;\n  border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n  height: 46px;\n  line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n  height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n  height: 30px;\n  padding: 5px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n  border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n  height: 30px;\n  line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n  height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n  display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n  border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n  width: 1%;\n  white-space: nowrap;\n  vertical-align: middle;\n}\n.input-group-addon {\n  padding: 6px 12px;\n  font-size: 14px;\n  font-weight: normal;\n  line-height: 1;\n  color: #555;\n  text-align: center;\n  background-color: #eee;\n  border: 1px solid #ccc;\n  border-radius: 4px;\n}\n.input-group-addon.input-sm {\n  padding: 5px 10px;\n  font-size: 12px;\n  border-radius: 3px;\n}\n.input-group-addon.input-lg {\n  padding: 10px 16px;\n  font-size: 18px;\n  border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n  margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n}\n.input-group-addon:first-child {\n  border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n  border-top-left-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.input-group-addon:last-child {\n  border-left: 0;\n}\n.input-group-btn {\n  position: relative;\n  font-size: 0;\n  white-space: nowrap;\n}\n.input-group-btn > .btn {\n  position: relative;\n}\n.input-group-btn > .btn + .btn {\n  margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n  z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n  margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n  z-index: 2;\n  margin-left: -1px;\n}\n.nav {\n  padding-left: 0;\n  margin-bottom: 0;\n  list-style: none;\n}\n.nav > li {\n  position: relative;\n  display: block;\n}\n.nav > li > a {\n  position: relative;\n  display: block;\n  padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n  text-decoration: none;\n  background-color: #eee;\n}\n.nav > li.disabled > a {\n  color: #777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n  color: #777;\n  text-decoration: none;\n  cursor: not-allowed;\n  background-color: transparent;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n  background-color: #eee;\n  border-color: #337ab7;\n}\n.nav .nav-divider {\n  height: 1px;\n  margin: 9px 0;\n  overflow: hidden;\n  background-color: #e5e5e5;\n}\n.nav > li > a > img {\n  max-width: none;\n}\n.nav-tabs {\n  border-bottom: 1px solid #ddd;\n}\n.nav-tabs > li {\n  float: left;\n  margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n  margin-right: 2px;\n  line-height: 1.42857143;\n  border: 1px solid transparent;\n  border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n  border-color: #eee #eee #ddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n  color: #555;\n  cursor: default;\n  background-color: #fff;\n  border: 1px solid #ddd;\n  border-bottom-color: transparent;\n}\n.nav-tabs.nav-justified {\n  width: 100%;\n  border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n  float: none;\n}\n.nav-tabs.nav-justified > li > a {\n  margin-bottom: 5px;\n  text-align: center;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n  top: auto;\n  left: auto;\n}\n@media (min-width: 768px) {\n  .nav-tabs.nav-justified > li {\n    display: table-cell;\n    width: 1%;\n  }\n  .nav-tabs.nav-justified > li > a {\n    margin-bottom: 0;\n  }\n}\n.nav-tabs.nav-justified > li > a {\n  margin-right: 0;\n  border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n  border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n  .nav-tabs.nav-justified > li > a {\n    border-bottom: 1px solid #ddd;\n    border-radius: 4px 4px 0 0;\n  }\n  .nav-tabs.nav-justified > .active > a,\n  .nav-tabs.nav-justified > .active > a:hover,\n  .nav-tabs.nav-justified > .active > a:focus {\n    border-bottom-color: #fff;\n  }\n}\n.nav-pills > li {\n  float: left;\n}\n.nav-pills > li > a {\n  border-radius: 4px;\n}\n.nav-pills > li + li {\n  margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n  color: #fff;\n  background-color: #337ab7;\n}\n.nav-stacked > li {\n  float: none;\n}\n.nav-stacked > li + li {\n  margin-top: 2px;\n  margin-left: 0;\n}\n.nav-justified {\n  width: 100%;\n}\n.nav-justified > li {\n  float: none;\n}\n.nav-justified > li > a {\n  margin-bottom: 5px;\n  text-align: center;\n}\n.nav-justified > .dropdown .dropdown-menu {\n  top: auto;\n  left: auto;\n}\n@media (min-width: 768px) {\n  .nav-justified > li {\n    display: table-cell;\n    width: 1%;\n  }\n  .nav-justified > li > a {\n    margin-bottom: 0;\n  }\n}\n.nav-tabs-justified {\n  border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n  margin-right: 0;\n  border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n  border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n  .nav-tabs-justified > li > a {\n    border-bottom: 1px solid #ddd;\n    border-radius: 4px 4px 0 0;\n  }\n  .nav-tabs-justified > .active > a,\n  .nav-tabs-justified > .active > a:hover,\n  .nav-tabs-justified > .active > a:focus {\n    border-bottom-color: #fff;\n  }\n}\n.tab-content > .tab-pane {\n  display: none;\n}\n.tab-content > .active {\n  display: block;\n}\n.nav-tabs .dropdown-menu {\n  margin-top: -1px;\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n}\n.navbar {\n  position: relative;\n  min-height: 50px;\n  margin-bottom: 20px;\n  border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n  .navbar {\n    border-radius: 4px;\n  }\n}\n@media (min-width: 768px) {\n  .navbar-header {\n    float: left;\n  }\n}\n.navbar-collapse {\n  padding-right: 15px;\n  padding-left: 15px;\n  overflow-x: visible;\n  -webkit-overflow-scrolling: touch;\n  border-top: 1px solid transparent;\n  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1);\n          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1);\n}\n.navbar-collapse.in {\n  overflow-y: auto;\n}\n@media (min-width: 768px) {\n  .navbar-collapse {\n    width: auto;\n    border-top: 0;\n    -webkit-box-shadow: none;\n            box-shadow: none;\n  }\n  .navbar-collapse.collapse {\n    display: block !important;\n    height: auto !important;\n    padding-bottom: 0;\n    overflow: visible !important;\n  }\n  .navbar-collapse.in {\n    overflow-y: visible;\n  }\n  .navbar-fixed-top .navbar-collapse,\n  .navbar-static-top .navbar-collapse,\n  .navbar-fixed-bottom .navbar-collapse {\n    padding-right: 0;\n    padding-left: 0;\n  }\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n  max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n  .navbar-fixed-top .navbar-collapse,\n  .navbar-fixed-bottom .navbar-collapse {\n    max-height: 200px;\n  }\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n  margin-right: -15px;\n  margin-left: -15px;\n}\n@media (min-width: 768px) {\n  .container > .navbar-header,\n  .container-fluid > .navbar-header,\n  .container > .navbar-collapse,\n  .container-fluid > .navbar-collapse {\n    margin-right: 0;\n    margin-left: 0;\n  }\n}\n.navbar-static-top {\n  z-index: 1000;\n  border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n  .navbar-static-top {\n    border-radius: 0;\n  }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n  position: fixed;\n  right: 0;\n  left: 0;\n  z-index: 1030;\n}\n@media (min-width: 768px) {\n  .navbar-fixed-top,\n  .navbar-fixed-bottom {\n    border-radius: 0;\n  }\n}\n.navbar-fixed-top {\n  top: 0;\n  border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n  bottom: 0;\n  margin-bottom: 0;\n  border-width: 1px 0 0;\n}\n.navbar-brand {\n  float: left;\n  height: 50px;\n  padding: 15px 15px;\n  font-size: 18px;\n  line-height: 20px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n  text-decoration: none;\n}\n.navbar-brand > img {\n  display: block;\n}\n@media (min-width: 768px) {\n  .navbar > .container .navbar-brand,\n  .navbar > .container-fluid .navbar-brand {\n    margin-left: -15px;\n  }\n}\n.navbar-toggle {\n  position: relative;\n  float: right;\n  padding: 9px 10px;\n  margin-top: 8px;\n  margin-right: 15px;\n  margin-bottom: 8px;\n  background-color: transparent;\n  background-image: none;\n  border: 1px solid transparent;\n  border-radius: 4px;\n}\n.navbar-toggle:focus {\n  outline: 0;\n}\n.navbar-toggle .icon-bar {\n  display: block;\n  width: 22px;\n  height: 2px;\n  border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n  margin-top: 4px;\n}\n@media (min-width: 768px) {\n  .navbar-toggle {\n    display: none;\n  }\n}\n.navbar-nav {\n  margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n  padding-top: 10px;\n  padding-bottom: 10px;\n  line-height: 20px;\n}\n@media (max-width: 767px) {\n  .navbar-nav .open .dropdown-menu {\n    position: static;\n    float: none;\n    width: auto;\n    margin-top: 0;\n    background-color: transparent;\n    border: 0;\n    -webkit-box-shadow: none;\n            box-shadow: none;\n  }\n  .navbar-nav .open .dropdown-menu > li > a,\n  .navbar-nav .open .dropdown-menu .dropdown-header {\n    padding: 5px 15px 5px 25px;\n  }\n  .navbar-nav .open .dropdown-menu > li > a {\n    line-height: 20px;\n  }\n  .navbar-nav .open .dropdown-menu > li > a:hover,\n  .navbar-nav .open .dropdown-menu > li > a:focus {\n    background-image: none;\n  }\n}\n@media (min-width: 768px) {\n  .navbar-nav {\n    float: left;\n    margin: 0;\n  }\n  .navbar-nav > li {\n    float: left;\n  }\n  .navbar-nav > li > a {\n    padding-top: 15px;\n    padding-bottom: 15px;\n  }\n}\n.navbar-form {\n  padding: 10px 15px;\n  margin-top: 8px;\n  margin-right: -15px;\n  margin-bottom: 8px;\n  margin-left: -15px;\n  border-top: 1px solid transparent;\n  border-bottom: 1px solid transparent;\n  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1);\n          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1);\n}\n@media (min-width: 768px) {\n  .navbar-form .form-group {\n    display: inline-block;\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .navbar-form .form-control {\n    display: inline-block;\n    width: auto;\n    vertical-align: middle;\n  }\n  .navbar-form .form-control-static {\n    display: inline-block;\n  }\n  .navbar-form .input-group {\n    display: inline-table;\n    vertical-align: middle;\n  }\n  .navbar-form .input-group .input-group-addon,\n  .navbar-form .input-group .input-group-btn,\n  .navbar-form .input-group .form-control {\n    width: auto;\n  }\n  .navbar-form .input-group > .form-control {\n    width: 100%;\n  }\n  .navbar-form .control-label {\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .navbar-form .radio,\n  .navbar-form .checkbox {\n    display: inline-block;\n    margin-top: 0;\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .navbar-form .radio label,\n  .navbar-form .checkbox label {\n    padding-left: 0;\n  }\n  .navbar-form .radio input[type=\"radio\"],\n  .navbar-form .checkbox input[type=\"checkbox\"] {\n    position: relative;\n    margin-left: 0;\n  }\n  .navbar-form .has-feedback .form-control-feedback {\n    top: 0;\n  }\n}\n@media (max-width: 767px) {\n  .navbar-form .form-group {\n    margin-bottom: 5px;\n  }\n  .navbar-form .form-group:last-child {\n    margin-bottom: 0;\n  }\n}\n@media (min-width: 768px) {\n  .navbar-form {\n    width: auto;\n    padding-top: 0;\n    padding-bottom: 0;\n    margin-right: 0;\n    margin-left: 0;\n    border: 0;\n    -webkit-box-shadow: none;\n            box-shadow: none;\n  }\n}\n.navbar-nav > li > .dropdown-menu {\n  margin-top: 0;\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n  margin-bottom: 0;\n  border-top-left-radius: 4px;\n  border-top-right-radius: 4px;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.navbar-btn {\n  margin-top: 8px;\n  margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n  margin-top: 14px;\n  margin-bottom: 14px;\n}\n.navbar-text {\n  margin-top: 15px;\n  margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n  .navbar-text {\n    float: left;\n    margin-right: 15px;\n    margin-left: 15px;\n  }\n}\n@media (min-width: 768px) {\n  .navbar-left {\n    float: left !important;\n  }\n  .navbar-right {\n    float: right !important;\n    margin-right: -15px;\n  }\n  .navbar-right ~ .navbar-right {\n    margin-right: 0;\n  }\n}\n.navbar-default {\n  background-color: #f8f8f8;\n  border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n  color: #777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n  color: #5e5e5e;\n  background-color: transparent;\n}\n.navbar-default .navbar-text {\n  color: #777;\n}\n.navbar-default .navbar-nav > li > a {\n  color: #777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n  color: #333;\n  background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n  color: #555;\n  background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n  color: #ccc;\n  background-color: transparent;\n}\n.navbar-default .navbar-toggle {\n  border-color: #ddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n  background-color: #ddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n  background-color: #888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n  border-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n  color: #555;\n  background-color: #e7e7e7;\n}\n@media (max-width: 767px) {\n  .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n    color: #777;\n  }\n  .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n  .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n    color: #333;\n    background-color: transparent;\n  }\n  .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n  .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n  .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n    color: #555;\n    background-color: #e7e7e7;\n  }\n  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n    color: #ccc;\n    background-color: transparent;\n  }\n}\n.navbar-default .navbar-link {\n  color: #777;\n}\n.navbar-default .navbar-link:hover {\n  color: #333;\n}\n.navbar-default .btn-link {\n  color: #777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n  color: #333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n  color: #ccc;\n}\n.navbar-inverse {\n  background-color: #222;\n  border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n  color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n  color: #fff;\n  background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n  color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n  color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n  color: #fff;\n  background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n  color: #fff;\n  background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n  color: #444;\n  background-color: transparent;\n}\n.navbar-inverse .navbar-toggle {\n  border-color: #333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n  background-color: #333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n  background-color: #fff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n  border-color: #101010;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n  color: #fff;\n  background-color: #080808;\n}\n@media (max-width: 767px) {\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n    border-color: #080808;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n    background-color: #080808;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n    color: #9d9d9d;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n    color: #fff;\n    background-color: transparent;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n    color: #fff;\n    background-color: #080808;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n    color: #444;\n    background-color: transparent;\n  }\n}\n.navbar-inverse .navbar-link {\n  color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n  color: #fff;\n}\n.navbar-inverse .btn-link {\n  color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n  color: #fff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n  color: #444;\n}\n.breadcrumb {\n  padding: 8px 15px;\n  margin-bottom: 20px;\n  list-style: none;\n  background-color: #f5f5f5;\n  border-radius: 4px;\n}\n.breadcrumb > li {\n  display: inline-block;\n}\n.breadcrumb > li + li:before {\n  padding: 0 5px;\n  color: #ccc;\n  content: \"/\\00a0\";\n}\n.breadcrumb > .active {\n  color: #777;\n}\n.pagination {\n  display: inline-block;\n  padding-left: 0;\n  margin: 20px 0;\n  border-radius: 4px;\n}\n.pagination > li {\n  display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n  position: relative;\n  float: left;\n  padding: 6px 12px;\n  margin-left: -1px;\n  line-height: 1.42857143;\n  color: #337ab7;\n  text-decoration: none;\n  background-color: #fff;\n  border: 1px solid #ddd;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n  margin-left: 0;\n  border-top-left-radius: 4px;\n  border-bottom-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n  border-top-right-radius: 4px;\n  border-bottom-right-radius: 4px;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n  z-index: 2;\n  color: #23527c;\n  background-color: #eee;\n  border-color: #ddd;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n  z-index: 3;\n  color: #fff;\n  cursor: default;\n  background-color: #337ab7;\n  border-color: #337ab7;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n  color: #777;\n  cursor: not-allowed;\n  background-color: #fff;\n  border-color: #ddd;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n  padding: 10px 16px;\n  font-size: 18px;\n  line-height: 1.3333333;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n  border-top-left-radius: 6px;\n  border-bottom-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n  border-top-right-radius: 6px;\n  border-bottom-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n  padding: 5px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n  border-top-left-radius: 3px;\n  border-bottom-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n  border-top-right-radius: 3px;\n  border-bottom-right-radius: 3px;\n}\n.pager {\n  padding-left: 0;\n  margin: 20px 0;\n  text-align: center;\n  list-style: none;\n}\n.pager li {\n  display: inline;\n}\n.pager li > a,\n.pager li > span {\n  display: inline-block;\n  padding: 5px 14px;\n  background-color: #fff;\n  border: 1px solid #ddd;\n  border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n  text-decoration: none;\n  background-color: #eee;\n}\n.pager .next > a,\n.pager .next > span {\n  float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n  float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n  color: #777;\n  cursor: not-allowed;\n  background-color: #fff;\n}\n.label {\n  display: inline;\n  padding: .2em .6em .3em;\n  font-size: 75%;\n  font-weight: bold;\n  line-height: 1;\n  color: #fff;\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: baseline;\n  border-radius: .25em;\n}\na.label:hover,\na.label:focus {\n  color: #fff;\n  text-decoration: none;\n  cursor: pointer;\n}\n.label:empty {\n  display: none;\n}\n.btn .label {\n  position: relative;\n  top: -1px;\n}\n.label-default {\n  background-color: #777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n  background-color: #5e5e5e;\n}\n.label-primary {\n  background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n  background-color: #286090;\n}\n.label-success {\n  background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n  background-color: #449d44;\n}\n.label-info {\n  background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n  background-color: #31b0d5;\n}\n.label-warning {\n  background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n  background-color: #ec971f;\n}\n.label-danger {\n  background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n  background-color: #c9302c;\n}\n.badge {\n  display: inline-block;\n  min-width: 10px;\n  padding: 3px 7px;\n  font-size: 12px;\n  font-weight: bold;\n  line-height: 1;\n  color: #fff;\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: middle;\n  background-color: #777;\n  border-radius: 10px;\n}\n.badge:empty {\n  display: none;\n}\n.btn .badge {\n  position: relative;\n  top: -1px;\n}\n.btn-xs .badge,\n.btn-group-xs > .btn .badge {\n  top: 0;\n  padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n  color: #fff;\n  text-decoration: none;\n  cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n  color: #337ab7;\n  background-color: #fff;\n}\n.list-group-item > .badge {\n  float: right;\n}\n.list-group-item > .badge + .badge {\n  margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n  margin-left: 3px;\n}\n.jumbotron {\n  padding-top: 30px;\n  padding-bottom: 30px;\n  margin-bottom: 30px;\n  color: inherit;\n  background-color: #eee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n  color: inherit;\n}\n.jumbotron p {\n  margin-bottom: 15px;\n  font-size: 21px;\n  font-weight: 200;\n}\n.jumbotron > hr {\n  border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n  padding-right: 15px;\n  padding-left: 15px;\n  border-radius: 6px;\n}\n.jumbotron .container {\n  max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n  .jumbotron {\n    padding-top: 48px;\n    padding-bottom: 48px;\n  }\n  .container .jumbotron,\n  .container-fluid .jumbotron {\n    padding-right: 60px;\n    padding-left: 60px;\n  }\n  .jumbotron h1,\n  .jumbotron .h1 {\n    font-size: 63px;\n  }\n}\n.thumbnail {\n  display: block;\n  padding: 4px;\n  margin-bottom: 20px;\n  line-height: 1.42857143;\n  background-color: #fff;\n  border: 1px solid #ddd;\n  border-radius: 4px;\n  -webkit-transition: border .2s ease-in-out;\n       -o-transition: border .2s ease-in-out;\n          transition: border .2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n  margin-right: auto;\n  margin-left: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n  border-color: #337ab7;\n}\n.thumbnail .caption {\n  padding: 9px;\n  color: #333;\n}\n.alert {\n  padding: 15px;\n  margin-bottom: 20px;\n  border: 1px solid transparent;\n  border-radius: 4px;\n}\n.alert h4 {\n  margin-top: 0;\n  color: inherit;\n}\n.alert .alert-link {\n  font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n  margin-bottom: 0;\n}\n.alert > p + p {\n  margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n  padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n  position: relative;\n  top: -2px;\n  right: -21px;\n  color: inherit;\n}\n.alert-success {\n  color: #3c763d;\n  background-color: #dff0d8;\n  border-color: #d6e9c6;\n}\n.alert-success hr {\n  border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n  color: #2b542c;\n}\n.alert-info {\n  color: #31708f;\n  background-color: #d9edf7;\n  border-color: #bce8f1;\n}\n.alert-info hr {\n  border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n  color: #245269;\n}\n.alert-warning {\n  color: #8a6d3b;\n  background-color: #fcf8e3;\n  border-color: #faebcc;\n}\n.alert-warning hr {\n  border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n  color: #66512c;\n}\n.alert-danger {\n  color: #a94442;\n  background-color: #f2dede;\n  border-color: #ebccd1;\n}\n.alert-danger hr {\n  border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n  color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n  from {\n    background-position: 40px 0;\n  }\n  to {\n    background-position: 0 0;\n  }\n}\n@-o-keyframes progress-bar-stripes {\n  from {\n    background-position: 40px 0;\n  }\n  to {\n    background-position: 0 0;\n  }\n}\n@keyframes progress-bar-stripes {\n  from {\n    background-position: 40px 0;\n  }\n  to {\n    background-position: 0 0;\n  }\n}\n.progress {\n  height: 20px;\n  margin-bottom: 20px;\n  overflow: hidden;\n  background-color: #f5f5f5;\n  border-radius: 4px;\n  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);\n          box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);\n}\n.progress-bar {\n  float: left;\n  width: 0;\n  height: 100%;\n  font-size: 12px;\n  line-height: 20px;\n  color: #fff;\n  text-align: center;\n  background-color: #337ab7;\n  -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15);\n          box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15);\n  -webkit-transition: width .6s ease;\n       -o-transition: width .6s ease;\n          transition: width .6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:      -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  -webkit-background-size: 40px 40px;\n          background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n  -webkit-animation: progress-bar-stripes 2s linear infinite;\n       -o-animation: progress-bar-stripes 2s linear infinite;\n          animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n  background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:      -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n  background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:      -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n  background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:      -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n  background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:      -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n}\n.media {\n  margin-top: 15px;\n}\n.media:first-child {\n  margin-top: 0;\n}\n.media,\n.media-body {\n  overflow: hidden;\n  zoom: 1;\n}\n.media-body {\n  width: 10000px;\n}\n.media-object {\n  display: block;\n}\n.media-object.img-thumbnail {\n  max-width: none;\n}\n.media-right,\n.media > .pull-right {\n  padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n  padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n  display: table-cell;\n  vertical-align: top;\n}\n.media-middle {\n  vertical-align: middle;\n}\n.media-bottom {\n  vertical-align: bottom;\n}\n.media-heading {\n  margin-top: 0;\n  margin-bottom: 5px;\n}\n.media-list {\n  padding-left: 0;\n  list-style: none;\n}\n.list-group {\n  padding-left: 0;\n  margin-bottom: 20px;\n}\n.list-group-item {\n  position: relative;\n  display: block;\n  padding: 10px 15px;\n  margin-bottom: -1px;\n  background-color: #fff;\n  border: 1px solid #ddd;\n}\n.list-group-item:first-child {\n  border-top-left-radius: 4px;\n  border-top-right-radius: 4px;\n}\n.list-group-item:last-child {\n  margin-bottom: 0;\n  border-bottom-right-radius: 4px;\n  border-bottom-left-radius: 4px;\n}\na.list-group-item,\nbutton.list-group-item {\n  color: #555;\n}\na.list-group-item .list-group-item-heading,\nbutton.list-group-item .list-group-item-heading {\n  color: #333;\n}\na.list-group-item:hover,\nbutton.list-group-item:hover,\na.list-group-item:focus,\nbutton.list-group-item:focus {\n  color: #555;\n  text-decoration: none;\n  background-color: #f5f5f5;\n}\nbutton.list-group-item {\n  width: 100%;\n  text-align: left;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n  color: #777;\n  cursor: not-allowed;\n  background-color: #eee;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n  color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n  color: #777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n  z-index: 2;\n  color: #fff;\n  background-color: #337ab7;\n  border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n  color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n  color: #c7ddef;\n}\n.list-group-item-success {\n  color: #3c763d;\n  background-color: #dff0d8;\n}\na.list-group-item-success,\nbutton.list-group-item-success {\n  color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading,\nbutton.list-group-item-success .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item-success:hover,\nbutton.list-group-item-success:hover,\na.list-group-item-success:focus,\nbutton.list-group-item-success:focus {\n  color: #3c763d;\n  background-color: #d0e9c6;\n}\na.list-group-item-success.active,\nbutton.list-group-item-success.active,\na.list-group-item-success.active:hover,\nbutton.list-group-item-success.active:hover,\na.list-group-item-success.active:focus,\nbutton.list-group-item-success.active:focus {\n  color: #fff;\n  background-color: #3c763d;\n  border-color: #3c763d;\n}\n.list-group-item-info {\n  color: #31708f;\n  background-color: #d9edf7;\n}\na.list-group-item-info,\nbutton.list-group-item-info {\n  color: #31708f;\n}\na.list-group-item-info .list-group-item-heading,\nbutton.list-group-item-info .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item-info:hover,\nbutton.list-group-item-info:hover,\na.list-group-item-info:focus,\nbutton.list-group-item-info:focus {\n  color: #31708f;\n  background-color: #c4e3f3;\n}\na.list-group-item-info.active,\nbutton.list-group-item-info.active,\na.list-group-item-info.active:hover,\nbutton.list-group-item-info.active:hover,\na.list-group-item-info.active:focus,\nbutton.list-group-item-info.active:focus {\n  color: #fff;\n  background-color: #31708f;\n  border-color: #31708f;\n}\n.list-group-item-warning {\n  color: #8a6d3b;\n  background-color: #fcf8e3;\n}\na.list-group-item-warning,\nbutton.list-group-item-warning {\n  color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading,\nbutton.list-group-item-warning .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item-warning:hover,\nbutton.list-group-item-warning:hover,\na.list-group-item-warning:focus,\nbutton.list-group-item-warning:focus {\n  color: #8a6d3b;\n  background-color: #faf2cc;\n}\na.list-group-item-warning.active,\nbutton.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\nbutton.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus,\nbutton.list-group-item-warning.active:focus {\n  color: #fff;\n  background-color: #8a6d3b;\n  border-color: #8a6d3b;\n}\n.list-group-item-danger {\n  color: #a94442;\n  background-color: #f2dede;\n}\na.list-group-item-danger,\nbutton.list-group-item-danger {\n  color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading,\nbutton.list-group-item-danger .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item-danger:hover,\nbutton.list-group-item-danger:hover,\na.list-group-item-danger:focus,\nbutton.list-group-item-danger:focus {\n  color: #a94442;\n  background-color: #ebcccc;\n}\na.list-group-item-danger.active,\nbutton.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\nbutton.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus,\nbutton.list-group-item-danger.active:focus {\n  color: #fff;\n  background-color: #a94442;\n  border-color: #a94442;\n}\n.list-group-item-heading {\n  margin-top: 0;\n  margin-bottom: 5px;\n}\n.list-group-item-text {\n  margin-bottom: 0;\n  line-height: 1.3;\n}\n.panel {\n  margin-bottom: 20px;\n  background-color: #fff;\n  border: 1px solid transparent;\n  border-radius: 4px;\n  -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05);\n          box-shadow: 0 1px 1px rgba(0, 0, 0, .05);\n}\n.panel-body {\n  padding: 15px;\n}\n.panel-heading {\n  padding: 10px 15px;\n  border-bottom: 1px solid transparent;\n  border-top-left-radius: 3px;\n  border-top-right-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n  color: inherit;\n}\n.panel-title {\n  margin-top: 0;\n  margin-bottom: 0;\n  font-size: 16px;\n  color: inherit;\n}\n.panel-title > a,\n.panel-title > small,\n.panel-title > .small,\n.panel-title > small > a,\n.panel-title > .small > a {\n  color: inherit;\n}\n.panel-footer {\n  padding: 10px 15px;\n  background-color: #f5f5f5;\n  border-top: 1px solid #ddd;\n  border-bottom-right-radius: 3px;\n  border-bottom-left-radius: 3px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n  margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n  border-width: 1px 0;\n  border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n  border-top: 0;\n  border-top-left-radius: 3px;\n  border-top-right-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n  border-bottom: 0;\n  border-bottom-right-radius: 3px;\n  border-bottom-left-radius: 3px;\n}\n.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child {\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n  border-top-width: 0;\n}\n.list-group + .panel-footer {\n  border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n  margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n  padding-right: 15px;\n  padding-left: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n  border-top-left-radius: 3px;\n  border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n  border-top-left-radius: 3px;\n  border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n  border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n  border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n  border-bottom-right-radius: 3px;\n  border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n  border-bottom-right-radius: 3px;\n  border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n  border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n  border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n  border-top: 1px solid #ddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n  border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n  border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n  border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n  border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n  border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n  border-bottom: 0;\n}\n.panel > .table-responsive {\n  margin-bottom: 0;\n  border: 0;\n}\n.panel-group {\n  margin-bottom: 20px;\n}\n.panel-group .panel {\n  margin-bottom: 0;\n  border-radius: 4px;\n}\n.panel-group .panel + .panel {\n  margin-top: 5px;\n}\n.panel-group .panel-heading {\n  border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n  border-top: 1px solid #ddd;\n}\n.panel-group .panel-footer {\n  border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n  border-bottom: 1px solid #ddd;\n}\n.panel-default {\n  border-color: #ddd;\n}\n.panel-default > .panel-heading {\n  color: #333;\n  background-color: #f5f5f5;\n  border-color: #ddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #ddd;\n}\n.panel-default > .panel-heading .badge {\n  color: #f5f5f5;\n  background-color: #333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #ddd;\n}\n.panel-primary {\n  border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n  color: #fff;\n  background-color: #337ab7;\n  border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n  color: #337ab7;\n  background-color: #fff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #337ab7;\n}\n.panel-success {\n  border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n  color: #3c763d;\n  background-color: #dff0d8;\n  border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n  color: #dff0d8;\n  background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #d6e9c6;\n}\n.panel-info {\n  border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n  color: #31708f;\n  background-color: #d9edf7;\n  border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n  color: #d9edf7;\n  background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #bce8f1;\n}\n.panel-warning {\n  border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n  color: #8a6d3b;\n  background-color: #fcf8e3;\n  border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n  color: #fcf8e3;\n  background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #faebcc;\n}\n.panel-danger {\n  border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n  color: #a94442;\n  background-color: #f2dede;\n  border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n  color: #f2dede;\n  background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n  position: relative;\n  display: block;\n  height: 0;\n  padding: 0;\n  overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  border: 0;\n}\n.embed-responsive-16by9 {\n  padding-bottom: 56.25%;\n}\n.embed-responsive-4by3 {\n  padding-bottom: 75%;\n}\n.well {\n  min-height: 20px;\n  padding: 19px;\n  margin-bottom: 20px;\n  background-color: #f5f5f5;\n  border: 1px solid #e3e3e3;\n  border-radius: 4px;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);\n          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);\n}\n.well blockquote {\n  border-color: #ddd;\n  border-color: rgba(0, 0, 0, .15);\n}\n.well-lg {\n  padding: 24px;\n  border-radius: 6px;\n}\n.well-sm {\n  padding: 9px;\n  border-radius: 3px;\n}\n.close {\n  float: right;\n  font-size: 21px;\n  font-weight: bold;\n  line-height: 1;\n  color: #000;\n  text-shadow: 0 1px 0 #fff;\n  filter: alpha(opacity=20);\n  opacity: .2;\n}\n.close:hover,\n.close:focus {\n  color: #000;\n  text-decoration: none;\n  cursor: pointer;\n  filter: alpha(opacity=50);\n  opacity: .5;\n}\nbutton.close {\n  -webkit-appearance: none;\n  padding: 0;\n  cursor: pointer;\n  background: transparent;\n  border: 0;\n}\n.modal-open {\n  overflow: hidden;\n}\n.modal {\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 1050;\n  display: none;\n  overflow: hidden;\n  -webkit-overflow-scrolling: touch;\n  outline: 0;\n}\n.modal.fade .modal-dialog {\n  -webkit-transition: -webkit-transform .3s ease-out;\n       -o-transition:      -o-transform .3s ease-out;\n          transition:         transform .3s ease-out;\n  -webkit-transform: translate(0, -25%);\n      -ms-transform: translate(0, -25%);\n       -o-transform: translate(0, -25%);\n          transform: translate(0, -25%);\n}\n.modal.in .modal-dialog {\n  -webkit-transform: translate(0, 0);\n      -ms-transform: translate(0, 0);\n       -o-transform: translate(0, 0);\n          transform: translate(0, 0);\n}\n.modal-open .modal {\n  overflow-x: hidden;\n  overflow-y: auto;\n}\n.modal-dialog {\n  position: relative;\n  width: auto;\n  margin: 10px;\n}\n.modal-content {\n  position: relative;\n  background-color: #fff;\n  -webkit-background-clip: padding-box;\n          background-clip: padding-box;\n  border: 1px solid #999;\n  border: 1px solid rgba(0, 0, 0, .2);\n  border-radius: 6px;\n  outline: 0;\n  -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5);\n          box-shadow: 0 3px 9px rgba(0, 0, 0, .5);\n}\n.modal-backdrop {\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 1040;\n  background-color: #000;\n}\n.modal-backdrop.fade {\n  filter: alpha(opacity=0);\n  opacity: 0;\n}\n.modal-backdrop.in {\n  filter: alpha(opacity=50);\n  opacity: .5;\n}\n.modal-header {\n  padding: 15px;\n  border-bottom: 1px solid #e5e5e5;\n}\n.modal-header .close {\n  margin-top: -2px;\n}\n.modal-title {\n  margin: 0;\n  line-height: 1.42857143;\n}\n.modal-body {\n  position: relative;\n  padding: 15px;\n}\n.modal-footer {\n  padding: 15px;\n  text-align: right;\n  border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n  margin-bottom: 0;\n  margin-left: 5px;\n}\n.modal-footer .btn-group .btn + .btn {\n  margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n  margin-left: 0;\n}\n.modal-scrollbar-measure {\n  position: absolute;\n  top: -9999px;\n  width: 50px;\n  height: 50px;\n  overflow: scroll;\n}\n@media (min-width: 768px) {\n  .modal-dialog {\n    width: 600px;\n    margin: 30px auto;\n  }\n  .modal-content {\n    -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5);\n            box-shadow: 0 5px 15px rgba(0, 0, 0, .5);\n  }\n  .modal-sm {\n    width: 300px;\n  }\n}\n@media (min-width: 992px) {\n  .modal-lg {\n    width: 900px;\n  }\n}\n.tooltip {\n  position: absolute;\n  z-index: 1070;\n  display: block;\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-size: 12px;\n  font-style: normal;\n  font-weight: normal;\n  line-height: 1.42857143;\n  text-align: left;\n  text-align: start;\n  text-decoration: none;\n  text-shadow: none;\n  text-transform: none;\n  letter-spacing: normal;\n  word-break: normal;\n  word-spacing: normal;\n  word-wrap: normal;\n  white-space: normal;\n  filter: alpha(opacity=0);\n  opacity: 0;\n\n  line-break: auto;\n}\n.tooltip.in {\n  filter: alpha(opacity=90);\n  opacity: .9;\n}\n.tooltip.top {\n  padding: 5px 0;\n  margin-top: -3px;\n}\n.tooltip.right {\n  padding: 0 5px;\n  margin-left: 3px;\n}\n.tooltip.bottom {\n  padding: 5px 0;\n  margin-top: 3px;\n}\n.tooltip.left {\n  padding: 0 5px;\n  margin-left: -3px;\n}\n.tooltip-inner {\n  max-width: 200px;\n  padding: 3px 8px;\n  color: #fff;\n  text-align: center;\n  background-color: #000;\n  border-radius: 4px;\n}\n.tooltip-arrow {\n  position: absolute;\n  width: 0;\n  height: 0;\n  border-color: transparent;\n  border-style: solid;\n}\n.tooltip.top .tooltip-arrow {\n  bottom: 0;\n  left: 50%;\n  margin-left: -5px;\n  border-width: 5px 5px 0;\n  border-top-color: #000;\n}\n.tooltip.top-left .tooltip-arrow {\n  right: 5px;\n  bottom: 0;\n  margin-bottom: -5px;\n  border-width: 5px 5px 0;\n  border-top-color: #000;\n}\n.tooltip.top-right .tooltip-arrow {\n  bottom: 0;\n  left: 5px;\n  margin-bottom: -5px;\n  border-width: 5px 5px 0;\n  border-top-color: #000;\n}\n.tooltip.right .tooltip-arrow {\n  top: 50%;\n  left: 0;\n  margin-top: -5px;\n  border-width: 5px 5px 5px 0;\n  border-right-color: #000;\n}\n.tooltip.left .tooltip-arrow {\n  top: 50%;\n  right: 0;\n  margin-top: -5px;\n  border-width: 5px 0 5px 5px;\n  border-left-color: #000;\n}\n.tooltip.bottom .tooltip-arrow {\n  top: 0;\n  left: 50%;\n  margin-left: -5px;\n  border-width: 0 5px 5px;\n  border-bottom-color: #000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n  top: 0;\n  right: 5px;\n  margin-top: -5px;\n  border-width: 0 5px 5px;\n  border-bottom-color: #000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n  top: 0;\n  left: 5px;\n  margin-top: -5px;\n  border-width: 0 5px 5px;\n  border-bottom-color: #000;\n}\n.popover {\n  position: absolute;\n  top: 0;\n  left: 0;\n  z-index: 1060;\n  display: none;\n  max-width: 276px;\n  padding: 1px;\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-size: 14px;\n  font-style: normal;\n  font-weight: normal;\n  line-height: 1.42857143;\n  text-align: left;\n  text-align: start;\n  text-decoration: none;\n  text-shadow: none;\n  text-transform: none;\n  letter-spacing: normal;\n  word-break: normal;\n  word-spacing: normal;\n  word-wrap: normal;\n  white-space: normal;\n  background-color: #fff;\n  -webkit-background-clip: padding-box;\n          background-clip: padding-box;\n  border: 1px solid #ccc;\n  border: 1px solid rgba(0, 0, 0, .2);\n  border-radius: 6px;\n  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2);\n          box-shadow: 0 5px 10px rgba(0, 0, 0, .2);\n\n  line-break: auto;\n}\n.popover.top {\n  margin-top: -10px;\n}\n.popover.right {\n  margin-left: 10px;\n}\n.popover.bottom {\n  margin-top: 10px;\n}\n.popover.left {\n  margin-left: -10px;\n}\n.popover-title {\n  padding: 8px 14px;\n  margin: 0;\n  font-size: 14px;\n  background-color: #f7f7f7;\n  border-bottom: 1px solid #ebebeb;\n  border-radius: 5px 5px 0 0;\n}\n.popover-content {\n  padding: 9px 14px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n  position: absolute;\n  display: block;\n  width: 0;\n  height: 0;\n  border-color: transparent;\n  border-style: solid;\n}\n.popover > .arrow {\n  border-width: 11px;\n}\n.popover > .arrow:after {\n  content: \"\";\n  border-width: 10px;\n}\n.popover.top > .arrow {\n  bottom: -11px;\n  left: 50%;\n  margin-left: -11px;\n  border-top-color: #999;\n  border-top-color: rgba(0, 0, 0, .25);\n  border-bottom-width: 0;\n}\n.popover.top > .arrow:after {\n  bottom: 1px;\n  margin-left: -10px;\n  content: \" \";\n  border-top-color: #fff;\n  border-bottom-width: 0;\n}\n.popover.right > .arrow {\n  top: 50%;\n  left: -11px;\n  margin-top: -11px;\n  border-right-color: #999;\n  border-right-color: rgba(0, 0, 0, .25);\n  border-left-width: 0;\n}\n.popover.right > .arrow:after {\n  bottom: -10px;\n  left: 1px;\n  content: \" \";\n  border-right-color: #fff;\n  border-left-width: 0;\n}\n.popover.bottom > .arrow {\n  top: -11px;\n  left: 50%;\n  margin-left: -11px;\n  border-top-width: 0;\n  border-bottom-color: #999;\n  border-bottom-color: rgba(0, 0, 0, .25);\n}\n.popover.bottom > .arrow:after {\n  top: 1px;\n  margin-left: -10px;\n  content: \" \";\n  border-top-width: 0;\n  border-bottom-color: #fff;\n}\n.popover.left > .arrow {\n  top: 50%;\n  right: -11px;\n  margin-top: -11px;\n  border-right-width: 0;\n  border-left-color: #999;\n  border-left-color: rgba(0, 0, 0, .25);\n}\n.popover.left > .arrow:after {\n  right: 1px;\n  bottom: -10px;\n  content: \" \";\n  border-right-width: 0;\n  border-left-color: #fff;\n}\n.carousel {\n  position: relative;\n}\n.carousel-inner {\n  position: relative;\n  width: 100%;\n  overflow: hidden;\n}\n.carousel-inner > .item {\n  position: relative;\n  display: none;\n  -webkit-transition: .6s ease-in-out left;\n       -o-transition: .6s ease-in-out left;\n          transition: .6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n  line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n  .carousel-inner > .item {\n    -webkit-transition: -webkit-transform .6s ease-in-out;\n         -o-transition:      -o-transform .6s ease-in-out;\n            transition:         transform .6s ease-in-out;\n\n    -webkit-backface-visibility: hidden;\n            backface-visibility: hidden;\n    -webkit-perspective: 1000px;\n            perspective: 1000px;\n  }\n  .carousel-inner > .item.next,\n  .carousel-inner > .item.active.right {\n    left: 0;\n    -webkit-transform: translate3d(100%, 0, 0);\n            transform: translate3d(100%, 0, 0);\n  }\n  .carousel-inner > .item.prev,\n  .carousel-inner > .item.active.left {\n    left: 0;\n    -webkit-transform: translate3d(-100%, 0, 0);\n            transform: translate3d(-100%, 0, 0);\n  }\n  .carousel-inner > .item.next.left,\n  .carousel-inner > .item.prev.right,\n  .carousel-inner > .item.active {\n    left: 0;\n    -webkit-transform: translate3d(0, 0, 0);\n            transform: translate3d(0, 0, 0);\n  }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n  display: block;\n}\n.carousel-inner > .active {\n  left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n  position: absolute;\n  top: 0;\n  width: 100%;\n}\n.carousel-inner > .next {\n  left: 100%;\n}\n.carousel-inner > .prev {\n  left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n  left: 0;\n}\n.carousel-inner > .active.left {\n  left: -100%;\n}\n.carousel-inner > .active.right {\n  left: 100%;\n}\n.carousel-control {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  width: 15%;\n  font-size: 20px;\n  color: #fff;\n  text-align: center;\n  text-shadow: 0 1px 2px rgba(0, 0, 0, .6);\n  background-color: rgba(0, 0, 0, 0);\n  filter: alpha(opacity=50);\n  opacity: .5;\n}\n.carousel-control.left {\n  background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%);\n  background-image:      -o-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%);\n  background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .5)), to(rgba(0, 0, 0, .0001)));\n  background-image:         linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n  background-repeat: repeat-x;\n}\n.carousel-control.right {\n  right: 0;\n  left: auto;\n  background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%);\n  background-image:      -o-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%);\n  background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .0001)), to(rgba(0, 0, 0, .5)));\n  background-image:         linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n  background-repeat: repeat-x;\n}\n.carousel-control:hover,\n.carousel-control:focus {\n  color: #fff;\n  text-decoration: none;\n  filter: alpha(opacity=90);\n  outline: 0;\n  opacity: .9;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n  position: absolute;\n  top: 50%;\n  z-index: 5;\n  display: inline-block;\n  margin-top: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n  left: 50%;\n  margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n  right: 50%;\n  margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n  width: 20px;\n  height: 20px;\n  font-family: serif;\n  line-height: 1;\n}\n.carousel-control .icon-prev:before {\n  content: '\\2039';\n}\n.carousel-control .icon-next:before {\n  content: '\\203a';\n}\n.carousel-indicators {\n  position: absolute;\n  bottom: 10px;\n  left: 50%;\n  z-index: 15;\n  width: 60%;\n  padding-left: 0;\n  margin-left: -30%;\n  text-align: center;\n  list-style: none;\n}\n.carousel-indicators li {\n  display: inline-block;\n  width: 10px;\n  height: 10px;\n  margin: 1px;\n  text-indent: -999px;\n  cursor: pointer;\n  background-color: #000 \\9;\n  background-color: rgba(0, 0, 0, 0);\n  border: 1px solid #fff;\n  border-radius: 10px;\n}\n.carousel-indicators .active {\n  width: 12px;\n  height: 12px;\n  margin: 0;\n  background-color: #fff;\n}\n.carousel-caption {\n  position: absolute;\n  right: 15%;\n  bottom: 20px;\n  left: 15%;\n  z-index: 10;\n  padding-top: 20px;\n  padding-bottom: 20px;\n  color: #fff;\n  text-align: center;\n  text-shadow: 0 1px 2px rgba(0, 0, 0, .6);\n}\n.carousel-caption .btn {\n  text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n  .carousel-control .glyphicon-chevron-left,\n  .carousel-control .glyphicon-chevron-right,\n  .carousel-control .icon-prev,\n  .carousel-control .icon-next {\n    width: 30px;\n    height: 30px;\n    margin-top: -10px;\n    font-size: 30px;\n  }\n  .carousel-control .glyphicon-chevron-left,\n  .carousel-control .icon-prev {\n    margin-left: -10px;\n  }\n  .carousel-control .glyphicon-chevron-right,\n  .carousel-control .icon-next {\n    margin-right: -10px;\n  }\n  .carousel-caption {\n    right: 20%;\n    left: 20%;\n    padding-bottom: 30px;\n  }\n  .carousel-indicators {\n    bottom: 20px;\n  }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-header:before,\n.modal-header:after,\n.modal-footer:before,\n.modal-footer:after {\n  display: table;\n  content: \" \";\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-header:after,\n.modal-footer:after {\n  clear: both;\n}\n.center-block {\n  display: block;\n  margin-right: auto;\n  margin-left: auto;\n}\n.pull-right {\n  float: right !important;\n}\n.pull-left {\n  float: left !important;\n}\n.hide {\n  display: none !important;\n}\n.show {\n  display: block !important;\n}\n.invisible {\n  visibility: hidden;\n}\n.text-hide {\n  font: 0/0 a;\n  color: transparent;\n  text-shadow: none;\n  background-color: transparent;\n  border: 0;\n}\n.hidden {\n  display: none !important;\n}\n.affix {\n  position: fixed;\n}\n@-ms-viewport {\n  width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n  display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n  display: none !important;\n}\n@media (max-width: 767px) {\n  .visible-xs {\n    display: block !important;\n  }\n  table.visible-xs {\n    display: table !important;\n  }\n  tr.visible-xs {\n    display: table-row !important;\n  }\n  th.visible-xs,\n  td.visible-xs {\n    display: table-cell !important;\n  }\n}\n@media (max-width: 767px) {\n  .visible-xs-block {\n    display: block !important;\n  }\n}\n@media (max-width: 767px) {\n  .visible-xs-inline {\n    display: inline !important;\n  }\n}\n@media (max-width: 767px) {\n  .visible-xs-inline-block {\n    display: inline-block !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .visible-sm {\n    display: block !important;\n  }\n  table.visible-sm {\n    display: table !important;\n  }\n  tr.visible-sm {\n    display: table-row !important;\n  }\n  th.visible-sm,\n  td.visible-sm {\n    display: table-cell !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .visible-sm-block {\n    display: block !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .visible-sm-inline {\n    display: inline !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .visible-sm-inline-block {\n    display: inline-block !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n  .visible-md {\n    display: block !important;\n  }\n  table.visible-md {\n    display: table !important;\n  }\n  tr.visible-md {\n    display: table-row !important;\n  }\n  th.visible-md,\n  td.visible-md {\n    display: table-cell !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n  .visible-md-block {\n    display: block !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n  .visible-md-inline {\n    display: inline !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n  .visible-md-inline-block {\n    display: inline-block !important;\n  }\n}\n@media (min-width: 1200px) {\n  .visible-lg {\n    display: block !important;\n  }\n  table.visible-lg {\n    display: table !important;\n  }\n  tr.visible-lg {\n    display: table-row !important;\n  }\n  th.visible-lg,\n  td.visible-lg {\n    display: table-cell !important;\n  }\n}\n@media (min-width: 1200px) {\n  .visible-lg-block {\n    display: block !important;\n  }\n}\n@media (min-width: 1200px) {\n  .visible-lg-inline {\n    display: inline !important;\n  }\n}\n@media (min-width: 1200px) {\n  .visible-lg-inline-block {\n    display: inline-block !important;\n  }\n}\n@media (max-width: 767px) {\n  .hidden-xs {\n    display: none !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .hidden-sm {\n    display: none !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n  .hidden-md {\n    display: none !important;\n  }\n}\n@media (min-width: 1200px) {\n  .hidden-lg {\n    display: none !important;\n  }\n}\n.visible-print {\n  display: none !important;\n}\n@media print {\n  .visible-print {\n    display: block !important;\n  }\n  table.visible-print {\n    display: table !important;\n  }\n  tr.visible-print {\n    display: table-row !important;\n  }\n  th.visible-print,\n  td.visible-print {\n    display: table-cell !important;\n  }\n}\n.visible-print-block {\n  display: none !important;\n}\n@media print {\n  .visible-print-block {\n    display: block !important;\n  }\n}\n.visible-print-inline {\n  display: none !important;\n}\n@media print {\n  .visible-print-inline {\n    display: inline !important;\n  }\n}\n.visible-print-inline-block {\n  display: none !important;\n}\n@media print {\n  .visible-print-inline-block {\n    display: inline-block !important;\n  }\n}\n@media print {\n  .hidden-print {\n    display: none !important;\n  }\n}\n/*# sourceMappingURL=bootstrap.css.map */\n"
  },
  {
    "path": "gui/static/css/carriergroups.css",
    "content": ".navbar {\n  background: #FFFFFF;\n  padding: 0;\n  margin: 0;\n}\n\n.navbar > ul {\n  list-style: none;\n  padding: 0;\n  margin: 0;\n}\n\n.navbar > ul > li {\n  -webkit-transition: all 0.2s cubic-bezier(0.2, 0.6, 0.4, 1);\n  -moz-transition: all 0.2s cubic-bezier(0.2, 0.6, 0.4, 1);\n  -o-transition: all 0.2s cubic-bezier(0.2, 0.6, 0.4, 1);\n  transition: all 0.2s cubic-bezier(0.2, 0.6, 0.4, 1);\n  background: rgba(0, 0, 0, 0.05);\n  margin: 0 2px;\n}\n\n.navbar > ul > li a {\n  -webkit-transition: all 0.2s ease;\n  -moz-transition: all 0.2s ease;\n  -o-transition: all 0.2s ease;\n  transition: all 0.2s ease;\n  -webkit-border-radius: 4px;\n  -moz-border-radius: 4px;\n  border-radius: 4px;\n  border: 2px solid transparent;\n  background: rgba(255, 255, 255, 0.75);\n}\n\n.navbar > ul > li a:hover,\n.current-navlink.current-navlink:hover {\n  border: 2px solid rgba(255, 129, 0, 0.8);\n  color: #0fafff;\n}\n\n.current-navlink.current-navlink {\n  border-top: 2px solid transparent;\n  border-right: 2px solid transparent;\n  border-bottom: 2px solid rgba(255, 129, 0, 1.0);\n  border-left: 2px solid transparent;\n  color: #3355ff;\n  background: rgba(255, 255, 255, 1.0);\n}\n\n.spinner {\n  width: 40px;\n  height: 40px;\n  clear: both;\n  margin: 20px auto;\n}\n\n.spinner-circle {\n  border: 4px rgba(0, 0, 0, 0.25) solid;\n  border-top: 4px black solid;\n  -webkit-border-radius: 50%;\n  -moz-border-radius: 50%;\n  border-radius: 50%;\n  -webkit-animation: rotator .5s infinite linear;\n  -moz-animation: rotator .5s infinite linear;\n  -o-animation: rotator .5s infinite linear;\n  animation: rotator .5s infinite linear;\n}\n\n@-webkit-keyframes rotator {\n  from {\n    -webkit-transform: rotate(0deg);\n  }\n  to {\n    -webkit-transform: rotate(359deg);\n  }\n}\n\n@-moz-keyframes rotator {\n  from {\n    -moz-transform: rotate(0deg);\n  }\n  to {\n    -moz-transform: rotate(359deg);\n  }\n}\n\n@-o-keyframes rotator {\n  from {\n    -o-transform: rotate(0deg);\n  }\n  to {\n    -o-transform: rotate(359deg);\n  }\n}\n\n@keyframes rotator {\n  from {\n    -webkit-transform: rotate(0deg);\n    -moz-transform: rotate(0deg);\n    -o-transform: rotate(0deg);\n    transform: rotate(0deg);\n  }\n  to {\n    -webkit-transform: rotate(359deg);\n    -moz-transform: rotate(359deg);\n    -o-transform: rotate(359deg);\n    transform: rotate(359deg);\n  }\n}\n\n.modal-dialog {\n    width: fit-content;\n    margin: 30px auto;\n    min-width: 600px;\n}\n"
  },
  {
    "path": "gui/static/css/cdrs.css",
    "content": "#downloadCDR, #refreshCDR {\n  left: .25em;\n  cursor: pointer;\n}\n\n#cdrs_wrapper > div.btn-group, #cdrs_wrapper > div.btn-group-vertical {\n    position: absolute;\n    display: -ms-inline-flexbox;\n    display: inline-flex;\n    vertical-align: middle;\n}\n"
  },
  {
    "path": "gui/static/css/certificates.css",
    "content": ".loader {\n  border: 16px solid #f3f3f3; /* Light grey */\n  border-top: 16px solid #3498db; /* Blue */\n  border-radius: 50%;\n  width: 120px;\n  height: 120px;\n  position: relative;\n  display: none;\n  z-index: 10;\n  animation: spin 2s linear infinite;\n}\n\n@keyframes spin {\n  0% { transform: rotate(0deg); }\n  100% { transform: rotate(360deg); }\n}\n"
  },
  {
    "path": "gui/static/css/combobox.css",
    "content": "/*\n* THIS WORK IS PROVIDED \"AS IS,\" AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.\n* COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT.\n* The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining to the work without specific, written prior permission. Title to copyright in this work will at all times remain with copyright holders.\n*\n* Original Version [combobox-1.1.css]: https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/css/combobox-1.1.css\n* Copyright © [2015] World Wide Web Consortium, (Massachusetts Institute of Technology, European Research Consortium for Informatics and Mathematics, Keio University, Beihang). All Rights Reserved. This work is distributed under the W3C® Software License [1] 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.\n* http://www.w3.org/Consortium/Legal/copyright-software\n*\n* Changes made by the dOpenSource Team\n* Copyright © [2018] W3C®, dOpenSource\n*/\n\n.combobox-wrapper {\n  position: relative;\n  font-size: 16px;\n}\n\n.listbox, .grid {\n  width: 100%;\n  color: #555;\n  background-color: #fff;\n  background-image: none;\n  border: 1px solid #ccc;\n  -webkit-border-radius: 4px;\n  -moz-border-radius: 4px;\n  border-radius: 4px;\n  list-style: none;\n  margin: 0;\n  padding: 0;\n  position: absolute;\n  top: 30px;\n  z-index: 1;\n}\n\n.listbox .result {\n  cursor: default;\n  margin: 0;\n}\n\n.listbox .result:hover, .grid .result-row:hover {\n  background: rgb(139, 189, 225);\n}\n\n.listbox .focused, .grid .focused {\n  background: rgb(139, 189, 225);\n}\n\n.grid .focused-cell {\n  outline-style: dotted;\n  outline-color: green;\n}\n\n.combobox-wrapper input {\n  padding-right: 30px;\n}\n\n.combobox-dropdown {\n  position: absolute;\n  padding: 0 0 2px;\n  background-color: #fff;\n  background-image: none;\n  border: 1px solid #ccc;\n  -webkit-border-radius: 5px;\n  -moz-border-radius: 5px;\n  border-radius: 5px;\n  right: .5px;\n  top: .5px;\n  height: 32.5px;\n  width: 32.5px;\n}\n\n.grid .result-row {\n  padding: 2px;\n  cursor: default;\n  margin: 0;\n}\n\n.grid .result-cell {\n  display: inline-block;\n  cursor: default;\n  margin: 0;\n  padding: 0 5px;\n}\n\n.grid .result-cell:last-child {\n  float: right;\n  font-size: 12px;\n  font-weight: 200;\n  color: #333;\n  line-height: 24px;\n}"
  },
  {
    "path": "gui/static/css/dashboard.css",
    "content": ".dashboard-metric {\n\tfont-size: 80px;\n\ttext-align: center;\n}\n\n.dashboard-metric-label {\n\tfont-size: 35px;\n\ttext-align: center;\n\tcolor: #ddd;\n}\n"
  },
  {
    "path": "gui/static/css/highlight/LICENSE",
    "content": "Copyright (c) 2006, Ivan Sagalaev\nAll rights reserved.\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright\n      notice, this list of conditions and the following disclaimer in the\n      documentation and/or other materials provided with the distribution.\n    * Neither the name of highlight.js nor the names of its contributors \n      may be used to endorse or promote products derived from this software \n      without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY\nEXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "gui/static/css/highlight/codepen-embed.css",
    "content": "/*\n  codepen.io Embed Theme\n  Author: Justin Perry <http://github.com/ourmaninamsterdam>\n  Original theme - https://github.com/chriskempson/tomorrow-theme\n*/\n\n.hljs {\n  display: block;\n  overflow-x: auto;\n  padding: 0.5em;\n  background: #222;\n  color: #fff;\n}\n\n.hljs-comment,\n.hljs-quote {\n  color: #777;\n}\n\n.hljs-variable,\n.hljs-template-variable,\n.hljs-tag,\n.hljs-regexp,\n.hljs-meta,\n.hljs-number,\n.hljs-built_in,\n.hljs-builtin-name,\n.hljs-literal,\n.hljs-params,\n.hljs-symbol,\n.hljs-bullet,\n.hljs-link,\n.hljs-deletion {\n  color: #ab875d;\n}\n\n.hljs-section,\n.hljs-title,\n.hljs-name,\n.hljs-selector-id,\n.hljs-selector-class,\n.hljs-type,\n.hljs-attribute {\n  color: #9b869b;\n}\n\n.hljs-string,\n.hljs-keyword,\n.hljs-selector-tag,\n.hljs-addition {\n  color: #8f9c6c;\n}\n\n.hljs-emphasis {\n  font-style: italic;\n}\n\n.hljs-strong {\n  font-weight: bold;\n}\n"
  },
  {
    "path": "gui/static/css/highlight/darcula.css",
    "content": "/*\n\nDarcula color scheme from the JetBrains family of IDEs\n\n*/\n\n\n.hljs {\n  display: block;\n  overflow-x: auto;\n  padding: 0.5em;\n  background: #2b2b2b;\n}\n\n.hljs {\n  color: #bababa;\n}\n\n.hljs-strong,\n.hljs-emphasis {\n  color: #a8a8a2;\n}\n\n.hljs-bullet,\n.hljs-quote,\n.hljs-link,\n.hljs-number,\n.hljs-regexp,\n.hljs-literal {\n  color: #6896ba;\n}\n\n.hljs-code,\n.hljs-selector-class {\n  color: #a6e22e;\n}\n\n.hljs-emphasis {\n  font-style: italic;\n}\n\n.hljs-keyword,\n.hljs-selector-tag,\n.hljs-section,\n.hljs-attribute,\n.hljs-name,\n.hljs-variable {\n  color: #cb7832;\n}\n\n.hljs-params {\n  color: #b9b9b9;\n}\n\n.hljs-string {\n  color: #6a8759;\n}\n\n.hljs-subst,\n.hljs-type,\n.hljs-built_in,\n.hljs-builtin-name,\n.hljs-symbol,\n.hljs-selector-id,\n.hljs-selector-attr,\n.hljs-selector-pseudo,\n.hljs-template-tag,\n.hljs-template-variable,\n.hljs-addition {\n  color: #e0c46c;\n}\n\n.hljs-comment,\n.hljs-deletion,\n.hljs-meta {\n  color: #7f7f7f;\n}\n"
  },
  {
    "path": "gui/static/css/highlight/default.css",
    "content": "/*\n\nOriginal highlight.js style (c) Ivan Sagalaev <maniac@softwaremaniacs.org>\n\n*/\n\n.hljs {\n  display: block;\n  overflow-x: auto;\n  padding: 0.5em;\n  background: #F0F0F0;\n}\n\n\n/* Base color: saturation 0; */\n\n.hljs,\n.hljs-subst {\n  color: #444;\n}\n\n.hljs-comment {\n  color: #888888;\n}\n\n.hljs-keyword,\n.hljs-attribute,\n.hljs-selector-tag,\n.hljs-meta-keyword,\n.hljs-doctag,\n.hljs-name {\n  font-weight: bold;\n}\n\n\n/* User color: hue: 0 */\n\n.hljs-type,\n.hljs-string,\n.hljs-number,\n.hljs-selector-id,\n.hljs-selector-class,\n.hljs-quote,\n.hljs-template-tag,\n.hljs-deletion {\n  color: #880000;\n}\n\n.hljs-title,\n.hljs-section {\n  color: #880000;\n  font-weight: bold;\n}\n\n.hljs-regexp,\n.hljs-symbol,\n.hljs-variable,\n.hljs-template-variable,\n.hljs-link,\n.hljs-selector-attr,\n.hljs-selector-pseudo {\n  color: #BC6060;\n}\n\n\n/* Language color: hue: 90; */\n\n.hljs-literal {\n  color: #78A960;\n}\n\n.hljs-built_in,\n.hljs-bullet,\n.hljs-code,\n.hljs-addition {\n  color: #397300;\n}\n\n\n/* Meta color: hue: 200 */\n\n.hljs-meta {\n  color: #1f7199;\n}\n\n.hljs-meta-string {\n  color: #4d99bf;\n}\n\n\n/* Misc effects */\n\n.hljs-emphasis {\n  font-style: italic;\n}\n\n.hljs-strong {\n  font-weight: bold;\n}\n"
  },
  {
    "path": "gui/static/css/highlight/github-gist.css",
    "content": "/**\n * GitHub Gist Theme\n * Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro\n */\n\n.hljs {\n  display: block;\n  background: white;\n  padding: 0.5em;\n  color: #333333;\n  overflow-x: auto;\n}\n\n.hljs-comment,\n.hljs-meta {\n  color: #969896;\n}\n\n.hljs-string,\n.hljs-variable,\n.hljs-template-variable,\n.hljs-strong,\n.hljs-emphasis,\n.hljs-quote {\n  color: #df5000;\n}\n\n.hljs-keyword,\n.hljs-selector-tag,\n.hljs-type {\n  color: #a71d5d;\n}\n\n.hljs-literal,\n.hljs-symbol,\n.hljs-bullet,\n.hljs-attribute {\n  color: #0086b3;\n}\n\n.hljs-section,\n.hljs-name {\n  color: #63a35c;\n}\n\n.hljs-tag {\n  color: #333333;\n}\n\n.hljs-title,\n.hljs-attr,\n.hljs-selector-id,\n.hljs-selector-class,\n.hljs-selector-attr,\n.hljs-selector-pseudo {\n  color: #795da3;\n}\n\n.hljs-addition {\n  color: #55a532;\n  background-color: #eaffea;\n}\n\n.hljs-deletion {\n  color: #bd2c00;\n  background-color: #ffecec;\n}\n\n.hljs-link {\n  text-decoration: underline;\n}\n"
  },
  {
    "path": "gui/static/css/highlight/github.css",
    "content": "/*\n\ngithub.com style (c) Vasily Polovnyov <vast@whiteants.net>\n\n*/\n\n.hljs {\n  display: block;\n  overflow-x: auto;\n  padding: 0.5em;\n  color: #333;\n  background: #f8f8f8;\n}\n\n.hljs-comment,\n.hljs-quote {\n  color: #998;\n  font-style: italic;\n}\n\n.hljs-keyword,\n.hljs-selector-tag,\n.hljs-subst {\n  color: #333;\n  font-weight: bold;\n}\n\n.hljs-number,\n.hljs-literal,\n.hljs-variable,\n.hljs-template-variable,\n.hljs-tag .hljs-attr {\n  color: #008080;\n}\n\n.hljs-string,\n.hljs-doctag {\n  color: #d14;\n}\n\n.hljs-title,\n.hljs-section,\n.hljs-selector-id {\n  color: #900;\n  font-weight: bold;\n}\n\n.hljs-subst {\n  font-weight: normal;\n}\n\n.hljs-type,\n.hljs-class .hljs-title {\n  color: #458;\n  font-weight: bold;\n}\n\n.hljs-tag,\n.hljs-name,\n.hljs-attribute {\n  color: #000080;\n  font-weight: normal;\n}\n\n.hljs-regexp,\n.hljs-link {\n  color: #009926;\n}\n\n.hljs-symbol,\n.hljs-bullet {\n  color: #990073;\n}\n\n.hljs-built_in,\n.hljs-builtin-name {\n  color: #0086b3;\n}\n\n.hljs-meta {\n  color: #999;\n  font-weight: bold;\n}\n\n.hljs-deletion {\n  background: #fdd;\n}\n\n.hljs-addition {\n  background: #dfd;\n}\n\n.hljs-emphasis {\n  font-style: italic;\n}\n\n.hljs-strong {\n  font-weight: bold;\n}\n"
  },
  {
    "path": "gui/static/css/highlight/googlecode.css",
    "content": "/*\n\nGoogle Code style (c) Aahan Krish <geekpanth3r@gmail.com>\n\n*/\n\n.hljs {\n  display: block;\n  overflow-x: auto;\n  padding: 0.5em;\n  background: white;\n  color: black;\n}\n\n.hljs-comment,\n.hljs-quote {\n  color: #800;\n}\n\n.hljs-keyword,\n.hljs-selector-tag,\n.hljs-section,\n.hljs-title,\n.hljs-name {\n  color: #008;\n}\n\n.hljs-variable,\n.hljs-template-variable {\n  color: #660;\n}\n\n.hljs-string,\n.hljs-selector-attr,\n.hljs-selector-pseudo,\n.hljs-regexp {\n  color: #080;\n}\n\n.hljs-literal,\n.hljs-symbol,\n.hljs-bullet,\n.hljs-meta,\n.hljs-number,\n.hljs-link {\n  color: #066;\n}\n\n.hljs-title,\n.hljs-doctag,\n.hljs-type,\n.hljs-attr,\n.hljs-built_in,\n.hljs-builtin-name,\n.hljs-params {\n  color: #606;\n}\n\n.hljs-attribute,\n.hljs-subst {\n  color: #000;\n}\n\n.hljs-formula {\n  background-color: #eee;\n  font-style: italic;\n}\n\n.hljs-selector-id,\n.hljs-selector-class {\n  color: #9B703F\n}\n\n.hljs-addition {\n  background-color: #baeeba;\n}\n\n.hljs-deletion {\n  background-color: #ffc8bd;\n}\n\n.hljs-doctag,\n.hljs-strong {\n  font-weight: bold;\n}\n\n.hljs-emphasis {\n  font-style: italic;\n}\n"
  },
  {
    "path": "gui/static/css/highlight/idea.css",
    "content": "/*\n\nIntellij Idea-like styling (c) Vasily Polovnyov <vast@whiteants.net>\n\n*/\n\n.hljs {\n  display: block;\n  overflow-x: auto;\n  padding: 0.5em;\n  color: #000;\n  background: #fff;\n}\n\n.hljs-subst,\n.hljs-title {\n  font-weight: normal;\n  color: #000;\n}\n\n.hljs-comment,\n.hljs-quote {\n  color: #808080;\n  font-style: italic;\n}\n\n.hljs-meta {\n  color: #808000;\n}\n\n.hljs-tag {\n  background: #efefef;\n}\n\n.hljs-section,\n.hljs-name,\n.hljs-literal,\n.hljs-keyword,\n.hljs-selector-tag,\n.hljs-type,\n.hljs-selector-id,\n.hljs-selector-class {\n  font-weight: bold;\n  color: #000080;\n}\n\n.hljs-attribute,\n.hljs-number,\n.hljs-regexp,\n.hljs-link {\n  font-weight: bold;\n  color: #0000ff;\n}\n\n.hljs-number,\n.hljs-regexp,\n.hljs-link {\n  font-weight: normal;\n}\n\n.hljs-string {\n  color: #008000;\n  font-weight: bold;\n}\n\n.hljs-symbol,\n.hljs-bullet,\n.hljs-formula {\n  color: #000;\n  background: #d0eded;\n  font-style: italic;\n}\n\n.hljs-doctag {\n  text-decoration: underline;\n}\n\n.hljs-variable,\n.hljs-template-variable {\n  color: #660e7a;\n}\n\n.hljs-addition {\n  background: #baeeba;\n}\n\n.hljs-deletion {\n  background: #ffc8bd;\n}\n\n.hljs-emphasis {\n  font-style: italic;\n}\n\n.hljs-strong {\n  font-weight: bold;\n}\n"
  },
  {
    "path": "gui/static/css/highlight/monokai-sublime.css",
    "content": "/*\n\nMonokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/\n\n*/\n\n.hljs {\n  display: block;\n  overflow-x: auto;\n  padding: 0.5em;\n  background: #23241f;\n}\n\n.hljs,\n.hljs-tag,\n.hljs-subst {\n  color: #f8f8f2;\n}\n\n.hljs-strong,\n.hljs-emphasis {\n  color: #a8a8a2;\n}\n\n.hljs-bullet,\n.hljs-quote,\n.hljs-number,\n.hljs-regexp,\n.hljs-literal,\n.hljs-link {\n  color: #ae81ff;\n}\n\n.hljs-code,\n.hljs-title,\n.hljs-section,\n.hljs-selector-class {\n  color: #a6e22e;\n}\n\n.hljs-strong {\n  font-weight: bold;\n}\n\n.hljs-emphasis {\n  font-style: italic;\n}\n\n.hljs-keyword,\n.hljs-selector-tag,\n.hljs-name,\n.hljs-attr {\n  color: #f92672;\n}\n\n.hljs-symbol,\n.hljs-attribute {\n  color: #66d9ef;\n}\n\n.hljs-params,\n.hljs-class .hljs-title {\n  color: #f8f8f2;\n}\n\n.hljs-string,\n.hljs-type,\n.hljs-built_in,\n.hljs-builtin-name,\n.hljs-selector-id,\n.hljs-selector-attr,\n.hljs-selector-pseudo,\n.hljs-addition,\n.hljs-variable,\n.hljs-template-variable {\n  color: #e6db74;\n}\n\n.hljs-comment,\n.hljs-deletion,\n.hljs-meta {\n  color: #75715e;\n}\n"
  },
  {
    "path": "gui/static/css/main.css",
    "content": "/* custom icons */\n@font-face {\n  font-family: 'icomoon';\n  src:  url('/static/fonts/icomoon.eot?imwift');\n  src:  url('/static/fonts/icomoon.eot?imwift#iefix') format('embedded-opentype'),\n    url('/static/fonts/icomoon.ttf?imwift') format('truetype'),\n    url('/static/fonts/icomoon.woff?imwift') format('woff'),\n    url('/static/fonts/icomoon.svg?imwift#icomoon') format('svg');\n  font-weight: normal;\n  font-style: normal;\n  font-display: block;\n}\n\n[class^=\"icon-\"], [class*=\" icon-\"] {\n  /* use !important to prevent issues with browser extensions that change fonts */\n  font-family: 'icomoon' !important;\n  speak: never;\n  font-style: normal;\n  font-weight: normal;\n  font-variant: normal;\n  text-transform: none;\n  line-height: 1;\n\n  /* Better Font Rendering =========== */\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n.icon-phone_enabled:before {\n  content: \"\\e90d\";\n}\n.icon-money:before {\n  content: \"\\e900\";\n}\n.icon-load_balance:before {\n  content: \"\\e111\";\n}\n.icon-fusionpbx_full:before {\n  content: \"\\e901\";\n}\n.icon-gryphon:before {\n  content: \"\\e902\";\n}\n.icon-fusionpbx:before {\n  content: \"\\e903\";\n}\n.icon-asterisk:before {\n  content: \"\\e904\";\n}\n.icon-kamailio:before {\n  content: \"\\e905\";\n}\n.icon-flowroute:before {\n  content: \"\\e906\";\n}\n.icon-freepbx:before {\n  content: \"\\e907\";\n}\n.icon-zencommunication:before {\n  content: \"\\e908\";\n}\n.icon-call_failfwd:before {\n  content: \"\\e909\";\n}\n.icon-call_hardfwd:before {\n  content: \"\\e90a\";\n}\n.icon-transnexus .path1:before {\n  content: \"\\e90b\";\n  color: rgb(0, 78, 168);\n}\n.icon-transnexus .path2:before {\n  content: \"\\e90c\";\n  margin-left: -1em;\n  color: rgb(255, 255, 255);\n}\n.icon-circle-up:before {\n  content: \"\\ea41\";\n}\n.icon-circle-down:before {\n  content: \"\\ea43\";\n}\n\n.wrap {\n  position: relative;\n}\n\n.wrap .nav-bar {\n  -webkit-border-radius: 0;\n  -moz-border-radius: 0;\n  border-radius: 0;\n}\n\n.wrap .nav-bar .navbar-brand {\n  color: #FFB266;\n  padding: 0.5em 2em;\n}\n\n.wrap .nav-bar .navbar-brand > * {\n  max-height: 100%;\n}\n\n.wrap .nav-bar .navbar-form {\n  margin-top: 25px;\n}\n\n.wrap .nav-bar .navbar-right {\n  float: right !important;\n}\n\n.wrap .nav-bar .navbar-right .btn {\n  margin-top: 0.5em;\n}\n\n.wrap .nav-bar .nav .dropdown span.fa,\n.wrap .nav-bar .nav .dropdown span.caret {\n  margin-right: 10px;\n}\n\n.wrap .nav-bar .nav .dropdown-menu {\n  background: #222;\n  left: auto;\n  width: 200px;\n  right: 0;\n}\n\n.wrap .nav-bar .nav .dropdown-menu > li > a {\n  color: #ddd;\n  padding: 8px;\n}\n\n.wrap .nav-bar .nav .dropdown-menu > li > a:hover {\n  background: #3355ff;\n}\n\n.wrap .side-menu-link {\n  left: 21em;\n}\n\n.wrap .burger {\n  position: relative;\n  width: 35px;\n  height: 30px;\n  left: 10px;\n  top: 10px;\n  z-index: 500000;\n}\n\n.wrap .burger .burger_inside {\n  position: absolute;\n  background-color: #222;\n  width: 30px;\n  height: 5px;\n  left: 2.5px;\n  -webkit-transition: all 0.5s ease-out;\n  -moz-transition: all 0.5s ease-out;\n  -o-transition: all 0.5s ease-out;\n  transition: all 0.5s ease-out;\n}\n\n.wrap .burger #bgrOne {\n  top: 0;\n}\n\n.wrap .burger #bgrTwo {\n  top: 10px;\n}\n\n.wrap .burger #bgrThree {\n  top: 20px;\n}\n\n.wrap #side-menu {\n  position: absolute;\n  z-index: -1;\n  background: #404040;\n  width: 22em;\n  padding-top: 10px;\n  padding-right: 20px;\n  padding-left: 10px;\n  float: left;\n  display: block;\n  left: 0;\n  height: 2000px;\n}\n\n.wrap .content {\n  position: absolute;\n  right: 0;\n  min-width: 400px;\n  -webkit-transition: all 0.3s ease-out;\n  -moz-transition: all 0.3s ease-out;\n  -o-transition: all 0.3s ease-out;\n  transition: all 0.3s ease-out;\n}\n\n.wrap .content .top-bar {\n  height: 45px;\n  background: #ddd;\n  border: 1px solid transparent;\n  overflow: hidden;\n}\n\n.wrap .content .content-inner {\n  padding: 0;\n  margin: 0;\n  background: #fff;\n  display: block;\n  position: absolute;\n  height: 2000px;\n  width: 100%;\n}\n\n.wrap .content .content-inner .content-header {\n  border-bottom: 2px solid #dddddd;\n  padding: 0 15px 15px 15px;\n  margin: 15px 0 20px 0;\n}\n\n.wrap .content .content-inner .content-section {\n  padding: 15px 0;\n  margin: 15px 0;\n}\n\n.wrap ul.accordion {\n  width: 100%;\n  background: transparent;\n}\n\n.wrap ul.accordion .link {\n  cursor: pointer;\n  display: block;\n  padding: 15px 15px 15px 42px;\n  color: #9D9D9D;\n  font-size: 14px;\n  font-weight: 700;\n  border-bottom: 1px solid #303030;\n  position: relative;\n  -webkit-transition: all 0.4s ease;\n  -o-transition: all 0.4s ease;\n  -moz-transition: all 0.4s ease;\n  transition: all 0.4s ease;\n}\n\n.wrap ul.accordion li:not(open) :last-child .link {\n  border-bottom: 0;\n}\n\n.wrap ul.accordion li i {\n  position: absolute;\n  top: 16px;\n  left: 12px;\n  font-size: 18px;\n  color: #999;\n  -webkit-transition: all 0.4s ease;\n  -o-transition: all 0.4s ease;\n  -moz-transition: all 0.4s ease;\n  transition: all 0.4s ease;\n}\n\n.wrap ul.accordion li i.glyphicon-chevron-down {\n  right: 12px;\n  left: auto;\n  font-size: 16px;\n  padding: 5px;\n}\n\n.wrap ul.accordion li.open .link {\n  color: #FF8100;\n}\n\n.wrap ul.accordion li.open i {\n  color: #FF8100;\n}\n\n.wrap ul.accordion li.open i.glyphicon-chevron-down {\n  -webkit-transform: rotate(180deg);\n  -ms-transform: rotate(180deg);\n  -o-transform: rotate(180deg);\n  -moz-transform: rotate(180deg);\n  transform: rotate(180deg);\n}\n\n.wrap ul.accordion ul.submenu {\n  display: none;\n  background: transparent;\n  font-size: 14px;\n  padding: 0;\n}\n\n.wrap ul.accordion ul.submenu li {\n  border-bottom: 1px solid #4b4a5e;\n  list-style: none;\n  padding: 5px;\n}\n\n.wrap ul.accordion ul.submenu li a {\n  display: block;\n  text-decoration: none;\n  color: #d9d9d9;\n  padding: 12px;\n  padding-left: 42px;\n  -webkit-transition: all 0.25s ease;\n  -o-transition: all 0.25s ease;\n  -moz-transition: all 0.25s ease;\n  transition: all 0.25s ease;\n}\n\n/*\n.wrap ul.accordion ul.submenu li a:hover {\n  background: rgba(240, 128, 128, 0.8);\n  color: #ffb266;\n} */\n\n.panel-info > .panel-heading {\n  color: #FF8100;\n  background-color: #000000;\n  border-color: #FF8100;\n}\n\n.panel-info {\n  border-color: #FF8100;\n}\n\n.loginlogo {\n  text-align: center;\n}\n\n.navlink a:link,\n.navlink a:visited,\n.navlink a:hover {\n  color: #9d9d9d;\n}\n\n.a {\n  color: #9d9d9d;\n  text-decoration: none;\n}\n\na.currentlink {\n  color: #FF8100 !important;\n}\n\na.navlink {\n  color: #c4c4c5;\n}\n\n.label-toggle {\n  margin-left: 10px;\n  width: 100%;\n}\n\n.container.container {\n  width: 100%;\n  height: 100%;\n  margin: auto;\n  padding: 0;\n}\n\n.left-half {\n  background-color: #f4f6f9;\n  position: absolute;\n  left: 0px;\n  width: 60%;\n  height: 100%;\n}\n\n.logon-image {\n  height: 100%;\n  max-width: 100%;\n  max-height: 100%;\n  margin: auto;\n}\n\n.right-half {\n  background-color: #f4f6f9;\n  position: absolute;\n  right: 0px;\n  width: 40%;\n  height: 100%;\n}\n\n.sponsor-title {\n  color: #5c9a4f;\n  font-size: 14px;\n  margin-left: 5px;\n  margin-bottom: 5px;\n}\n\n.sponsor-group {\n  width: 100%;\n  text-align: left;\n  margin-left: 10px\n}\n\n.sponsor {\n  display: inline-block;\n  margin-right: 10px;\n}\n\n.sponsor img {\n  width: 120px;\n}\n\n.navBarButtons {\n  float: right;\n}\n\n.table-centered th,\n.table-centered td {\n  text-align: center;\n  vertical-align: middle;\n}\n\n/* horizontal wrapper for block elements */\n.wrapper-horizontal {\n  display: -webkit-box;\n  display: -moz-box;\n  display: -ms-flexbox;\n  display: -webkit-flex;\n  display: flex;\n  flex-direction: row;\n}\n\n.wrapper-horizontal > div,\n.wrapper-horizontal > h1,\n.wrapper-horizontal > h2,\n.wrapper-horizontal > h3,\n.wrapper-horizontal > h4,\n.wrapper-horizontal > h5,\n.wrapper-horizontal > h6,\n.wrapper-horizontal > p,\n.wrapper-horizontal > form,\n.wrapper-horizontal > header,\n.wrapper-horizontal > footer,\n.wrapper-horizontal > section,\n.wrapper-horizontal > li,\n.wrapper-horizontal > ul,\n.wrapper-horizontal > ol,\n.wrapper-horizontal > table,\n.wrapper-horizontal > main,\n.wrapper-horizontal > nav,\n.wrapper-horizontal > pre,\n.wrapper-horizontal > noscript,\n.wrapper-horizontal > output,\n.wrapper-horizontal > section,\n.wrapper-horizontal > video {\n  display: inline-block;\n}\n\n/* vertical wrapper for block elements */\n.wrapper-vertical {\n  display: -webkit-box;\n  display: -moz-box;\n  display: -ms-flexbox;\n  display: -webkit-flex;\n  display: flex;\n  flex-direction: column;\n}\n\n.wrapper-vertical > div,\n.wrapper-vertical > h1,\n.wrapper-vertical > h2,\n.wrapper-vertical > h3,\n.wrapper-vertical > h4,\n.wrapper-vertical > h5,\n.wrapper-vertical > h6,\n.wrapper-vertical > p,\n.wrapper-vertical > form,\n.wrapper-vertical > header,\n.wrapper-vertical > footer,\n.wrapper-vertical > section,\n.wrapper-vertical > li,\n.wrapper-vertical > ul,\n.wrapper-vertical > ol,\n.wrapper-vertical > table,\n.wrapper-vertical > main,\n.wrapper-vertical > nav,\n.wrapper-vertical > pre,\n.wrapper-vertical > noscript,\n.wrapper-vertical > output,\n.wrapper-vertical > section,\n.wrapper-vertical > video {\n  display: block;\n}\n\n.centered {\n  margin: auto;\n  align-items: center;\n  justify-content: center;\n}\n\n.centered > * {\n  text-align: center;\n}\n\n.edge-centered {\n  margin: auto;\n  align-items: center;\n  justify-content: space-between;\n}\n\n.edge-centered > * {\n  text-align: center;\n}\n\n.equal-centered {\n  margin: auto;\n  align-items: center;\n  justify-content: space-evenly;\n}\n\n.equal-centered > * {\n  text-align: center;\n}\n\n.children-align-inherit.children-align-inherit > * {\n  text-align: inherit;\n}\n\n.hidden {\n  display: none;\n  visibility: hidden;\n  -webkit-transition: opacity 3s ease-in-out;\n  -moz-transition: opacity 3s ease-in-out;\n  -ms-transition: opacity 3s ease-in-out;\n  -o-transition: opacity 3s ease-in-out;\n  transition: opacity 3s ease-in-out;\n}\n\n/* add to parent elem */\n.resizable {\n  overflow: hidden;\n}\n\n.resizable > * {\n  overflow: auto;\n  resize: both;\n}\n\n/* make the modal dialogs relatively larger */\n.modal-dialog {\n  width: 50vw;\n}\n\n/* fix bootstrap modals when stacking */\n.modal-open.modal {\n  overflow-x: hidden;\n  overflow-y: auto;\n  z-index: 9999;\n}\n\n.selected {\n  background-color: #acbad4 !important;\n}\n\n.wrapper-fieldicon-right {\n  position: relative;\n}\n\n.wrapper-fieldicon-right > span {\n  position: absolute;\n  bottom: 6px;\n  right: 12px;\n  cursor: pointer;\n  line-height: inherit;\n  top: inherit;\n}\n\n.submit-form-btn.submit-form-btn {\n  width: 100%;\n}\n\n/* force dataTables to use full table width */\ntable.dataTable {\n  width: 100% !important;\n}\n\n/* give dataTables header and footer some padding */\n.dataTables_wrapper > div:first-child,\n.dataTables_wrapper > div:last-child {\n  padding-right: 10px;\n  padding-left: 10px;\n}\n\n#reloading_overlay {\n  position: absolute;\n  width: 100%;\n  height: 100%;\n  background-image: url(\"/static/images/spinner.svg\"), -webkit-gradient(linear, left bottom, left top, from(rgba(74, 243, 245, 0.85)), to(rgba(34, 45, 55, 0.85)));\n  background-image: url(\"/static/images/spinner.svg\"), -o-linear-gradient(bottom, rgba(74, 243, 245, 0.85) 0%, rgba(34, 45, 55, 0.85) 100%);\n  background-image: url(\"/static/images/spinner.svg\"), linear-gradient(0deg, rgba(74, 243, 245, 0.85) 0%, rgba(34, 45, 55, 0.85) 100%);\n  background-repeat: no-repeat;\n  background-position-x: center;\n  background-size: auto auto;\n  -webkit-box-shadow: inset 2em 2em 2em -20px rgba(20, 20, 20, .1);\n  box-shadow: inset 2em 2em 2em -20px rgba(20, 20, 20, .1);\n  z-index: 1050;\n}\n\n/* iPhone 11 */\n@media screen and (max-width: 552px) {\n  .left-half {\n    display: none;\n  }\n\n  .right-half {\n    width: 100%;\n  }\n\n  .col-xs-4 {\n    width: 70%;\n    display: inline-block;\n  }\n\n  /*.wrap .content .content-inner .content-section {\n margin: 0 auto;\n  }*/\n\n}\n\n@media screen and (min-width: 769px) {\n  .side-menu-link {\n    display: none;\n  }\n\n  .wrap {\n    position: relative;\n  }\n\n  .wrap .content {\n    left: 20em;\n    right: 0;\n  }\n}\n\n@media screen and (max-width: 768px) {\n  .wrap .nav-bar .navbar-brand {\n    margin-top: 0;\n    padding-left: 0;\n  }\n\n  .wrap .side-menu-link {\n    display: inline-block;\n  }\n\n  .wrap #side-menu #qform {\n    position: absolute;\n    top: 10px;\n  }\n\n  .wrap .content {\n    left: 0;\n  }\n\n  .wrap.active .content {\n    left: 20em;\n  }\n\n  .wrap.active .content #bgrOne {\n    top: 10px;\n    -webkit-transform: rotate(225deg);\n    -moz-transform: rotate(225deg);\n    -ms-transform: rotate(225deg);\n    -o-transform: rotate(225deg);\n    transform: rotate(225deg);\n  }\n\n  .wrap.active .content #bgrTwo {\n    opacity: 0;\n  }\n\n  .wrap.active .content #bgrThree {\n    top: 10px;\n    -webkit-transform: rotate(315deg);\n    -moz-transform: rotate(315deg);\n    -ms-transform: rotate(315deg);\n    -o-transform: rotate(315deg);\n    transform: rotate(315deg);\n  }\n}\n\n#reload_kam, #reload_dsip {\n  cursor: pointer;\n}"
  },
  {
    "path": "gui/static/css/msteams.css",
    "content": "/* The Configuration Panel */\n.configurationpanel {\n  height:auto; /* Specify a height */\n  width: 0; /* 0 width - change this with JavaScript */\n  /*position: fixed;  Stay in place */\n  z-index: 1; /* Stay on top */\n  /*top: 0;\n  left: 0; */\n  background-color: #111; /* Black*/\n  overflow-x: hidden; /* Disable horizontal scroll */\n  padding-top: 60px; /* Place content 60px from the top */\n  transition: 0.5s; /* 0.5 second transition effect to slide in the sidepanel */\n}\n\n.tooltip-inner{\n    font-size: 10px;\n    border:1px solid #e74c3c;\n    background-color: red;\n    height:100%;\n}\n"
  },
  {
    "path": "gui/static/css/titatoggle-dist.css",
    "content": "/*******************************************************\nVariables\n*******************************************************/\n/*******************************************************\nAnimation\n*******************************************************/\n@keyframes popIn {\n  0% {\n    transform: scale(1, 1);\n  }\n  25% {\n    transform: scale(1.2, 1);\n  }\n  50% {\n    transform: scale(1.4, 1);\n  }\n  100% {\n    transform: scale(1, 1);\n  }\n}\n@keyframes popOut {\n  0% {\n    transform: scale(1, 1);\n  }\n  25% {\n    transform: scale(1.2, 1);\n  }\n  50% {\n    transform: scale(1.4, 1);\n  }\n  100% {\n    transform: scale(1, 1);\n  }\n}\n@keyframes splashIn {\n  0% {\n    transform: scale(1);\n    opacity: 1;\n  }\n  25% {\n    transform: scale(1.1);\n    opacity: 0.8;\n  }\n  50% {\n    transform: scale(1.1);\n    opacity: .9;\n  }\n  100% {\n    transform: scale(1);\n    opacity: 1;\n  }\n}\n@keyframes splashOut {\n  0% {\n    transform: scale(1);\n    opacity: 1;\n  }\n  25% {\n    transform: scale(1);\n    opacity: 0.8;\n  }\n  50% {\n    transform: scale(1);\n    opacity: .9;\n  }\n  100% {\n    transform: scale(0.5);\n    opacity: 1;\n  }\n}\n/*******************************************************\nMain Slider basics\n*******************************************************/\n.checkbox-toggle {\n  position: relative;\n}\n.checkbox-toggle input[type=\"checkbox\"] {\n  display: block;\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  width: 0%;\n  height: 0%;\n  margin: 0 0;\n  cursor: pointer;\n  opacity: 0;\n}\n.checkbox-toggle input[type=\"checkbox\"]:focus + *:before {\n  outline: solid #66afe9 2px;\n}\n.checkbox-toggle input + span {\n  cursor: pointer;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.checkbox-toggle input + span:before {\n  position: absolute;\n  left: 0px;\n  display: inline-block;\n}\n.checkbox-toggle input + span > h4 {\n  display: inline;\n}\n.form-horizontal [class^='checkbox'] input + span:after {\n  top: 7px;\n}\n/*******************************************************\nMain Slider\n*******************************************************/\n.checkbox-slider {\n  position: relative;\n}\n.checkbox-slider input[type=\"checkbox\"] {\n  display: block;\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  width: 0%;\n  height: 0%;\n  margin: 0 0;\n  cursor: pointer;\n  opacity: 0;\n}\n.checkbox-slider input[type=\"checkbox\"]:focus + *:before {\n  outline: solid #66afe9 2px;\n}\n.checkbox-slider input + span {\n  cursor: pointer;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.checkbox-slider input + span:before {\n  position: absolute;\n  left: 0px;\n  display: inline-block;\n}\n.checkbox-slider input + span > h4 {\n  display: inline;\n}\n.checkbox-slider input + span {\n  padding-left: 40px;\n}\n.checkbox-slider input + span:before {\n  content: \"\";\n  height: 20px;\n  width: 40px;\n  background: rgba(100, 100, 100, 0.2);\n  box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.8);\n  transition: background 0.2s ease-out;\n}\n.checkbox-slider input + span:after {\n  width: 20px;\n  height: 20px;\n  position: absolute;\n  left: 0px;\n  top: 0;\n  display: block;\n  background: #FFF;\n  transition: margin-left 0.1s ease-in-out;\n  text-align: center;\n  font-weight: bold;\n  content: \"\";\n}\n.checkbox-slider input:checked + span:after {\n  margin-left: 20px;\n  content: \"\";\n}\n.checkbox-slider input:checked + span:before {\n  transition: background 0.2s ease-in;\n}\n/*******************************************************\nSlider default\n*******************************************************/\n.checkbox-slider--default {\n  position: relative;\n}\n.checkbox-slider--default input[type=\"checkbox\"] {\n  display: block;\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  width: 0%;\n  height: 0%;\n  margin: 0 0;\n  cursor: pointer;\n  opacity: 0;\n}\n.checkbox-slider--default input[type=\"checkbox\"]:focus + *:before {\n  outline: solid #66afe9 2px;\n}\n.checkbox-slider--default input + span {\n  cursor: pointer;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.checkbox-slider--default input + span:before {\n  position: absolute;\n  left: 0px;\n  display: inline-block;\n}\n.checkbox-slider--default input + span > h4 {\n  display: inline;\n}\n.checkbox-slider--default input + span {\n  padding-left: 40px;\n}\n.checkbox-slider--default input + span:before {\n  content: \"\";\n  height: 20px;\n  width: 40px;\n  background: rgba(100, 100, 100, 0.2);\n  box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.8);\n  transition: background 0.2s ease-out;\n}\n.checkbox-slider--default input + span:after {\n  width: 20px;\n  height: 20px;\n  position: absolute;\n  left: 0px;\n  top: 0;\n  display: block;\n  background: #FFF;\n  transition: margin-left 0.1s ease-in-out;\n  text-align: center;\n  font-weight: bold;\n  content: \"\";\n}\n.checkbox-slider--default input:checked + span:after {\n  margin-left: 20px;\n  content: \"\";\n}\n.checkbox-slider--default input:checked + span:before {\n  transition: background 0.2s ease-in;\n}\n.checkbox-slider--default input + span:after {\n  background: #FFF;\n  border: solid transparent 1px;\n  background-clip: content-box;\n}\n.checkbox-slider--default input:checked + span:after {\n  background: #5cb85c;\n  border: solid transparent 1px;\n  background-clip: content-box;\n}\n/*******************************************************\nSlider default rounded\n*******************************************************/\n.checkbox-slider--a-rounded {\n  position: relative;\n}\n.checkbox-slider--a-rounded input[type=\"checkbox\"] {\n  display: block;\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  width: 0%;\n  height: 0%;\n  margin: 0 0;\n  cursor: pointer;\n  opacity: 0;\n}\n.checkbox-slider--a-rounded input[type=\"checkbox\"]:focus + *:before {\n  outline: solid #66afe9 2px;\n}\n.checkbox-slider--a-rounded input + span {\n  cursor: pointer;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.checkbox-slider--a-rounded input + span:before {\n  position: absolute;\n  left: 0px;\n  display: inline-block;\n}\n.checkbox-slider--a-rounded input + span > h4 {\n  display: inline;\n}\n.checkbox-slider--a-rounded input + span {\n  padding-left: 40px;\n}\n.checkbox-slider--a-rounded input + span:before {\n  content: \"\";\n  height: 20px;\n  width: 40px;\n  background: rgba(100, 100, 100, 0.2);\n  box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.8);\n  transition: background 0.2s ease-out;\n}\n.checkbox-slider--a-rounded input + span:after {\n  width: 20px;\n  height: 20px;\n  position: absolute;\n  left: 0px;\n  top: 0;\n  display: block;\n  background: #FFF;\n  transition: margin-left 0.1s ease-in-out;\n  text-align: center;\n  font-weight: bold;\n  content: \"\";\n}\n.checkbox-slider--a-rounded input:checked + span:after {\n  margin-left: 20px;\n  content: \"\";\n}\n.checkbox-slider--a-rounded input:checked + span:before {\n  transition: background 0.2s ease-in;\n}\n.checkbox-slider--a-rounded input + span:after {\n  background: #FFF;\n  border: solid transparent 1px;\n  background-clip: content-box;\n}\n.checkbox-slider--a-rounded input:checked + span:after {\n  background: #5cb85c;\n  border: solid transparent 1px;\n  background-clip: content-box;\n}\n.checkbox-slider--a-rounded input + span:after,\n.checkbox-slider--a-rounded input + span:before {\n  border-radius: 4px;\n}\n.checkbox-slider--a-rounded input + span:after,\n.checkbox-slider--a-rounded input:checked + span:after {\n  border: solid transparent 2px;\n  background-clip: content-box;\n}\n/*******************************************************\nSlider default rounded Sizes\n*******************************************************/\n.checkbox-slider--a-rounded.checkbox-slider-sm input + span:before,\n.checkbox-slider--a-rounded.checkbox-slider-sm input + span:after {\n  border-radius: 3px;\n}\n.checkbox-slider--a-rounded.checkbox-slider-md input + span:before,\n.checkbox-slider--a-rounded.checkbox-slider-md input + span:after {\n  border-radius: 4px;\n}\n.checkbox-slider--a-rounded.checkbox-slider-lg input + span:before,\n.checkbox-slider--a-rounded.checkbox-slider-lg input + span:after {\n  border-radius: 6px;\n}\n/*******************************************************\nSlider A\n*******************************************************/\n.checkbox-slider--a {\n  position: relative;\n}\n.checkbox-slider--a input[type=\"checkbox\"] {\n  display: block;\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  width: 0%;\n  height: 0%;\n  margin: 0 0;\n  cursor: pointer;\n  opacity: 0;\n}\n.checkbox-slider--a input[type=\"checkbox\"]:focus + *:before {\n  outline: solid #66afe9 2px;\n}\n.checkbox-slider--a input + span {\n  cursor: pointer;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.checkbox-slider--a input + span:before {\n  position: absolute;\n  left: 0px;\n  display: inline-block;\n}\n.checkbox-slider--a input + span > h4 {\n  display: inline;\n}\n.checkbox-slider--a input + span {\n  padding-left: 40px;\n}\n.checkbox-slider--a input + span:before {\n  content: \"\";\n  height: 20px;\n  width: 40px;\n  background: rgba(100, 100, 100, 0.2);\n  box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.8);\n  transition: background 0.2s ease-out;\n}\n.checkbox-slider--a input + span:after {\n  width: 20px;\n  height: 20px;\n  position: absolute;\n  left: 0px;\n  top: 0;\n  display: block;\n  background: #FFF;\n  transition: margin-left 0.1s ease-in-out;\n  text-align: center;\n  font-weight: bold;\n  content: \"\";\n}\n.checkbox-slider--a input:checked + span:after {\n  margin-left: 20px;\n  content: \"\";\n}\n.checkbox-slider--a input:checked + span:before {\n  transition: background 0.2s ease-in;\n}\n.checkbox-slider--a input + span {\n  padding-left: 60px;\n}\n.checkbox-slider--a input + span:before {\n  content: \"\";\n  width: 60px;\n}\n.checkbox-slider--a input + span:after {\n  width: 40px;\n  font-size: 10px;\n  color: #000;\n  content: \"Off\";\n  border: solid transparent 1px;\n  background-clip: content-box;\n}\n.checkbox-slider--a input:checked + span:after {\n  content: \"On\";\n  color: #fff;\n  background: #5cb85c;\n  border: solid transparent 1px;\n  background-clip: content-box;\n}\n/*******************************************************\nSlider A SIZES\n*******************************************************/\n.checkbox-slider--a.checkbox-slider-sm input + span {\n  padding-left: 30px;\n}\n.checkbox-slider--a.checkbox-slider-sm input + span:before {\n  width: 30px;\n}\n.checkbox-slider--a.checkbox-slider-sm input + span:after {\n  width: 20px;\n  font-size: 5px;\n}\n.checkbox-slider--a.checkbox-slider-sm input:checked + span:after {\n  margin-left: 10px;\n}\n.checkbox-slider--a.checkbox-slider-md input + span {\n  padding-left: 90px;\n}\n.checkbox-slider--a.checkbox-slider-md input + span:before {\n  width: 90px;\n}\n.checkbox-slider--a.checkbox-slider-md input + span:after {\n  width: 60px;\n  font-size: 15px;\n}\n.checkbox-slider--a.checkbox-slider-md input:checked + span:after {\n  margin-left: 30px;\n}\n.checkbox-slider--a.checkbox-slider-lg input + span {\n  padding-left: 120px;\n}\n.checkbox-slider--a.checkbox-slider-lg input + span:before {\n  width: 120px;\n}\n.checkbox-slider--a.checkbox-slider-lg input + span:after {\n  width: 80px;\n  font-size: 20px;\n}\n.checkbox-slider--a.checkbox-slider-lg input:checked + span:after {\n  margin-left: 40px;\n}\n/*******************************************************\nSlider B\n*******************************************************/\n.checkbox-slider--b {\n  position: relative;\n}\n.checkbox-slider--b input[type=\"checkbox\"] {\n  display: block;\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  width: 0%;\n  height: 0%;\n  margin: 0 0;\n  cursor: pointer;\n  opacity: 0;\n}\n.checkbox-slider--b input[type=\"checkbox\"]:focus + *:before {\n  outline: solid #66afe9 2px;\n}\n.checkbox-slider--b input + span {\n  cursor: pointer;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.checkbox-slider--b input + span:before {\n  position: absolute;\n  left: 0px;\n  display: inline-block;\n}\n.checkbox-slider--b input + span > h4 {\n  display: inline;\n}\n.checkbox-slider--b input + span {\n  padding-left: 40px;\n}\n.checkbox-slider--b input + span:before {\n  content: \"\";\n  height: 20px;\n  width: 40px;\n  background: rgba(100, 100, 100, 0.2);\n  box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.8);\n  transition: background 0.2s ease-out;\n}\n.checkbox-slider--b input + span:after {\n  width: 20px;\n  height: 20px;\n  position: absolute;\n  left: 0px;\n  top: 0;\n  display: block;\n  background: #FFF;\n  transition: margin-left 0.1s ease-in-out;\n  text-align: center;\n  font-weight: bold;\n  content: \"\";\n}\n.checkbox-slider--b input:checked + span:after {\n  margin-left: 20px;\n  content: \"\";\n}\n.checkbox-slider--b input:checked + span:before {\n  transition: background 0.2s ease-in;\n}\n.checkbox-slider--b input + span {\n  padding-left: 40px;\n}\n.checkbox-slider--b input + span:before {\n  border-radius: 20px;\n  width: 40px;\n}\n.checkbox-slider--b input + span:after {\n  background: #FFF;\n  content: \"\";\n  width: 20px;\n  border: solid transparent 2px;\n  background-clip: padding-box;\n  border-radius: 20px;\n}\n.checkbox-slider--b input:not(:checked) + span:after {\n  animation: popOut ease-in 0.3s normal;\n}\n.checkbox-slider--b input:checked + span:after {\n  content: \"\";\n  margin-left: 20px;\n  border: solid transparent 2px;\n  background-clip: padding-box;\n  animation: popIn ease-in 0.3s normal;\n}\n.checkbox-slider--b input:checked + span:before {\n  background: #5cb85c;\n}\n/*******************************************************\nSlider B Sizes\n*******************************************************/\n.checkbox-slider--b.checkbox-slider-md input + span:before {\n  border-radius: 30px;\n}\n.checkbox-slider--b.checkbox-slider-md input + span:after {\n  border-radius: 30px;\n}\n.checkbox-slider--b.checkbox-slider-lg input + span:before {\n  border-radius: 40px;\n}\n.checkbox-slider--b.checkbox-slider-lg input + span:after {\n  border-radius: 40px;\n}\n/*******************************************************\nSlider B-flat\n*******************************************************/\n.checkbox-slider--b-flat {\n  position: relative;\n}\n.checkbox-slider--b-flat input[type=\"checkbox\"] {\n  display: block;\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  width: 0%;\n  height: 0%;\n  margin: 0 0;\n  cursor: pointer;\n  opacity: 0;\n}\n.checkbox-slider--b-flat input[type=\"checkbox\"]:focus + *:before {\n  outline: solid #66afe9 2px;\n}\n.checkbox-slider--b-flat input + span {\n  cursor: pointer;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.checkbox-slider--b-flat input + span:before {\n  position: absolute;\n  left: 0px;\n  display: inline-block;\n}\n.checkbox-slider--b-flat input + span > h4 {\n  display: inline;\n}\n.checkbox-slider--b-flat input + span {\n  padding-left: 40px;\n}\n.checkbox-slider--b-flat input + span:before {\n  content: \"\";\n  height: 20px;\n  width: 40px;\n  background: rgba(100, 100, 100, 0.2);\n  box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.8);\n  transition: background 0.2s ease-out;\n}\n.checkbox-slider--b-flat input + span:after {\n  width: 20px;\n  height: 20px;\n  position: absolute;\n  left: 0px;\n  top: 0;\n  display: block;\n  background: #FFF;\n  transition: margin-left 0.1s ease-in-out;\n  text-align: center;\n  font-weight: bold;\n  content: \"\";\n}\n.checkbox-slider--b-flat input:checked + span:after {\n  margin-left: 20px;\n  content: \"\";\n}\n.checkbox-slider--b-flat input:checked + span:before {\n  transition: background 0.2s ease-in;\n}\n.checkbox-slider--b-flat input + span {\n  padding-left: 40px;\n}\n.checkbox-slider--b-flat input + span:before {\n  border-radius: 20px;\n  width: 40px;\n}\n.checkbox-slider--b-flat input + span:after {\n  background: #FFF;\n  content: \"\";\n  width: 20px;\n  border: solid transparent 2px;\n  background-clip: padding-box;\n  border-radius: 20px;\n}\n.checkbox-slider--b-flat input:not(:checked) + span:after {\n  animation: popOut ease-in 0.3s normal;\n}\n.checkbox-slider--b-flat input:checked + span:after {\n  content: \"\";\n  margin-left: 20px;\n  border: solid transparent 2px;\n  background-clip: padding-box;\n  animation: popIn ease-in 0.3s normal;\n}\n.checkbox-slider--b-flat input:checked + span:before {\n  background: #5cb85c;\n}\n.checkbox-slider--b-flat.checkbox-slider-md input + span:before {\n  border-radius: 30px;\n}\n.checkbox-slider--b-flat.checkbox-slider-md input + span:after {\n  border-radius: 30px;\n}\n.checkbox-slider--b-flat.checkbox-slider-lg input + span:before {\n  border-radius: 40px;\n}\n.checkbox-slider--b-flat.checkbox-slider-lg input + span:after {\n  border-radius: 40px;\n}\n.checkbox-slider--b-flat input + span:before {\n  box-shadow: none;\n}\n/*******************************************************\nSlider C\n*******************************************************/\n.checkbox-slider--c {\n  position: relative;\n}\n.checkbox-slider--c input[type=\"checkbox\"] {\n  display: block;\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  width: 0%;\n  height: 0%;\n  margin: 0 0;\n  cursor: pointer;\n  opacity: 0;\n}\n.checkbox-slider--c input[type=\"checkbox\"]:focus + *:before {\n  outline: solid #66afe9 2px;\n}\n.checkbox-slider--c input + span {\n  cursor: pointer;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.checkbox-slider--c input + span:before {\n  position: absolute;\n  left: 0px;\n  display: inline-block;\n}\n.checkbox-slider--c input + span > h4 {\n  display: inline;\n}\n.checkbox-slider--c input + span {\n  padding-left: 40px;\n}\n.checkbox-slider--c input + span:before {\n  content: \"\";\n  height: 20px;\n  width: 40px;\n  background: rgba(100, 100, 100, 0.2);\n  box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.8);\n  transition: background 0.2s ease-out;\n}\n.checkbox-slider--c input + span:after {\n  width: 20px;\n  height: 20px;\n  position: absolute;\n  left: 0px;\n  top: 0;\n  display: block;\n  background: #FFF;\n  transition: margin-left 0.1s ease-in-out;\n  text-align: center;\n  font-weight: bold;\n  content: \"\";\n}\n.checkbox-slider--c input:checked + span:after {\n  margin-left: 20px;\n  content: \"\";\n}\n.checkbox-slider--c input:checked + span:before {\n  transition: background 0.2s ease-in;\n}\n.checkbox-slider--c input + span {\n  padding-left: 40px;\n}\n.checkbox-slider--c input + span:before {\n  height: 2px !important;\n  top: 10px;\n  box-shadow: none;\n  width: 40px;\n  background: #555555;\n}\n.checkbox-slider--c input + span:after {\n  box-shadow: none;\n  width: 20px;\n  border: solid #555555 2px;\n  border-radius: 20px;\n}\n.checkbox-slider--c input:checked + span:after {\n  background: #5cb85c;\n  margin-left: 20px;\n  border: solid #5cb85c 2px;\n  animation: splashIn ease-in 0.3s normal;\n}\n.checkbox-slider--c input:checked + span:before {\n  background: #5cb85c;\n}\n/*******************************************************\nSlider C Sizes\n*******************************************************/\n.checkbox-slider--c.checkbox-slider-sm input + span:before {\n  top: 4px;\n}\n.checkbox-slider--c.checkbox-slider-md input + span:before {\n  top: 14px;\n}\n.checkbox-slider--c.checkbox-slider-md input + span:after {\n  width: 30px;\n  border-radius: 30px;\n}\n.checkbox-slider--c.checkbox-slider-lg input + span:before {\n  top: 19px;\n}\n.checkbox-slider--c.checkbox-slider-lg input + span:after {\n  width: 40px;\n  border-radius: 40px;\n}\n.form-horizontal [class*='checkbox-slider--c'].checkbox-slider-sm input + span:before {\n  top: 10px;\n}\n.form-horizontal [class*='checkbox-slider--c'].checkbox-slider-md input + span:before {\n  top: 20px;\n}\n.form-horizontal [class*='checkbox-slider--c'].checkbox-slider-lg input + span:before {\n  top: 25px;\n}\n/*******************************************************\nSlider C-weight\n*******************************************************/\n.checkbox-slider--c-weight {\n  position: relative;\n}\n.checkbox-slider--c-weight input[type=\"checkbox\"] {\n  display: block;\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  width: 0%;\n  height: 0%;\n  margin: 0 0;\n  cursor: pointer;\n  opacity: 0;\n}\n.checkbox-slider--c-weight input[type=\"checkbox\"]:focus + *:before {\n  outline: solid #66afe9 2px;\n}\n.checkbox-slider--c-weight input + span {\n  cursor: pointer;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.checkbox-slider--c-weight input + span:before {\n  position: absolute;\n  left: 0px;\n  display: inline-block;\n}\n.checkbox-slider--c-weight input + span > h4 {\n  display: inline;\n}\n.checkbox-slider--c-weight input + span {\n  padding-left: 40px;\n}\n.checkbox-slider--c-weight input + span:before {\n  content: \"\";\n  height: 20px;\n  width: 40px;\n  background: rgba(100, 100, 100, 0.2);\n  box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.8);\n  transition: background 0.2s ease-out;\n}\n.checkbox-slider--c-weight input + span:after {\n  width: 20px;\n  height: 20px;\n  position: absolute;\n  left: 0px;\n  top: 0;\n  display: block;\n  background: #FFF;\n  transition: margin-left 0.1s ease-in-out;\n  text-align: center;\n  font-weight: bold;\n  content: \"\";\n}\n.checkbox-slider--c-weight input:checked + span:after {\n  margin-left: 20px;\n  content: \"\";\n}\n.checkbox-slider--c-weight input:checked + span:before {\n  transition: background 0.2s ease-in;\n}\n.checkbox-slider--c-weight input + span {\n  padding-left: 40px;\n}\n.checkbox-slider--c-weight input + span:before {\n  height: 2px !important;\n  top: 10px;\n  box-shadow: none;\n  width: 40px;\n  background: #555555;\n}\n.checkbox-slider--c-weight input + span:after {\n  box-shadow: none;\n  width: 20px;\n  border: solid #555555 2px;\n  border-radius: 20px;\n}\n.checkbox-slider--c-weight input:checked + span:after {\n  background: #5cb85c;\n  margin-left: 20px;\n  border: solid #5cb85c 2px;\n  animation: splashIn ease-in 0.3s normal;\n}\n.checkbox-slider--c-weight input:checked + span:before {\n  background: #5cb85c;\n}\n.checkbox-slider--c-weight.checkbox-slider-sm input + span:before {\n  top: 4px;\n}\n.checkbox-slider--c-weight.checkbox-slider-md input + span:before {\n  top: 14px;\n}\n.checkbox-slider--c-weight.checkbox-slider-md input + span:after {\n  width: 30px;\n  border-radius: 30px;\n}\n.checkbox-slider--c-weight.checkbox-slider-lg input + span:before {\n  top: 19px;\n}\n.checkbox-slider--c-weight.checkbox-slider-lg input + span:after {\n  width: 40px;\n  border-radius: 40px;\n}\n.checkbox-slider--c-weight input + span:before {\n  height: 1px !important;\n}\n.checkbox-slider--c-weight input:checked + span:before {\n  height: 2px !important;\n}\n.checkbox-slider--c-weight input:not(:checked) + span:after {\n  transform: scale(0.7);\n  left: -6px;\n}\n/******************************************************\nState Disabled\n*******************************************************/\n.checkbox-slider--default input:disabled + span:after {\n  background: #777777;\n}\n.checkbox-slider--default input:disabled + span:before {\n  box-shadow: 0 0 0 black;\n}\n.checkbox-slider--default input:disabled + span {\n  color: #777777;\n}\n.checkbox-slider--a-rounded input:disabled + span:after,\n.checkbox-slider--a input:disabled + span:after {\n  background: #777777;\n  color: #FFF;\n}\n.checkbox-slider--a-rounded input:disabled + span:before,\n.checkbox-slider--a input:disabled + span:before {\n  box-shadow: 0 0 0 black;\n}\n.checkbox-slider--a-rounded input:disabled + span,\n.checkbox-slider--a input:disabled + span {\n  color: #777777;\n}\n.checkbox-slider--b-flat input:disabled + span:after,\n.checkbox-slider--b input:disabled + span:after {\n  border: solid transparent 2px;\n  border-radius: 40px;\n}\n.checkbox-slider--b-flat input:disabled + span:before,\n.checkbox-slider--b input:disabled + span:before {\n  box-shadow: 0 0 0 black;\n}\n.checkbox-slider--b-flat input:disabled + span,\n.checkbox-slider--b input:disabled + span {\n  color: #777777;\n}\n.checkbox-slider--b-flat input:disabled:checked + span:before,\n.checkbox-slider--b input:disabled:checked + span:before {\n  background: #777777;\n}\n.checkbox-slider--c-weight input:disabled:checked + span:after,\n.checkbox-slider--c input:disabled:checked + span:after {\n  background: #777777;\n}\n.checkbox-slider--c-weight input:disabled + span:after,\n.checkbox-slider--c input:disabled + span:after {\n  border-color: #777777;\n}\n.checkbox-slider--c-weight input:disabled + span:before,\n.checkbox-slider--c input:disabled + span:before {\n  background: #777777;\n}\n.checkbox-slider--c-weight input:disabled + span,\n.checkbox-slider--c input:disabled + span {\n  color: #777777;\n}\n/*******************************************************\nIndicators\n*******************************************************/\ninput:checked + .indicator-primary {\n  color: #337ab7;\n}\ninput:checked + .indicator-success {\n  color: #5cb85c;\n}\ninput:checked + .indicator-info {\n  color: #5bc0de;\n}\ninput:checked + .indicator-warning {\n  color: #f0ad4e;\n}\ninput:checked + .indicator-danger {\n  color: #d9534f;\n}\n/*******************************************************\nSizes\n*******************************************************/\n.checkbox-slider-sm {\n  line-height: 10px;\n}\n.checkbox-slider-sm input + span {\n  padding-left: 20px;\n}\n.checkbox-slider-sm input + span:before {\n  width: 20px;\n}\n.checkbox-slider-sm input + span:after,\n.checkbox-slider-sm input + span:before {\n  height: 10px;\n  line-height: 10px;\n}\n.checkbox-slider-sm input + span:after {\n  width: 10px;\n  vertical-align: middle;\n}\n.checkbox-slider-sm input:checked + span:after {\n  margin-left: 10px;\n}\n.checkbox-slider-md {\n  line-height: 30px;\n}\n.checkbox-slider-md input + span {\n  padding-left: 60px;\n}\n.checkbox-slider-md input + span:before {\n  width: 60px;\n}\n.checkbox-slider-md input + span:after,\n.checkbox-slider-md input + span:before {\n  height: 30px;\n  line-height: 30px;\n}\n.checkbox-slider-md input + span:after {\n  width: 30px;\n  vertical-align: middle;\n}\n.checkbox-slider-md input:checked + span:after {\n  margin-left: 30px;\n}\n.checkbox-slider-lg {\n  line-height: 40px;\n}\n.checkbox-slider-lg input + span {\n  padding-left: 80px;\n}\n.checkbox-slider-lg input + span:before {\n  width: 80px;\n}\n.checkbox-slider-lg input + span:after,\n.checkbox-slider-lg input + span:before {\n  height: 40px;\n  line-height: 40px;\n}\n.checkbox-slider-lg input + span:after {\n  width: 40px;\n  vertical-align: middle;\n}\n.checkbox-slider-lg input:checked + span:after {\n  margin-left: 40px;\n}\n/*******************************************************\nVariations\n*******************************************************/\n.checkbox-slider-primary.checkbox-slider--default input:checked + span:after,\n.checkbox-slider-primary.checkbox-slider--a input:checked + span:after,\n.checkbox-slider-primary.checkbox-slider--a-rounded input:checked + span:after,\n.checkbox-slider-primary.checkbox-slider--c input:checked + span:after,\n.checkbox-slider-primary.checkbox-slider--c-weight input:checked + span:after {\n  background: #337ab7;\n  background-clip: content-box;\n}\n.checkbox-slider-primary.checkbox-slider--c input:checked + span:after,\n.checkbox-slider-primary.checkbox-slider--c-weight input:checked + span:after {\n  border-color: #337ab7;\n}\n.checkbox-slider-primary.checkbox-slider--b input:checked + span:before,\n.checkbox-slider-primary.checkbox-slider--b-flat input:checked + span:before,\n.checkbox-slider-primary.checkbox-slider--c input:checked + span:before,\n.checkbox-slider-primary.checkbox-slider--c-weight input:checked + span:before {\n  background: #337ab7;\n}\n.checkbox-slider-info.checkbox-slider--default input:checked + span:after,\n.checkbox-slider-info.checkbox-slider--a input:checked + span:after,\n.checkbox-slider-info.checkbox-slider--a-rounded input:checked + span:after,\n.checkbox-slider-info.checkbox-slider--c input:checked + span:after,\n.checkbox-slider-info.checkbox-slider--c-weight input:checked + span:after {\n  background: #5bc0de;\n  background-clip: content-box;\n}\n.checkbox-slider-info.checkbox-slider--c input:checked + span:after,\n.checkbox-slider-info.checkbox-slider--c-weight input:checked + span:after {\n  border-color: #5bc0de;\n}\n.checkbox-slider-info.checkbox-slider--b input:checked + span:before,\n.checkbox-slider-info.checkbox-slider--b-flat input:checked + span:before,\n.checkbox-slider-info.checkbox-slider--c input:checked + span:before,\n.checkbox-slider-info.checkbox-slider--c-weight input:checked + span:before {\n  background: #5bc0de;\n}\n.checkbox-slider-warning.checkbox-slider--default input:checked + span:after,\n.checkbox-slider-warning.checkbox-slider--a input:checked + span:after,\n.checkbox-slider-warning.checkbox-slider--a-rounded input:checked + span:after,\n.checkbox-slider-warning.checkbox-slider--c input:checked + span:after,\n.checkbox-slider-warning.checkbox-slider--c-weight input:checked + span:after {\n  background: #f0ad4e;\n  background-clip: content-box;\n}\n.checkbox-slider-warning.checkbox-slider--c input:checked + span:after,\n.checkbox-slider-warning.checkbox-slider--c-weight input:checked + span:after {\n  border-color: #f0ad4e;\n}\n.checkbox-slider-warning.checkbox-slider--b input:checked + span:before,\n.checkbox-slider-warning.checkbox-slider--b-flat input:checked + span:before,\n.checkbox-slider-warning.checkbox-slider--c input:checked + span:before,\n.checkbox-slider-warning.checkbox-slider--c-weight input:checked + span:before {\n  background: #f0ad4e;\n}\n.checkbox-slider-danger.checkbox-slider--default input:checked + span:after,\n.checkbox-slider-danger.checkbox-slider--a input:checked + span:after,\n.checkbox-slider-danger.checkbox-slider--a-rounded input:checked + span:after,\n.checkbox-slider-danger.checkbox-slider--c input:checked + span:after,\n.checkbox-slider-danger.checkbox-slider--c-weight input:checked + span:after {\n  background: #d9534f;\n  background-clip: content-box;\n}\n.checkbox-slider-danger.checkbox-slider--c input:checked + span:after,\n.checkbox-slider-danger.checkbox-slider--c-weight input:checked + span:after {\n  border-color: #d9534f;\n}\n.checkbox-slider-danger.checkbox-slider--b input:checked + span:before,\n.checkbox-slider-danger.checkbox-slider--b-flat input:checked + span:before,\n.checkbox-slider-danger.checkbox-slider--c input:checked + span:before,\n.checkbox-slider-danger.checkbox-slider--c-weight input:checked + span:before {\n  background: #d9534f;\n}\n"
  },
  {
    "path": "gui/static/fonts/icomoon.json",
    "content": "{\n  \"selection\": [\n    {\n      \"order\": 40,\n      \"prevSize\": 28,\n      \"name\": \"money\"\n    },\n    {\n      \"order\": 41,\n      \"prevSize\": 20,\n      \"name\": \"load_balance\"\n    },\n    {\n      \"order\": 42,\n      \"ligatures\": \"\",\n      \"prevSize\": 20,\n      \"name\": \"fusionpbx_full\"\n    },\n    {\n      \"order\": 43,\n      \"ligatures\": \"\",\n      \"prevSize\": 20,\n      \"name\": \"gryphon\"\n    },\n    {\n      \"order\": 44,\n      \"ligatures\": \"\",\n      \"prevSize\": 20,\n      \"name\": \"fusionpbx\"\n    },\n    {\n      \"order\": 45,\n      \"ligatures\": \"\",\n      \"prevSize\": 20,\n      \"name\": \"asterisk\"\n    },\n    {\n      \"order\": 46,\n      \"ligatures\": \"\",\n      \"prevSize\": 20,\n      \"name\": \"kamailio\"\n    },\n    {\n      \"order\": 47,\n      \"ligatures\": \"\",\n      \"prevSize\": 20,\n      \"name\": \"flowroute\"\n    },\n    {\n      \"order\": 48,\n      \"ligatures\": \"\",\n      \"prevSize\": 20,\n      \"name\": \"freepbx\"\n    },\n    {\n      \"order\": 49,\n      \"ligatures\": \"\",\n      \"prevSize\": 20,\n      \"name\": \"zencommunication\"\n    },\n    {\n      \"order\": 50,\n      \"name\": \"call_failfwd\",\n      \"prevSize\": 20\n    },\n    {\n      \"order\": 51,\n      \"name\": \"call_hardfwd\",\n      \"prevSize\": 20\n    },\n    {\n      \"order\": 52,\n      \"name\": \"transnexus\",\n      \"prevSize\": 20,\n      \"codes\": [\n        59658,\n        59659\n      ]\n    },\n    {\n      \"order\": 53,\n      \"ligatures\": \"\",\n      \"prevSize\": 20,\n      \"name\": \"circle-up\"\n    },\n    {\n      \"order\": 54,\n      \"ligatures\": \"\",\n      \"prevSize\": 20,\n      \"name\": \"circle-down\"\n    }\n  ],\n  \"metadata\": {\n    \"name\": \"icomoon\",\n    \"iconsHash\": 510358743\n  },\n  \"height\": 1024,\n  \"prevSize\": 32,\n  \"icons\": [\n    {\n      \"paths\": [\n        \"M438.857 658.286h219.429v-54.857h-73.143v-256h-65.143l-84.571 78.286 44 45.714c13.714-12 22.286-18.286 31.429-32.571h1.143v164.571h-73.143v54.857zM731.429 512c0 104-62.857 237.714-182.857 237.714s-182.857-133.714-182.857-237.714 62.857-237.714 182.857-237.714 182.857 133.714 182.857 237.714zM1024 658.286v-292.571c-80.571 0-146.286-65.714-146.286-146.286h-658.286c0 80.571-65.714 146.286-146.286 146.286v292.571c80.571 0 146.286 65.714 146.286 146.286h658.286c0-80.571 65.714-146.286 146.286-146.286zM1097.143 182.857v658.286c0 20-16.571 36.571-36.571 36.571h-1024c-20 0-36.571-16.571-36.571-36.571v-658.286c0-20 16.571-36.571 36.571-36.571h1024c20 0 36.571 16.571 36.571 36.571z\"\n      ],\n      \"width\": 1097,\n      \"tags\": [\n        \"money\"\n      ],\n      \"grid\": 14\n    },\n    {\n      \"paths\": [\n        \"M870.4 614.4h-307.2v204.8h51.2v204.8h-204.8v-204.8h51.2v-204.8h-307.2v204.8h51.2v204.8h-204.8v-204.8h51.2v-204.8c0-56.554 45.846-102.4 102.4-102.4v0h307.2v-204.8h-102.4v-307.2h307.2v307.2h-102.4v204.8h307.2c56.554 0 102.4 45.846 102.4 102.4v0 204.8h51.2v204.8h-204.8v-204.8h51.2v-204.8z\"\n      ],\n      \"tags\": [\n        \"load_balance\"\n      ],\n      \"defaultCode\": 57617,\n      \"grid\": 20\n    },\n    {\n      \"paths\": [\n        \"M170.797 60.487c-133.263 40.003-208.245 79.971-208.245 109.95s-6.676 31.64 104.97-6.676c118.305-41.636 258.208-58.291 354.879-43.333 74.945 10.014 76.642 11.656 66.645 61.62-4.998 26.678-24.982 71.616-44.965 98.294-43.333 61.62-31.64 98.294 34.978 98.294 38.307 0 58.291-11.656 83.3-53.32 18.324-30.007 36.665-74.945 41.636-99.954 11.656-48.294 58.291-64.949 58.291-20.011 0 13.353-14.985 61.62-34.978 106.621-33.336 78.274-33.336 83.3-6.676 141.627 16.682 31.64 38.307 56.658 48.294 54.962 28.311-8.327 123.276-213.27 134.959-288.197 8.327-64.949 8.327-64.949-18.324 14.985-13.353 43.333-28.311 78.274-31.64 74.945-6.676-6.676-91.609-231.557-91.609-241.554 0-6.676 51.633 3.347 119.947 20.011 14.985 4.998 31.64 20.011 34.978 33.336 4.998 14.985 6.676 13.353 4.998-4.998-3.347-49.991-116.609-76.642-343.195-81.603-164.939-4.998-229.925 0-308.226 24.982zM878.913 173.785c0 26.678-20.011 98.294-43.333 163.243-23.349 63.316-43.333 129.934-44.965 144.956s-6.676 26.678-13.353 24.982c-4.998-1.669-30.007 21.653-56.658 51.633-36.665 43.333-49.991 49.991-68.287 34.978-20.011-14.985-21.653-14.985-14.985 4.998 14.985 38.307-40.003 51.633-86.629 20.011l-41.636-26.678 38.307 51.633c26.678 36.665 33.336 56.658 20.011 69.974-11.656 11.656-40.003-8.327-93.296-66.645-66.645-76.642-81.603-84.942-143.259-84.942-38.307 0-68.287 4.998-68.287 10.014 0 23.349 119.947 186.565 169.91 231.557 49.991 46.662 51.633 46.662 14.985 4.998-23.349-26.678-36.665-51.633-30.007-56.658 18.324-20.011 244.883 11.656 234.886 33.336-4.998 10.014 16.682-10.014 48.294-44.965 81.603-88.271 156.585-239.912 194.919-388.206 40.003-156.585 43.333-223.257 8.327-223.257-16.682 0-24.982 16.682-24.982 44.965zM404.060 595.3c13.353 21.653-8.327 21.653-41.636 0-21.653-13.353-21.653-16.682 3.347-16.682 14.985 0 33.336 6.676 38.307 16.682zM162.461 195.456c-24.982 8.327 6.676 10.014 74.945 4.998l116.609-8.327-53.32 54.962c-30.007 30.007-78.274 63.316-108.318 71.616-40.003 13.353-54.962 28.311-54.962 56.658-1.669 48.294-69.974 119.947-81.603 84.942-4.998-13.353-21.653-53.32-34.978-89.967l-24.982-66.645 10.014 58.291c4.998 31.64 38.307 121.634 76.642 198.248 49.991 104.97 96.625 169.91 186.565 259.904 128.301 129.934 174.936 163.243 208.245 143.259 13.353-10.014-14.985-48.294-94.956-128.301-129.934-129.934-226.586-298.212-184.923-324.89 13.353-8.327 64.949-14.985 116.609-14.985 108.318 0 136.601-30.007 86.629-88.271-26.678-28.311-44.965-31.64-141.627-23.349-86.629 4.998-113.28 1.669-113.28-14.985 0-23.349 98.294-56.658 169.91-56.658 31.64 0 53.32-14.985 78.274-56.658 20.011-30.007 34.978-59.987 34.978-66.645 0-14.985-216.599-10.014-266.563 6.676zM9.178 247.134c-16.682 8.327-30.007 24.982-30.007 36.665 0 13.353 4.998 14.985 13.353 1.669 6.676-10.014 26.678-26.678 44.965-36.665s26.678-18.324 16.682-18.324c-8.327 0-30.007 6.676-44.965 16.682zM1028.839 536.982v208.245h49.991c49.991 0 49.991-1.669 49.991-91.609v-91.609h74.945c68.287 0 74.945-3.347 74.945-41.636s-6.676-41.636-74.945-41.636c-63.316 0-74.945-4.998-74.945-33.336s11.656-33.336 74.945-33.336c68.287 0 74.945-3.347 74.945-41.636 0-40.003-3.347-41.636-124.972-41.636h-124.972v208.245zM2761.596 536.982v191.59h41.636c38.307 0 41.636-6.676 41.636-71.616 0-69.974 1.669-73.312 61.62-88.271 84.942-20.011 119.947-81.603 89.967-154.943-13.353-28.311-30.007-51.633-38.307-51.633-10.014 0-8.327 10.014 3.347 24.982 11.656 13.353 14.985 38.307 8.327 53.32-10.014 28.311-11.656 28.311-26.678 0-21.653-38.307-114.976-40.003-114.976-1.669 0 23.349-4.998 23.349-24.982 6.676-13.353-11.656-24.982-34.978-24.982-51.633 0-23.349 16.682-31.64 79.971-36.665l78.274-4.998-174.936-6.676v191.59zM2919.868 462.009c0 13.353-16.682 26.678-36.665 30.007-30.007 4.998-38.307-3.347-38.307-30.007s8.327-34.978 38.307-30.007c20.011 3.347 36.665 16.682 36.665 30.007zM3083.166 357.057c24.982 3.347 61.62 3.347 83.3 0 20.011-3.347 0-6.676-46.662-6.676-44.965 0-63.316 3.347-36.665 6.676zM3294.776 355.415c0 4.998 23.349 43.333 53.32 84.942 28.311 43.333 51.633 86.629 51.633 96.625s-23.349 53.32-51.633 96.625c-63.316 89.967-63.316 94.956-20.011 94.956 20.011 0 48.294-23.349 69.974-58.291 21.653-31.64 43.333-58.291 49.991-58.291s30.007 26.678 49.991 58.291c24.982 38.307 51.633 58.291 76.642 58.291 41.636 0 41.636 0-33.336-111.647-24.982-38.307-46.662-73.312-46.662-79.971s23.349-44.965 53.32-88.271c44.965-68.287 46.662-73.312 10.014-41.636-53.32 49.991-66.645 48.294-43.333-1.669 10.014-24.982 34.978-44.965 61.62-48.294l43.333-6.676-44.965-3.347c-33.336-1.669-51.633 11.656-79.971 56.658-20.011 31.64-41.636 58.291-48.294 58.291-4.998 0-24.982-26.678-41.636-58.291-23.349-41.636-43.333-58.291-71.616-58.291-20.011 0-38.307 4.998-38.307 10.014zM3373.086 382.066c10.014 11.656 21.653 34.978 26.678 49.991 14.985 41.636-30.007 20.011-63.316-30.007-21.653-33.336-21.653-40.003-1.669-40.003 11.656 0 30.007 10.014 38.307 20.011zM3246.454 390.366c26.678 44.965 8.327 68.287-34.978 41.636-44.965-28.311-91.609-24.982-118.305 8.327-21.653 24.982-24.982 21.653-33.336-21.653-6.676-26.678-11.656 31.64-13.353 129.934l-1.669 179.898h96.625c73.312 0 104.97-8.327 124.972-30.007 30.007-33.336 38.307-128.301 13.353-143.259-8.327-4.998-10.014-41.636-6.676-81.603 6.676-54.962 1.669-76.642-21.653-94.956-26.678-23.349-26.678-21.653-4.998 11.656zM3194.794 462.009c0 24.982-11.656 33.336-40.003 33.336-31.64 0-40.003-8.327-38.307-33.336 1.669-23.349 13.353-33.336 40.003-33.336s38.307 10.014 38.307 33.336zM3196.482 578.645c24.982 0 16.682 66.645-10.014 76.642-44.965 18.324-71.616-4.998-61.62-51.633 4.998-30.007 14.985-40.003 31.64-34.978 13.353 4.998 31.64 10.014 40.003 10.014zM1312.084 503.673c0 131.63 26.678 211.574 74.945 231.557 61.62 23.349 143.259 10.014 183.227-31.64 38.307-36.665 41.636-53.32 41.636-183.227 0-139.93 0-141.627-41.636-141.627s-41.636 1.669-41.636 133.263c0 113.28-4.998 134.959-31.64 149.918-41.636 21.653-44.965 21.653-74.945-10.014-20.011-18.324-26.678-59.987-26.678-149.918 0-119.947-1.669-123.276-41.636-123.276s-41.636 3.347-41.636 124.972zM1677.004 413.706c-53.32 53.32-36.665 113.28 48.294 169.91 38.307 26.678 69.974 53.32 69.974 61.62 0 28.311-56.658 33.336-86.629 4.998-26.678-24.982-33.336-24.982-54.962-3.347-23.349 21.653-21.653 30.007 8.327 61.62 49.991 53.32 136.601 49.991 181.594-6.676 20.011-24.982 34.978-54.962 34.978-68.287 0-23.349-49.991-76.642-118.305-123.276-49.991-34.978-30.007-71.616 28.311-44.965 31.64 14.985 44.965 14.985 61.62-1.669 14.985-16.682 11.656-26.678-20.011-53.32-49.991-41.636-109.95-40.003-153.256 3.347zM1895.209 561.991v183.227h83.3v-366.517h-83.3v183.227zM2100.151 410.368c-99.954 63.316-119.947 178.265-44.965 266.563 109.95 131.63 323.202 59.987 323.202-108.318 0-146.589-154.943-234.886-278.201-158.281zM2255.113 475.335c69.974 44.965 58.291 158.281-18.324 186.565-71.616 26.678-141.627-21.653-141.627-98.294s96.625-131.63 159.914-88.271zM2411.725 561.991v183.227h41.636c38.307 0 40.003-6.676 44.965-103.292l4.998-103.292 63.316 103.292c43.333 69.974 74.945 103.292 96.625 103.292 30.007 0 31.64-13.353 31.64-183.227s-1.669-183.227-33.336-183.227c-28.311 0-31.64 13.353-36.665 111.647l-4.998 111.647-73.312-111.647c-46.662-71.616-83.3-111.647-103.292-111.647-28.311 0-31.64 14.985-31.64 183.227z\"\n      ],\n      \"width\": 3657,\n      \"tags\": [\n        \"fusionpbx_full\"\n      ],\n      \"defaultCode\": 59648,\n      \"grid\": 20\n    },\n    {\n      \"paths\": [\n        \"M130.768 87.178c75.416 34.595 298.897 85.795 375.005 85.795 31.135 0 31.135-0.692 37.362 93.405 13.146 213.103 112.778 371.546 251.157 401.297 26.984 5.535 26.984 7.611 4.843 25.6-26.292 20.757-32.519 35.978-22.832 58.811 4.843 10.378 8.303 22.141 8.995 26.292 1.384 20.065 11.070 24.216 31.135 13.838 22.832-11.762 45.665-13.146 51.892-3.459 2.076 4.151-2.076 6.919-9.686 6.919s-15.914 3.459-17.989 6.919c-2.768 4.151 4.151 6.919 15.222 6.919 20.065 0 50.508 16.605 50.508 27.676 0 2.768-11.070 2.768-23.524 0-13.146-2.768-25.6-4.151-26.984-2.768-1.384 2.076 7.611 13.838 21.449 27.676 13.146 13.146 22.141 25.6 20.065 27.676s-15.914-2.768-31.827-10.378c-15.222-8.303-35.978-14.53-46.357-14.53-21.449 0-78.876 25.6-78.876 35.286 0 3.459-4.843 6.227-9.686 6.227-15.222 0 0-38.054 22.141-55.351 16.605-13.838 17.297-14.53 4.151-24.216-6.919-5.535-18.681-8.303-24.908-6.227-14.53 4.151-59.503-17.989-79.568-40.13-20.757-22.141-29.751-49.124-29.751-88.562 0-31.827-0.692-33.903-24.216-41.514-32.519-10.378-87.178-61.578-107.935-99.632l-16.605-31.827h-42.205c-53.968 0-94.097-12.454-141.838-43.589-42.897-27.676-92.714-72.649-87.178-78.876 2.076-2.076 13.838 3.459 25.6 11.762 32.519 23.524 94.789 42.205 155.676 48.432 30.443 2.768 56.735 3.459 58.119 2.076 1.384-0.692-0.692-9.686-4.843-19.373-6.227-13.838-13.146-17.297-34.595-17.297-71.957 0-168.13-43.589-222.097-100.324-33.903-35.978-83.027-121.081-69.881-121.081 3.459 0 20.065 12.454 35.286 27.676 54.659 52.584 143.222 95.481 222.789 106.551 20.757 2.768 25.6 1.384 25.6-8.303 0-20.757-9.686-28.368-44.281-35.978-76.8-17.297-170.205-71.265-248.389-144.605l-33.211-30.443 2.076 352.173c2.076 348.022 2.076 352.865 17.297 380.541 23.524 43.589 45.665 67.114 86.486 89.254l36.67 20.757h747.243v-740.324l-19.373-39.438c-20.757-42.205-44.281-65.73-90.638-90.638-28.368-15.222-31.827-15.222-391.611-16.605l-363.243-1.384 37.362 17.297zM635.849 207.568c8.303 0 26.292-4.843 40.13-10.378 28.368-11.762 80.259-13.838 99.632-2.768 7.611 3.459 20.065 17.297 26.984 29.059 10.378 16.605 18.681 22.141 35.286 22.832 42.897 2.076 68.497 28.368 68.497 69.881 0 16.605-0.692 17.297-11.762 7.611-24.216-20.757-36.67-20.065-58.119 0.692-31.135 31.135-24.216 71.957 17.989 111.395 27.676 25.6 40.13 54.659 40.13 93.405 0 35.286-13.146 69.881-32.519 85.795-8.303 6.919-18.681 6.919-47.049 1.384-74.032-15.914-128-63.654-168.822-148.065-13.146-26.984-23.524-52.584-23.524-56.735 0-3.459 10.378-11.762 22.832-17.297 29.751-13.838 31.135-29.059 4.151-40.822-26.292-11.070-51.2-61.578-58.119-116.238-5.535-42.897 0-56.043 16.605-40.822 6.919 6.227 18.681 11.070 27.676 11.070zM214.486 528.605c8.995 4.151 21.449 16.605 26.984 28.368 5.535 11.070 15.914 31.827 22.832 45.665 21.449 42.897 15.914 64.346-26.292 112.086-24.908 26.984-28.368 42.205-15.914 69.881 7.611 16.605 45.665 40.822 55.351 35.286 2.076-2.076 6.227-21.449 8.995-44.281 6.919-60.886 44.973-132.843 87.87-166.746 43.589-34.595 61.578-40.13 70.573-23.524 3.459 7.611 24.908 31.827 47.741 53.968 22.141 22.832 40.13 41.514 40.13 42.897 0 0.692-10.378 4.151-22.141 6.919-32.519 6.919-54.659 23.524-65.038 49.124-13.838 32.519-12.454 36.67 16.605 37.362 40.13 0 84.411 20.757 107.243 49.124 19.373 24.216 22.141 46.357 3.459 27.676-4.843-4.843-13.146-8.303-18.681-8.303-8.303 0-8.303 2.076-2.076 8.303 4.843 4.843 8.303 12.454 8.303 17.989 0 6.919-2.768 6.919-15.222-1.384-8.303-6.227-20.065-11.070-25.6-11.070-7.611 0-7.611 2.076 1.384 12.454 17.989 19.373 14.53 22.141-12.454 11.070-17.297-6.919-52.584-9.686-125.924-9.686-116.238 0-144.605-5.535-178.508-35.286-47.741-42.205-49.124-104.476-2.768-154.984 14.53-15.914 26.984-33.211 26.984-38.054 0-14.53-21.449-34.595-35.978-34.595-13.146 0-33.211-35.978-44.281-78.184-4.151-19.373-4.151-19.373 22.141-15.222 15.222 2.768 34.595 8.303 44.281 13.146z\"\n      ],\n      \"width\": 1038,\n      \"tags\": [\n        \"gryphon\"\n      ],\n      \"defaultCode\": 59649,\n      \"grid\": 20\n    },\n    {\n      \"paths\": [\n        \"M288.768 102.4c-116.736 28.672-225.28 83.968-212.992 112.64 8.192 22.528 22.528 20.48 81.92-4.096 90.112-36.864 333.824-57.344 372.736-30.72 24.576 18.432 22.528 34.816-14.336 108.544-26.624 51.2-36.864 96.256-26.624 106.496 36.864 36.864 96.256 2.048 135.168-81.92 63.488-135.168 100.352-112.64 40.96 26.624-26.624 65.536-26.624 79.872 2.048 137.216l32.768 61.44 38.912-49.152c45.056-57.344 108.544-253.952 94.208-290.816-6.144-12.288-12.288-4.096-12.288 20.48-4.096 77.824-40.96 53.248-55.296-32.768-10.24-67.584-22.528-81.92-73.728-92.16-83.968-16.384-317.44-12.288-403.456 8.192zM794.624 137.216c14.336 6.144 32.768 4.096 38.912-2.048 8.192-6.144-4.096-12.288-26.624-10.24-22.528 0-28.672 6.144-12.288 12.288zM888.832 260.096c-8.192 53.248-16.384 112.64-14.336 129.024 4.096 45.056-53.248 147.456-112.64 198.656-47.104 43.008-88.064 57.344-135.168 49.152-10.24-2.048-12.288 10.24-6.144 26.624 32.768 81.92-49.152 49.152-194.56-79.872-43.008-34.816-77.824-40.96-77.824-10.24 0 28.672 94.208 178.176 124.928 196.608 16.384 8.192 18.432 6.144 10.24-8.192-10.24-18.432 22.528-24.576 116.736-24.576 100.352 0 139.264-8.192 163.84-36.864 92.16-102.4 210.944-471.040 163.84-518.144-14.336-14.336-26.624 6.144-38.912 77.824zM387.072 223.232l49.152 12.288-65.536 57.344c-65.536 53.248-65.536 55.296-18.432 55.296 57.344 0 151.552-88.064 129.024-120.832-6.144-12.288-43.008-22.528-77.824-20.48-59.392 2.048-61.44 4.096-16.384 16.384zM262.144 239.616c14.336 6.144 32.768 4.096 38.912-2.048 8.192-6.144-4.096-12.288-26.624-10.24-22.528 0-28.672 6.144-12.288 12.288zM288.768 403.456c-26.624 6.144-65.536 30.72-86.016 53.248-32.768 38.912-40.96 40.96-59.392 14.336-38.912-57.344-20.48 2.048 32.768 104.448 86.016 169.984 315.392 395.264 358.4 352.256 10.24-10.24-16.384-47.104-69.632-92.16-92.16-75.776-221.184-288.768-192.512-315.392 8.192-10.24 55.296-16.384 102.4-18.432 100.352 0 126.976-26.624 81.92-75.776-32.768-36.864-69.632-40.96-167.936-22.528z\"\n      ],\n      \"tags\": [\n        \"fusionpbx\"\n      ],\n      \"defaultCode\": 59650,\n      \"grid\": 20\n    },\n    {\n      \"paths\": [\n        \"M668.374 102.904c-2.522 1.009-23.204 6.053-45.399 11.098-88.78 19.673-160.914 57.001-240.615 125.1-58.514 49.939-123.586 129.135-143.764 175.543-3.531 8.071-8.575 14.124-11.602 14.124-2.522 0-5.044-1.009-5.044-2.018 0-6.558 31.779-75.665 44.39-97.86 8.575-14.124 15.133-26.231 14.124-26.231-2.522 0-54.479 65.576-66.081 83.232-24.717 39.346-44.895 78.692-57.001 113.498-12.611 34.806-13.62 44.39-13.115 110.975 0 67.090 1.513 76.169 13.62 106.94 38.841 97.86 105.427 157.888 217.915 196.225 32.284 11.098 43.381 12.106 131.153 12.106 84.24 0 101.391-1.513 141.241-11.602 50.443-13.115 84.745-25.222 134.179-47.417l33.293-15.133 41.364 13.115c22.7 7.062 45.399 13.115 50.443 13.115 5.549 0 29.762 5.549 54.983 11.602l45.903 12.106-35.31-68.099c-19.168-37.833-34.806-71.63-34.806-75.161 0-4.035 5.044-12.106 11.602-18.16 33.797-32.788 85.249-105.931 113.498-162.428 74.152-148.303 53.974-300.642-53.47-404.556-31.275-29.762-49.939-39.346-26.231-13.62 24.717 27.239 38.841 48.426 50.443 75.665 16.142 36.824 11.602 35.815-15.133-5.044-61.036-90.798-130.144-123.586-269.872-127.117-41.364-1.009-77.683-1.009-80.709 0zM809.616 143.259c130.648 23.204 215.393 126.613 214.889 262.81 0 96.851-46.912 205.809-123.586 288.536l-28.753 31.275 27.239 41.868c30.266 45.903 38.841 61.541 38.841 68.603 0 4.54-28.753-4.54-95.338-31.275-18.16-7.062-36.319-13.115-41.364-13.115-4.54 0-27.239 7.567-50.948 17.151-23.204 9.584-67.090 23.204-97.356 30.77-49.939 12.611-60.532 13.62-120.56 11.098-73.143-3.027-120.56-15.133-172.012-44.39-114.002-64.063-164.445-190.171-126.108-314.262 23.204-75.161 55.992-128.126 120.56-192.694 124.091-123.586 298.625-183.614 454.495-156.374zM574.045 322.837l-1.513 80.709-32.788-21.186c-18.16-11.602-45.399-29.257-60.532-39.346s-30.77-19.673-34.301-22.195c-3.531-2.018-7.567-0.504-9.584 5.549-1.513 5.044-7.062 14.629-11.602 21.691s-14.124 23.204-21.691 36.319l-13.62 23.708 38.841 21.691c21.186 12.106 50.948 28.753 66.585 37.328 15.133 8.575 28.248 17.655 29.257 20.177s-9.080 10.593-22.195 17.655c-27.239 15.133-108.958 61.036-111.984 63.054-1.009 1.009 3.027 8.071 9.080 16.142 6.053 8.575 10.593 16.142 10.593 17.655 0 5.044 31.779 53.974 34.806 53.974 1.513 0 8.575-4.54 15.637-10.089s14.629-10.089 16.646-10.089c2.522 0 11.602-5.044 20.177-11.602 17.151-12.106 24.213-16.142 58.010-35.815l21.186-12.611v155.87h101.391l-1.513-76.169c-1.513-68.603-0.504-75.665 7.062-73.647 4.54 1.513 34.806 18.664 67.090 38.841 31.779 20.177 59.019 35.815 60.028 34.806s13.115-21.186 27.239-44.39l25.222-42.877-66.081-36.824c-36.319-20.177-68.099-38.841-70.116-40.859-3.531-3.531 60.532-42.877 118.037-73.143l19.168-9.584-9.080-13.62c-14.629-22.195-20.682-32.284-31.779-53.47-6.053-11.098-11.602-20.682-12.106-21.691-1.009-1.009-16.142 8.071-33.293 20.177-33.797 23.204-99.373 64.567-101.391 64.567-1.009 0-0.504-36.319 0-80.709l2.018-80.709h-101.896l-1.009 80.709z\"\n      ],\n      \"width\": 1251,\n      \"tags\": [\n        \"asterisk\"\n      ],\n      \"defaultCode\": 59651,\n      \"grid\": 20\n    },\n    {\n      \"paths\": [\n        \"M1492.234 124.822c-152.161 5.636-351.326 56.836-588.74 151.322-48.562 19.425-109.954 45.325-109.235 46.164 0.36 0.24 7.674-1.799 16.547-4.556 28.897-8.993 33.814-10.432 80.697-23.981 47.483-13.669 112.472-29.737 170.867-42.447 149.044-32.255 294.010-47.723 430.464-45.924 56.236 0.839 86.213 2.998 125.062 9.233 56.236 8.993 98.683 26.739 125.662 52.399l9.353 8.873 1.319-4.556c3.477-12.47 5.036-26.499 4.437-40.528-1.079-24.821-7.554-39.929-24.701-57.075-25.060-25.060-61.992-38.010-129.019-45.444-21.104-2.278-68.946-4.916-82.735-4.556-3.237 0.12-16.787 0.6-29.977 1.079zM788.984 323.747c-0.959 0.6-1.199 1.199-0.6 1.199 0.719 0 2.038-0.6 2.998-1.199s1.319-1.199 0.6-1.199c-0.6 0-2.038 0.6-2.998 1.199zM1687.681 382.621c-74.582 4.916-125.182 76.5-106.237 150.243 2.758 10.312 11.031 28.778 16.787 37.171 14.629 21.223 39.449 38.73 64.989 45.804 14.029 3.837 40.289 5.276 54.677 2.878 38.49-6.115 71.344-30.096 87.891-64.15 7.074-14.509 10.312-25.66 11.991-41.967 2.998-27.339-4.197-56.476-19.665-79.498-6.355-9.353-21.343-24.461-30.576-30.696-22.183-14.988-49.641-21.823-79.858-19.785zM1715.74 420.871c36.571 8.873 60.433 40.768 60.433 80.937 0 37.89-21.343 68.107-55.517 78.659-12.71 3.957-31.655 3.957-44.365 0-22.183-6.835-38.37-20.984-48.682-42.687-10.672-22.063-10.312-53.238 0.719-75.781 16.187-32.974 51.68-49.641 87.412-41.128zM444.373 389.815c-1.799 4.317-19.065 44.126-37.291 85.613-3.837 8.513-9.473 21.463-12.59 28.778-3.118 7.194-15.228 34.893-26.859 61.392s-21.104 48.802-21.104 49.521c0 0.839 6.235 1.199 20.024 0.959l20.024-0.36 16.307-38.37c8.993-21.104 23.861-55.876 32.974-77.34 8.993-21.463 19.185-45.325 22.542-53.119 3.237-7.794 6.355-13.669 6.955-13.19 0.839 0.959 39.449 89.69 39.449 90.889 0 0.36-12.59 0.6-27.818 0.6h-27.938l-6.835 16.547c-3.837 9.113-6.955 16.907-6.955 17.386s18.945 0.959 42.207 1.079l42.207 0.36 6.715 15.588c3.717 8.513 9.113 21.104 11.871 27.818l5.156 12.35h41.847l-0.839-2.758c-0.36-1.439-1.439-3.957-2.278-5.636-1.679-3.237-21.104-47.603-95.326-217.99l-2.278-5.036h-38.25l-1.919 4.916zM976.877 424.708c-32.015 73.143-81.416 186.215-82.496 188.972l-1.079 2.638 20.624-0.24 20.504-0.36 8.393-19.785c7.434-17.626 30.576-72.304 58.874-138.612 5.276-12.59 10.192-23.022 10.792-23.142s4.197 6.955 8.034 15.708c3.717 8.753 12.35 28.658 19.065 44.126s12.47 28.897 12.83 29.617c0.48 1.199-5.516 1.559-27.459 1.559h-28.178l-7.194 17.027-7.074 17.147 85.133 1.199 11.631 27.578 11.511 27.578 21.104 0.36 21.223 0.24-1.079-2.638c-1.079-2.758-6.115-14.149-47.723-109.474-11.511-26.379-23.741-54.198-26.979-61.752-3.357-7.554-10.312-23.622-15.708-35.732l-9.593-21.823h-37.651l-17.506 39.809zM130.698 501.208v115.11h39.569v-66.548l17.506-18.346c9.593-9.952 17.986-18.226 18.466-18.226 0.959 0 11.271 13.43 66.908 86.572l12.59 16.547h23.861c18.825 0 23.741-0.36 23.022-1.559-0.839-1.319-43.646-57.555-81.776-107.556l-16.907-22.183 8.034-8.393c12.83-13.43 42.207-43.766 65.589-67.747l21.583-22.183-23.382-0.36c-12.83-0.12-23.861-0.12-24.701 0.24-1.199 0.48-25.18 25.42-87.412 91.249-11.871 12.47-21.943 22.902-22.422 23.142-0.6 0.24-0.959-25.54-0.959-57.195v-57.675h-39.569v115.11zM625.911 501.208v115.11h39.569l0.24-82.256 0.36-82.376 35.252 52.639c19.305 29.017 35.852 53.238 36.571 54.078 0.959 0.959 10.432-12.35 37.291-52.519 19.785-29.617 36.332-53.838 36.931-53.838 0.48 0 0.839 36.931 0.839 82.136v82.136h39.569v-230.22h-42.687l-2.998 4.437c-1.559 2.518-17.386 27.099-35.133 54.677l-32.255 50.121-3.717-5.636c-2.038-3.118-17.866-27.578-35.013-54.318l-31.415-48.682-21.703-0.36-21.703-0.24v115.11zM1176.281 501.208v115.11h39.569v-230.22h-39.569v115.11zM1278.201 501.208v115.11h161.874v-35.972h-121.105v-194.248h-40.768v115.11zM1485.639 501.208v115.11h39.569v-230.22h-39.569v115.11zM1133.714 708.766c-20.264 5.756-86.093 22.662-107.796 27.698-31.775 7.314-36.452 8.393-52.279 11.751-84.294 17.866-130.458 25.78-221.227 37.77-7.194 0.959-21.823 2.518-32.375 3.477-87.652 8.513-110.194 9.593-195.447 9.712-77.699 0-100.721-1.079-149.283-7.314-82.496-10.312-136.333-30.336-168.588-62.711l-9.712-9.593-2.518 10.432c-14.748 58.754 7.794 99.043 68.706 122.904 47.843 18.705 140.65 28.178 229.98 23.502 64.51-3.357 159.356-17.267 226.863-33.454 5.276-1.319 16.667-3.957 25.18-5.995 101.8-24.101 215.352-59.833 345.93-108.875 14.149-5.396 33.574-12.71 43.166-16.427 24.581-9.593 24.461-9.952-0.6-2.878z\"\n      ],\n      \"width\": 1919,\n      \"tags\": [\n        \"kamailio\"\n      ],\n      \"defaultCode\": 59652,\n      \"grid\": 20\n    },\n    {\n      \"paths\": [\n        \"M253.2 236c-26 15.6-25.2 6.4-25.2 276.8 0 236 0.4 245.6 8 258 15.6 26 6.4 25.2 276.8 25.2 236 0 245.6-0.4 258-8 26-15.6 25.2-6.4 25.2-276s0.8-260.4-25.2-276c-12.4-7.6-22-8-258.8-8s-246.4 0.4-258.8 8zM566 320.4c8.8 2.4 24.4 8 35.2 12.4 16 6.8 18.8 10 18.8 19.2 0 10.8-0.8 10.8-27.2 12.8-70.8 5.2-126.4 63.6-131.6 138.4-2.8 36.4-12 58-30.4 68.8-28.8 17.6-70.4 13.2-93.2-9.6-18.4-18.4-23.6-40.8-18-80 10.4-75.6 57.6-133.2 127.6-157.2 30-10 90-12.8 118.8-4.8zM675.6 448c8.8 4.4 20 14.4 24.8 22.8 8 12.8 9.6 20 8.8 47.2-2 91.2-59.6 163.6-146 184-45.6 10.8-94 6.4-134-12-17.2-7.6-21.2-11.6-21.2-19.2 0-8.8 2-10 23.2-12 47.2-5.2 84.8-26.8 110-63.6 15.6-22.8 26.8-58.8 26.8-85.6 0-30 13.6-53.2 38-64 18-8 51.6-6.8 69.6 2.4zM456 328.8c-49.6 17.6-77.6 38.8-103.6 79.2-27.6 42.8-37.6 102.8-22.4 133.6 15.2 30.4 47.6 43.2 80 32 33.2-11.2 42-25.6 46.4-74.4 4-42 16.4-70.8 42.4-97.2 24.8-24.8 56.4-39.6 90-41.2 22-1.6 27.2-2.8 27.2-8.4 0-4.4-9.2-10.4-28.8-18.4-41.2-16-94-18.4-131.2-5.2zM536 328c0 2 3.2 4 7.2 4 10.4 0 44.8 12 44.8 16 0 1.6-8 4-18 4.8-32.4 4-90.8 40-83.6 52 1.6 2.8 1.2 3.2-1.6 1.6-6-3.6-22.4 22.8-30.8 48.8-4 12.4-6.4 29.2-5.2 42.4 0.4 12.4 0 22.4-1.2 22.4-1.6 0-4 4.4-5.2 9.6-2 8-11.6 18-37.2 38.4-6.8 5.6-28.4 4.8-36-1.2-18.4-14.4-33.2-30.8-33.2-36.4 0-3.6-1.6-6.4-4-6.4-2 0-4-10-4-22s1.6-22 3.2-22c2 0 5.6-8.8 8.4-20 6.4-25.2 29.6-61.2 54-83.2 20.8-19.2 60-39.6 83.6-43.2 8-1.6 14.8-4 14.8-6s10-3.6 22-3.6c12 0 22 1.6 22 4zM608 452.8c-24 12.4-32.4 28-36.4 70-4.4 43.2-16.8 70.8-43.6 97.2-24.8 25.2-54.8 39.6-89.6 42.8-35.6 3.2-34.4 11.2 3.6 26 54.4 20.8 104.4 18.8 158-7.2 62.4-29.6 103.6-94.8 104.8-164.4 0.4-34.8-12-56.4-38.4-67.2-20-8.4-38-7.6-58.4 2.8zM644 452c0 2 4 4 8.4 4 7.6 0 26.4 16.4 42.8 37.2 5.6 7.2 6.8 46.8 1.2 46.8-2 0-4.8 6-6 12.8-4.8 25.2-25.6 62-47.6 84.4-26 26.8-70 49.6-102 53.6-11.6 1.2-20.8 4-20.8 5.6 0 2-2.8 3.6-6 3.6s-6-2-6-4c0-2.4-5.2-4-12-4-6.4 0-24-4-38.8-8.8l-27.2-9.2 22-3.2c71.2-10 120.8-64.4 127.6-140 4-40.4 12.4-57.6 34.4-68.8 8.8-4.4 16.8-9.2 17.2-11.2 2-4.4 12.8-3.6 12.8 1.2z\"\n      ],\n      \"tags\": [\n        \"flowroute\"\n      ],\n      \"defaultCode\": 59653,\n      \"grid\": 20\n    },\n    {\n      \"paths\": [\n        \"M843.419 67.559c-2.266 0.283-9.631 2.266-16.288 4.391-6.657 1.983-12.889 3.682-13.88 3.541-0.991 0-1.558 0.283-0.991 0.708 0.85 0.85-10.622 6.657-12.464 6.232-0.708-0.142-0.85 0.142-0.425 0.85s-2.266 2.549-5.807 4.391c-6.373 2.974-6.657 3.116-17.279 0.85-14.588-3.116-19.97-4.957-19.97-6.515 0-0.85-0.567-1.133-1.133-0.708-1.558 0.85-13.172-2.124-12.464-3.258 0.708-0.991-4.249-1.275-20.395-1.133-20.395 0.142-22.661 0.283-22.661 1.558 0 0.567-1.275 0.85-2.833 0.425-1.841-0.425-2.549-0.142-1.841 0.85 0.567 0.85-0.283 1.133-2.266 0.567-1.841-0.425-2.833-0.425-2.408 0 1.275 1.275-6.373 4.815-8.073 3.682-0.708-0.425-0.991-0.142-0.567 0.567 0.567 0.85-1.133 2.408-3.541 3.399-7.082 2.974-19.97 16.996-24.786 27.052-5.665 11.755-8.215 26.768-6.657 39.090 0.708 5.24 1.133 10.622 1.133 11.897s0.567 1.983 1.275 1.558c0.85-0.567 1.133 0.708 0.708 2.833-0.425 2.266-0.142 3.399 0.708 2.833s1.133 0.283 0.708 2.124c-0.567 1.7-0.283 2.833 0.283 2.408 0.708-0.425 1.7 0.708 2.124 2.408 0.85 2.974 5.099 13.313 10.056 24.786 2.549 5.807 4.815 17.562 4.815 25.494 0 6.090-1.558 9.773-13.172 32.292-13.313 25.352-15.013 52.687-4.674 70.108 8.64 14.163 16.571 31.159 15.58 32.717-0.708 1.133-0.425 1.275 0.85 0.567 1.416-0.85 1.7-0.142 1.133 2.549-0.425 2.266-0.142 3.399 0.708 2.833s1.133 0.567 0.708 2.974c-0.425 1.983-0.142 3.682 0.425 3.682 1.983 0 1.416 24.077-0.708 31.867-2.549 9.631-7.082 18.837-12.322 25.211s-18.412 16.571-25.635 19.828c-2.974 1.275-7.507 3.399-10.198 4.674-2.691 1.133-7.082 2.266-9.773 2.408s-4.674 0.708-4.249 1.275c1.7 2.974-23.369 0.142-29.601-3.258-1.416-0.708-3.541-1.275-4.674-1.275-3.258 0-12.039-5.099-10.906-6.373 0.567-0.708 0.142-0.708-0.85-0.142-0.991 0.708-8.356-3.116-18.554-9.489-9.206-5.807-18.129-11.047-19.828-11.614-1.7-0.425-2.549-1.558-2.124-2.408 0.567-0.991 0.283-1.133-0.567-0.567-2.124 1.275-10.339-3.399-9.206-5.24 0.425-0.85 0.283-0.991-0.567-0.567-1.7 1.133-31.867-18.129-30.451-19.404 0.567-0.567 0.283-0.85-0.708-0.567-0.85 0.283-7.365-3.824-14.446-9.206-7.082-5.24-13.172-9.489-13.738-9.489-1.133 0-25.635-20.962-32.15-27.477l-5.524-5.807 0.85 5.665c0.708 4.249 0.567 5.099-0.567 3.541-1.275-1.7-1.416-0.708-1.133 3.824 0.283 3.258-0.142 5.524-0.85 5.099s-0.991 0.85-0.567 2.833c0.425 2.124 0.283 3.399-0.283 3.116s-1.7 1.841-2.691 4.815c-0.85 3.116-3.541 7.507-5.807 10.056-4.107 4.391-4.532 4.532-13.455 3.824-5.24-0.425-9.773-1.133-10.198-1.558-0.567-0.425-0.142-0.85 0.85-0.85s1.275-0.708 0.85-1.558c-0.567-0.85-1.983-1.133-3.116-0.708s-1.7 0.283-1.275-0.425c0.85-1.275-5.24-4.391-8.781-4.391s-9.064 4.107-7.931 5.949c0.567 0.85 0.283 1.133-0.567 0.567-0.708-0.425-2.833 0.85-4.532 2.974-1.983 2.549-2.549 2.974-1.558 0.991 2.266-4.532 11.189-11.897 14.588-11.897 1.7 0 6.94 2.266 11.897 4.957 11.614 6.515 17.279 6.657 22.944 0.142 6.657-7.507 11.189-30.593 11.614-58.211 0.283-23.794 6.798-48.863 18.837-72.091 5.807-11.331 11.472-17.987 21.245-25.352 15.296-11.472 14.163-34.7-2.266-46.88-5.665-4.107-25.352-5.099-34.417-1.558-10.198 3.824-11.047 6.090-16.854 45.747-1.275 7.931-3.541 17.562-5.099 21.103-6.373 14.871-19.404 25.635-28.893 23.936-2.691-0.567-11.897-4.391-20.82-8.498-18.554-8.781-24.219-14.022-40.082-36.966-5.382-7.79-12.18-16.996-15.155-20.395-3.824-4.391-6.232-9.206-8.356-16.288-1.558-5.524-3.824-13.597-5.099-17.846-5.524-19.687-22.378-32.575-42.206-32.575-20.395 0-34.558 20.253-26.202 37.391 3.258 6.798 19.262 22.661 21.528 21.245 0.708-0.425 0.991-0.283 0.85 0.425-0.567 1.558 11.614 16.996 23.228 29.459 7.931 8.64 9.914 11.897 12.322 19.97 3.966 12.747 5.382 25.919 4.391 37.957-0.991 11.472-2.974 13.738-15.296 17.279-14.305 4.249-23.794 2.974-58.777-7.79-23.086-6.94-29.743-11.047-37.391-22.661-7.223-11.047-12.605-14.871-24.361-17.987-15.013-3.824-27.618-0.425-36.683 9.914-8.073 9.206-7.79 20.253 0.708 33 4.532 6.798 6.94 8.923 16.004 13.313 12.889 6.515 21.103 9.206 36.258 11.755 21.386 3.824 45.322 15.58 80.022 39.232 11.472 7.931 13.455 9.914 16.713 16.854 3.399 7.082 3.682 9.348 3.541 25.494 0 9.773-0.85 20.395-1.7 23.653-3.824 12.889-18.129 26.344-45.181 42.348-8.781 5.099-16.713 10.339-17.704 11.614-2.266 2.691-2.266 14.871 0.142 21.953 4.674 14.022 15.438 19.687 29.884 16.004 19.262-4.957 32.292-15.296 46.314-36.683 15.296-23.228 31.584-41.357 45.039-50.421 6.657-4.532 22.095-11.472 25.211-11.472 1.275 0 3.258-1.133 4.674-2.549 1.416-1.275 3.116-2.408 3.824-2.408 0.85 0 1.7-0.85 2.124-1.983s0-1.558-0.991-0.991c-0.991 0.708-1.275 0.142-0.567-1.275 0.708-1.841 0.991-1.983 1.983-0.425s1.275 1.416 1.275-0.142c0-1.133 0.425-1.558 0.85-1.133 1.133 1.133-3.399 8.356-4.815 7.648-0.567-0.283-2.408 0.85-3.966 2.408l-3.116 2.974 15.863-0.425c16.004-0.425 25.494 0.425 24.361 2.266-0.283 0.567 0.85 1.133 2.691 1.133 5.665 0.425 16.288 5.099 15.296 6.798-0.425 0.85-0.425 1.133 0.283 0.567 0.991-0.991 7.082 1.983 31.584 15.58 3.966 2.124 6.94 4.532 6.373 5.24-0.708 0.567-0.425 0.708 0.425 0.283 0.708-0.425 6.94 2.833 13.738 7.223 18.412 12.039 22.236 14.446 23.653 14.446 2.549 0 18.271 11.047 17.279 12.039-0.567 0.425-0.283 0.708 0.425 0.425 1.558-0.425 21.386 11.331 27.76 16.429 1.983 1.7 5.382 4.249 7.507 5.807 2.266 1.558 6.94 5.099 10.622 7.931 3.541 2.691 7.507 5.665 8.781 6.373s9.773 7.931 18.837 16.146c16.713 15.013 35.266 35.691 35.266 39.232 0 0.991 0.425 1.558 0.708 1.133 1.558-1.416 11.897 16.996 10.481 18.554-0.567 0.567-0.283 0.567 0.708 0 1.133-0.567 2.124 0.425 2.691 2.974 0.567 2.124 3.116 8.64 5.807 14.305 2.549 5.807 4.249 11.047 3.824 11.897-0.567 0.85-0.283 1.133 0.425 0.708 1.841-1.133 3.824 5.524 2.408 7.79-0.567 0.85-0.425 1.133 0.425 0.708 0.85-0.567 2.266 1.841 3.258 5.099 1.133 3.258 2.408 6.515 2.974 7.365 0.567 0.708 2.974 8.073 5.524 16.288 4.249 13.88 12.039 31.442 18.695 42.206 1.558 2.549 2.549 4.957 2.124 5.524-0.425 0.425-1.983-1.416-3.399-4.249-1.416-2.691-5.099-9.914-8.356-16.004-3.116-6.090-7.507-16.996-9.773-23.936-4.249-13.455-9.064-25.211-9.206-22.378 0 0.85 0.708 3.399 1.558 5.665 0.85 2.124 3.399 9.773 5.524 16.996 4.249 13.88 17.279 40.082 28.185 56.086 3.541 5.24 8.498 12.747 10.764 16.429 15.438 24.502 39.657 49.146 63.026 64.018 17.704 11.331 26.060 15.438 43.198 21.245 7.79 2.691 14.446 5.24 14.871 5.665 0.708 0.991 10.481 3.824 16.854 5.099 1.983 0.283 3.824 0.85 4.391 1.133 0.425 0.283 8.498 1.275 18.129 2.124 22.378 2.124 58.494 0 73.224-4.532 5.524-1.7 10.198-3.541 10.622-4.107s2.974-1.558 5.665-2.408c13.030-3.966 26.91-9.348 33.992-13.313 28.468-15.863 55.378-38.382 69.966-58.636 3.966-5.524 11.189-14.871 16.004-20.962 9.489-11.614 15.155-20.678 26.060-41.498 6.657-12.747 9.064-19.687 4.815-14.022-1.133 1.558-2.124 2.266-2.124 1.7 0-1.133 6.090-16.146 8.498-20.678 0.567-1.275 1.558-4.249 2.124-6.798 0.425-2.549 1.558-4.249 2.266-3.824 0.708 0.567 0.85-0.283 0.283-1.558-0.567-1.558-0.283-2.124 0.567-1.558s1.558 0.142 1.416-0.85c-0.142-4.391 0.567-6.798 1.7-6.090 0.708 0.567 0.991-0.567 0.425-2.266-0.567-2.266-0.283-2.691 1.133-1.841 1.275 0.85 1.558 0.567 0.708-0.708-0.708-1.133 0.142-4.249 1.841-7.648 1.7-3.258 7.082-14.446 12.18-24.927 4.957-10.481 13.030-24.502 17.846-31.159 9.914-13.455 39.374-44.189 41.357-43.056 0.708 0.425 0.991 0.142 0.85-0.708-0.283-0.85 1.275-2.691 3.258-3.966 2.124-1.416 9.348-7.082 16.288-12.747 6.798-5.665 13.030-9.773 13.738-9.348 0.85 0.425 1.133 0.283 0.708-0.425-0.991-1.558 32.575-24.077 34.841-23.511 0.85 0.283 1.133 0 0.425-0.567-0.567-0.567 1.7-2.549 5.099-4.249s12.605-7.223 20.395-12.464c14.871-9.773 29.601-18.412 36.258-21.103 2.124-0.85 3.824-2.124 3.824-2.691s0.567-1.133 1.416-1.133c0.708 0 4.391-1.983 8.073-4.249 3.682-2.408 7.648-3.966 8.64-3.541 0.991 0.283 1.558 0.142 0.991-0.567-0.425-0.708 4.249-2.974 10.339-4.957 10.056-3.399 12.889-3.682 28.751-3.258l17.562 0.425-4.249-4.391c-2.266-2.408-4.815-4.107-5.524-3.682-0.708 0.283-0.85-0.142-0.283-0.991 1.133-1.841-6.232-17.421-7.931-16.429-0.567 0.425-0.708-0.425-0.142-1.7 0.567-1.416 0.283-2.124-0.425-1.558-0.708 0.425-1.416-0.425-1.416-1.983 0-3.966 1.416-1.983 7.931 11.472 6.515 13.313 9.348 16.996 12.464 16.996 1.133 0 7.365 2.266 13.88 5.099 18.271 7.79 38.099 27.76 57.503 57.928 3.541 5.382 10.622 13.88 15.721 18.837 9.914 9.489 19.404 14.588 32.434 17.421 7.507 1.558 8.498 1.416 14.871-1.7 8.923-4.532 14.588-14.73 14.588-26.627 0-4.391-0.85-8.923-1.841-10.198-0.991-1.133-8.073-5.949-15.863-10.481-18.554-11.047-28.043-17.987-37.249-27.335-9.489-9.914-12.039-17.987-12.18-38.524-0.283-25.069 3.966-33.284 23.511-45.747 42.065-27.052 56.653-33.85 81.155-37.957 17.704-2.974 38.807-12.18 45.181-19.828 14.588-17.137 12.747-34.7-4.674-44.897-5.524-3.258-8.215-3.824-17.137-3.824-8.781 0-12.039 0.708-18.695 3.966-7.931 3.966-9.348 5.099-18.554 17.987-6.94 9.489-14.022 13.455-34.275 19.687-35.691 10.906-46.172 12.322-60.477 8.073-5.382-1.558-9.206-3.682-11.614-6.657-3.258-3.824-3.682-5.524-3.682-15.438 0-12.18 2.833-29.318 6.232-37.249 1.983-4.957 34.7-43.339 45.464-53.395 10.481-9.773 13.455-15.155 13.455-24.077 0-6.657-0.708-9.064-4.107-13.738-7.507-10.339-11.897-12.464-25.635-12.464-14.446 0-20.112 2.549-30.168 13.313-7.931 8.498-9.914 12.605-13.738 29.176-2.833 12.464-4.815 16.429-11.472 24.361-2.124 2.408-9.064 12.18-15.438 21.67-6.373 9.348-14.588 19.687-18.271 22.803-8.781 7.648-35.833 20.253-43.481 20.395-7.79 0-16.146-5.949-22.52-16.146-6.232-9.773-6.798-12.18-11.189-40.79-2.124-13.88-4.532-24.786-5.949-27.335-3.541-5.949-11.189-8.498-25.069-8.498-14.305 0-19.687 2.408-26.060 11.897-8.498 12.464-7.082 27.477 3.399 36.966 19.262 17.279 24.077 24.361 32.717 49.146 6.232 17.704 8.356 29.884 10.198 57.786 0.85 13.597 2.408 28.61 3.399 33.284 1.133 4.674 1.841 8.781 1.416 9.064-0.283 0.283-0.567-0.142-0.567-1.133s-0.567-1.416-1.275-0.991c-0.567 0.425-0.85-0.85-0.425-2.974s0.142-3.399-0.708-2.833c-0.708 0.425-1.133-0.85-0.708-3.399 0.283-2.266 0-4.107-0.708-4.107s-0.991-2.266-0.708-5.099c0.425-3.116 0.142-4.815-0.708-4.391-0.708 0.425-1.133-1.133-0.991-3.682 0.283-2.549 0.142-3.258-0.283-1.7-1.558 5.382-28.751 30.309-32.009 29.318-0.708-0.142-0.991 0.142-0.567 0.85 0.283 0.567-2.691 3.258-6.657 5.949-4.107 2.691-11.189 7.79-15.863 11.331-17.987 13.738-51.979 34.558-53.82 32.859-0.708-0.567-0.708-0.283-0.142 0.708s-0.425 1.983-2.266 2.691c-1.841 0.567-5.807 2.549-8.923 4.674-3.116 1.983-6.94 4.391-8.498 5.382s-5.665 3.399-9.206 5.524c-3.399 2.124-7.082 3.824-8.073 3.824s-1.558 0.425-1.133 0.708c1.983 1.983-22.661 9.914-33.992 11.047-6.515 0.708-11.047 0.142-19.12-2.124-14.588-4.249-18.554-5.949-17.562-7.365 0.425-0.708 0.283-1.275-0.425-1.133-7.223 1.7-31.017-22.661-33.284-34.133-0.283-1.133-0.991-2.549-1.7-2.974-0.708-0.567-1.275-2.833-1.416-5.24 0-2.408-0.708-3.966-1.416-3.541-0.567 0.425-0.991-2.124-0.708-5.665s0.142-6.090-0.425-5.665c-0.708 0.283-1.133-2.124-1.133-5.524 0-4.107 0.425-5.382 1.275-4.107 0.708 1.133 0.991-0.708 0.425-4.815-0.425-3.824-0.142-6.515 0.567-5.949 0.708 0.425 1.416-1.558 1.7-4.391 0.142-2.974 0.85-5.382 1.558-5.665s0.991-1.558 0.708-2.833c-0.283-1.275 1.983-6.515 5.099-11.614 11.331-18.695 15.155-28.326 16.571-40.648 0.708-6.373 0.708-11.189 0.142-10.906-0.991 0.567-1.558-5.24-1.558-15.296 0-1.416-0.567-2.124-1.416-1.558s-1.133-0.567-0.708-2.974c0.425-2.124 0.425-3.399 0-2.974s-2.266-3.258-4.249-8.073c-1.983-4.674-5.099-10.906-6.94-13.738-7.365-10.764-9.489-17.279-9.489-29.035-0.142-7.79 0.425-11.472 1.7-11.897 0.991-0.283 1.275-1.133 0.85-1.983-0.567-0.708-0.283-2.833 0.567-4.391 0.85-1.7 3.541-8.215 6.090-14.446 2.408-6.232 5.099-11.897 5.665-12.605 0.708-0.85 1.275-2.833 1.275-4.674 0-1.7 0.85-3.966 1.841-4.957 1.133-0.991 1.7-2.974 1.275-4.532-0.425-1.416-0.142-2.408 0.567-1.983s1.416-2.691 1.558-7.082c0.283-4.532 1.133-8.356 2.124-9.064 1.416-0.85 1.416-1.133 0.142-1.133s-1.841-2.266-1.841-6.798c0-21.245-15.155-45.039-35.408-55.237-1.983-0.991-5.807-2.974-8.498-4.532-2.691-1.416-9.773-3.116-15.58-3.682-7.79-0.708-9.914-1.275-7.79-2.124 2.124-0.991 1.841-1.133-1.416-0.567-5.382 0.991-20.112 2.549-25.069 2.691-2.266 0.142-3.966 0.567-3.966 1.133 0 0.708-0.85 1.133-1.841 1.133-1.133 0-3.824 0.991-6.090 2.124-2.408 1.133-11.047 3.682-19.404 5.807-16.854 4.249-15.58 4.391-30.026-4.532-3.824-2.408-9.348-5.099-12.039-6.232-2.691-0.991-5.524-2.408-6.232-3.116-0.85-0.567-2.974-1.133-4.815-1.133-2.124 0-2.974-0.567-2.266-1.7 0.567-0.85 0.425-1.133-0.425-0.708-0.85 0.567-5.807 0-11.189-1.133-9.489-1.983-15.296-2.266-23.936-1.133zM723.315 80.164c-0.425 0.425-1.7 0.567-2.691 0.142-1.133-0.425-0.708-0.85 0.85-0.85 1.558-0.142 2.408 0.283 1.841 0.708zM734.079 80.305c-1.841 0.283-4.674 0.283-6.373 0-1.841-0.283-0.425-0.567 3.116-0.567s4.957 0.283 3.258 0.567zM979.103 80.305c-1.841 0.283-4.674 0.283-6.373 0-1.841-0.283-0.425-0.567 3.116-0.567s4.957 0.283 3.258 0.567zM986.751 80.164c-0.425 0.425-1.7 0.567-2.691 0.142-1.133-0.425-0.708-0.85 0.85-0.85 1.558-0.142 2.408 0.283 1.841 0.708zM730.538 82.288c9.773 0.142 16.854 0.85 16.429 1.558s0.283 1.133 1.558 0.85c1.558-0.283 2.833 0.708 3.258 2.408 1.416 5.949 0.283 20.678-1.416 19.545-1.133-0.567-1.7 1.7-1.275 4.249 0.142 0.708-0.425 0.85-1.133 0.283-1.841-0.991-4.249 3.541-3.399 6.373 0.567 1.7 0.425 1.7-0.708 0.142-1.7-2.266-3.824 0.425-2.266 2.974 0.567 0.85 0.425 1.133-0.425 0.708-1.983-1.275-10.764 9.914-9.489 12.18 0.567 0.85 0.425 1.275-0.142 0.567-0.708-0.567-2.408 0-3.824 1.416-2.833 2.549-3.541 5.382-0.708 3.824 0.85-0.567 1.416-0.708 0.991-0.142-0.425 0.425-1.275 0.708-1.841 0.708-1.7 0-9.489 7.648-9.489 9.206 0 0.708 1.133 0.425 2.549-0.567 2.124-1.841 2.266-1.7 0.425 0.425-0.991 1.416-2.408 2.124-2.974 1.841-1.416-0.85-12.039 11.331-12.039 13.88-0.142 0.991-0.85 1.558-1.841 1.416-1.983-0.425-7.507 3.966-6.657 5.382 0.425 0.567-0.425 0.85-1.841 0.708-1.275 0-2.266 0.708-1.983 1.7 0.142 1.133-0.283 1.558-0.991 1.133s-3.966 0.142-7.223 1.275c-7.507 2.691-14.588 2.691-19.545 0.142-4.391-2.266-10.906-9.206-11.047-11.897-0.142-0.85-0.283-2.974-0.283-4.674-0.142-1.558-0.708-2.549-1.275-2.266-0.708 0.425-1.133-3.399-1.133-8.498-0.142-5.099 0.283-8.073 0.708-6.657 0.708 1.841 1.275 0.567 1.983-4.107 0.567-3.682 0.708-7.223 0.283-7.931s0.142-1.558 1.275-2.124c1.133-0.425 2.124-1.7 2.124-2.691 0-2.833 3.966-10.339 6.373-12.322 2.974-2.549 2.691-4.957-0.283-2.691-1.416 0.991-0.991 0.142 0.85-1.841 1.841-2.124 3.824-3.541 4.391-3.258 1.133 0.708 8.923-6.94 8.64-8.64-0.142-0.567 0.708-1.275 1.7-1.416 3.541-0.425 9.489-5.099 8.498-6.657-0.425-0.708-0.283-0.991 0.567-0.567 1.7 0.991 16.429-3.824 15.296-4.957-0.425-0.425 0.708-0.425 2.549-0.142 1.983 0.425 3.966 0 4.532-0.85 0.425-0.85 1.416-1.275 1.983-0.991 0.567 0.425 8.781 0.85 18.271 0.991zM1003.463 83.28c-0.425 0.425 2.833 2.124 7.082 3.966 4.391 1.7 9.631 4.249 11.755 5.665 2.124 1.558 4.674 2.691 5.524 2.691 0.991 0 1.841 0.708 1.983 1.416 0.142 2.408 2.974 4.957 4.249 4.107 0.708-0.425 1.133 0 0.991 1.133-0.283 1.133 0.567 1.7 2.124 1.416 1.275-0.283 2.549 0.283 2.549 1.133 0.425 2.691 0.708 3.541 2.408 6.798 0.991 1.7 2.549 2.691 3.399 2.266 0.85-0.567 1.133-0.425 0.567 0.567-0.991 1.558 3.258 11.897 4.674 11.047 0.425-0.283 0.991 1.133 0.991 3.116 0.142 2.124 0.708 5.382 1.275 7.365 1.416 5.099 1.983 28.468 0.567 27.477-0.708-0.283-1.558 0.85-2.124 2.833-0.425 1.841-1.983 4.532-3.541 5.949-1.416 1.275-2.408 2.833-1.983 3.258 0.283 0.567-0.567 0.991-1.841 1.133s-5.099 0.708-8.356 1.133c-3.966 0.708-7.931 0.283-12.464-1.275-3.682-1.133-7.365-1.841-7.931-1.416-0.708 0.425-1.133 0-0.991-0.991 0.283-0.85-0.425-1.983-1.558-2.408-1.275-0.425-1.558 0-0.85 1.133 0.708 1.275 0.567 1.558-0.567 0.85-0.85-0.567-1.275-1.558-0.85-2.266 0.991-1.558-3.116-4.674-4.674-3.682-1.416 0.85-5.524-3.258-4.815-4.674 0.708-1.558-11.047-14.305-12.889-13.88-0.991 0.142-1.558-0.567-1.275-1.558 0.425-2.124-3.682-6.657-5.949-6.657-0.85-0.142-1.416-0.85-1.275-1.841 0.567-2.408-3.966-7.365-5.665-6.373-0.85 0.567-0.991 0.142-0.425-0.85 1.416-2.124-3.258-7.507-5.24-6.232-0.85 0.425-0.991 0.142-0.425-0.85 1.416-2.124-1.841-6.090-3.824-4.815-0.85 0.425-0.991 0.142-0.425-0.708 0.991-1.558-8.215-16.288-9.348-15.155-0.283 0.283-0.708-3.116-0.85-7.507-0.283-5.807 0.283-9.064 1.983-10.906 1.133-1.558 1.841-1.841 1.275-0.708-0.567 1.275-0.142 1.275 0.991-0.283 1.558-2.266 4.391-2.408 24.077-2.266 12.322 0.142 22.095 0.567 21.67 0.85zM873.587 132.568c5.665 1.275 9.914 2.549 9.631 3.116s1.133 1.133 3.116 1.133c4.532 0.142 21.953 10.056 20.82 11.755-0.283 0.708 0.425 1.133 1.841 0.991 1.275 0 2.266 0.567 2.124 1.416-0.142 0.991 0.708 1.558 2.124 1.416 1.275 0 2.266 0.567 2.124 1.416-0.142 0.991 0.708 1.558 2.124 1.416 1.275 0 2.266 0.567 2.124 1.416-0.142 0.991 0.708 1.558 2.124 1.416 1.275 0 2.266 0.708 1.983 1.7-0.142 1.133 0.283 1.558 1.133 0.991 1.841-1.133 11.755 6.090 10.622 7.79-0.425 0.708 0 1.7 0.85 2.266 1.133 0.567 1.275 0.425 0.708-0.567-1.841-3.116 0.991-1.7 7.082 3.399 3.399 2.691 6.090 5.665 5.807 6.657-0.142 0.991 0.708 1.558 1.983 1.275 1.275-0.142 3.116 0.708 4.107 1.841s1.275 2.124 0.708 2.124c-0.567 0-0.283 0.991 0.708 2.124s2.408 1.7 3.116 1.275c1.558-0.991 8.498 4.957 7.507 6.515-0.425 0.567 0.425 1.983 1.7 2.974 1.7 1.416 1.983 1.416 1.275 0.142-2.408-3.966 1.841-1.275 7.648 4.957 3.541 3.682 4.815 5.524 2.833 3.966l-3.541-2.833 2.549 3.116c1.558 1.7 3.399 2.833 4.391 2.408 0.85-0.283 6.94 4.957 13.597 11.755s9.206 9.914 5.665 6.798l-6.373-5.665 5.382 5.807c2.974 3.258 6.373 5.807 7.365 5.665 0.991 0 9.489 7.365 18.979 16.571 16.288 16.004 19.404 19.404 15.438 17.137-1.133-0.567-1.416-0.425-0.708 0.283s1.983 1.275 2.833 1.275c2.691 0 2.833 4.957 0.142 7.931-3.966 4.391-12.747 3.541-21.811-1.983-4.249-2.549-7.648-5.524-7.365-6.515 0.142-0.991-0.283-1.416-1.133-0.85-1.841 1.133-12.747-7.365-12.322-9.631 0.142-0.991-0.567-1.558-1.7-1.275-2.549 0.567-8.781-5.382-7.648-7.223 0.425-0.85 0.142-0.991-0.85-0.425-1.558 0.991-15.013-8.923-13.597-10.198 1.133-1.133-2.266-4.532-3.824-3.682-1.558 0.991-5.665-2.124-4.674-3.682 0.425-0.708 0-1.7-0.85-2.266-1.133-0.567-1.275-0.425-0.708 0.567 1.841 2.974-1.416 1.841-4.532-1.558-1.7-1.841-2.691-3.824-2.124-4.532 0.708-0.567 0.283-0.708-0.708-0.142-1.983 1.133-20.537-13.030-19.404-14.73 0.425-0.567-0.425-0.991-1.7-0.85-1.416 0-2.549-0.567-2.549-1.558 0-0.85-0.567-2.124-1.275-2.833s-0.85-0.425-0.283 0.567c1.983 3.399-1.275 1.841-8.356-3.966-3.966-3.258-6.94-6.515-6.657-7.223 0.142-0.708-0.283-0.991-0.991-0.567-1.7 1.133-12.322-6.090-11.755-7.931 0.142-0.708-0.283-0.991-0.991-0.567s-6.232-1.7-12.464-4.815c-17.137-8.923-31.442-13.030-47.872-13.738-17.987-0.991-27.618 1.275-47.447 11.189-8.215 4.107-15.58 7.365-16.571 7.365-0.991-0.142-1.558 0.283-1.416 0.708 0.283 0.991-23.936 19.97-25.352 19.97-0.425 0-0.283-0.85 0.283-1.841 0.567-0.85 0.567-1.275 0-0.708-0.567 0.425-0.85 1.133-0.567 1.558 0.425 0.708-20.395 22.944-21.67 22.944-1.983 0-5.382 3.966-4.674 5.24 1.133 1.7-9.348 12.605-11.047 11.614-0.708-0.425-0.991 0-0.85 0.991 0.283 0.991-0.283 1.841-1.275 1.983-0.85 0-2.124 0.567-2.833 1.275s-0.425 0.85 0.708 0.283c0.85-0.567 1.7-0.425 1.7 0.142 0 1.841-22.095 23.228-23.511 22.803-0.708-0.283-1.275 0-1.275 0.567 0 2.124-9.914 11.047-11.614 10.481-0.991-0.425-1.7 0-1.416 0.991 0.142 0.991-0.85 1.558-2.124 1.558-1.416-0.142-2.124 0.283-1.7 0.991 0.991 1.7-7.931 6.94-12.039 6.94-8.781 0-10.906-8.498-5.099-20.962 3.824-8.215 18.695-24.786 20.962-23.369 0.708 0.425 0.991 0.283 0.567-0.567-1.133-1.841 15.013-17.704 17.562-17.137 1.133 0.283 1.841-0.283 1.7-1.133s12.322-13.455 27.618-28.185c15.296-14.73 26.627-25.211 25.069-23.369l-2.833 3.541 3.116-2.691c1.841-1.416 2.833-3.258 2.408-3.966-0.567-0.85 1.841-4.249 5.24-7.648 5.807-5.949 9.773-8.215 7.507-4.532-0.708 1.275-0.425 1.275 1.275-0.142 1.275-0.991 2.124-2.408 1.7-2.974-0.991-1.558 2.833-4.391 5.099-3.966 0.991 0.283 1.841-0.283 1.983-0.991 0.142-2.833 2.408-5.099 4.391-4.391 1.133 0.425 1.841 0 1.558-0.85-0.425-2.266 16.996-12.889 21.67-13.030 2.124-0.142 3.682-0.567 3.399-0.991-0.708-1.275 16.713-5.382 24.927-5.949 10.764-0.708 28.468 0.567 39.374 2.691zM649.666 152.538c-0.425 1.416-0.708 0.567-0.708-1.7 0-2.408 0.283-3.399 0.708-2.549 0.283 0.991 0.283 2.974 0 4.249zM885.201 181.147c7.365 2.549 15.721 5.949 18.412 7.648 12.747 7.648 29.884 22.236 55.237 47.022 29.035 28.468 43.481 40.932 53.395 45.889 8.498 4.249 20.395 8.498 27.618 9.914 5.524 1.133 5.665 1.275 1.416 1.416-2.549 0.142-9.064-1.416-14.73-3.399-18.837-6.515-30.876-15.155-56.653-40.507-49.288-48.58-59.485-56.653-82.572-65.434-27.477-10.339-63.026-8.215-89.936 5.382-10.906 5.524-28.468 18.837-40.79 30.876-12.605 12.464-24.927 28.043-41.073 52.121-7.507 11.331-11.614 16.004-16.854 19.404-9.773 6.232-21.953 9.773-36.541 10.764-15.863 0.991-14.163 0 2.974-1.841 14.022-1.558 27.052-5.949 35.125-12.18 2.833-2.124 9.773-10.906 15.58-19.687 17.279-25.635 19.545-28.61 33.284-43.339 20.112-21.811 38.524-34.841 60.335-42.631 16.288-5.949 22.661-6.798 43.906-6.373 16.429 0.425 19.828 0.85 31.867 4.957zM676.010 180.864c-0.991 0.283-2.974 0.283-4.249 0-1.416-0.425-0.567-0.708 1.7-0.708 2.408 0 3.399 0.283 2.549 0.708zM1035.755 180.864c-0.991 0.283-2.974 0.283-4.249 0-1.416-0.425-0.567-0.708 1.7-0.708 2.408 0 3.399 0.283 2.549 0.708zM1048.077 295.586c0 0.567-2.833 7.507-6.232 15.296-11.614 26.060-16.854 54.953-16.854 92.769-0.142 42.49 8.356 84.413 24.502 121.095 5.099 11.614 15.155 43.056 17.704 55.237 3.258 15.438 4.249 43.764 2.266 60.194-2.408 19.687-11.897 56.794-13.88 54.67-0.425-0.283 0.567-4.674 2.266-9.631 15.863-48.297 14.163-96.593-5.24-146.872-16.996-44.472-21.245-58.494-26.060-87.245-4.391-26.344-4.391-70.674 0-94.893 3.399-19.12 8.215-34.841 15.155-49.854 2.549-5.524 4.249-10.481 3.824-10.906s0-0.85 0.85-0.85c0.991 0 1.7 0.425 1.7 0.991zM649.666 305.359c-0.425 0.425 1.841 4.107 5.099 8.073 10.481 12.889 26.627 48.438 26.485 58.211-0.142 1.7-2.266-3.399-4.815-11.189-6.515-20.112-15.721-37.674-25.069-48.58-4.391-4.957-5.099-7.365-2.408-7.365 0.85 0 1.275 0.425 0.708 0.85zM685.358 401.81c1.558 17.562 0.142 47.73-2.691 58.494-0.85 3.399-0.991 1.275-0.283-6.373 1.275-14.73 1.275-53.395 0-67.983-0.708-7.931-0.708-9.914 0.283-6.373 0.708 2.691 1.983 12.747 2.691 22.236zM1308.539 390.763c4.957 2.691 11.897 1.7 18.554-2.691 3.541-2.266 7.082-4.249 7.79-4.249 0.85 0.142-0.425 1.133-2.549 2.266s-3.541 2.691-3.116 3.258c0.283 0.708-0.425 1.133-1.841 0.991-1.275 0-2.266 0.567-2.124 1.558 0.283 1.133-0.567 1.416-1.983 0.85-1.275-0.567-1.983-0.283-1.416 0.425 0.567 0.991-1.841 1.133-6.373 0.567-5.665-0.708-8.073-1.983-11.331-5.524-2.124-2.408-3.966-5.24-3.966-6.232 0-0.85 1.275 0.283 2.833 2.833 1.416 2.408 3.966 5.24 5.524 5.949zM1345.363 384.389c2.124 1.133 4.815 3.258 5.949 4.674 1.841 2.408 1.7 2.408-1.133 0-1.7-1.558-3.824-2.266-4.815-1.558-1.133 0.708-1.416 0.425-0.567-0.85 0.85-1.416 0.567-1.7-1.275-0.991-1.275 0.567-1.983 0.425-1.558-0.283s-0.708-1.7-2.408-1.983c-2.266-0.425-2.549-0.708-0.708-0.85 1.416 0 4.249 0.708 6.515 1.841zM394.729 396.003c-0.425 0.425-1.7 0.567-2.691 0.142-1.133-0.425-0.708-0.85 0.85-0.85 1.558-0.142 2.408 0.283 1.841 0.708zM351.814 400.111c0.425 1.133 0 1.983-0.991 1.7-0.991-0.142-1.7 1.133-1.558 2.833 0 1.7-0.567 2.691-1.275 2.266-0.85-0.567-1.133 0-0.708 1.275 0.567 1.275 0 2.124-1.133 2.124-2.549 0-2.549-0.283 1.133-6.798 3.541-6.373 3.399-6.232 4.532-3.399zM680.825 471.918c-1.558 11.189-10.764 36.399-22.095 61.043-6.232 13.455-12.464 28.61-14.022 33.708-10.481 34.275-6.94 72.516 11.047 120.529 8.781 23.511 20.537 45.464 36.399 68.408 8.64 12.464 8.073 12.18-9.914-4.957-8.356-7.79-16.004-17.137-12.322-14.871 1.841 1.133 9.064 8.923 12.322 13.313 0.85 1.275 1.983 1.841 2.549 1.416 0.425-0.567-2.833-7.223-7.365-14.871-11.755-19.828-16.146-28.751-22.944-46.88-12.605-33.708-17.137-55.095-17.137-81.863 0.142-24.219 4.815-44.189 15.721-66.001 12.322-24.786 26.768-63.734 26.768-72.374 0-1.416 0.425-2.549 0.85-2.549 0.567 0 0.567 2.691 0.142 5.949zM1068.189 692.864c-0.991 0.991-1.7 1.133-1.7 0.283 0-1.983 1.7-3.682 2.691-2.691 0.283 0.425-0.142 1.558-0.991 2.408zM1059.691 709.152c-3.966 6.798-8.215 13.738-9.489 15.296-1.558 1.841-1.416 0.991 0.425-2.124 2.549-4.391 6.94-12.605 12.889-23.794 0.991-1.841 2.124-2.974 2.549-2.549 0.425 0.283-2.549 6.232-6.373 13.172zM1054.451 701.362c-0.567 2.549-4.107 11.189-7.79 19.12-3.824 8.073-6.515 14.588-5.949 14.588 0.425 0 2.408-1.7 4.249-3.966 2.266-2.691 2.124-1.983-0.85 2.549-4.815 7.365-11.472 14.73-12.464 13.88-0.283-0.425 3.824-9.348 9.348-19.97 5.382-10.764 10.622-21.953 11.472-25.069 2.124-6.798 3.258-7.507 1.983-1.133zM665.671 732.804c0 0.85-1.558-0.708-3.541-3.399s-3.541-5.099-3.541-5.524c0-1.416 6.94 7.507 7.082 8.923zM714.534 84.554c-8.64 1.133-19.262 4.815-26.627 9.064-9.206 5.382-21.386 18.129-25.919 27.052-10.481 20.678-9.773 45.464 1.416 52.97 5.099 3.258 18.554 1.983 25.919-2.408 7.79-4.532 18.979-15.58 32.009-31.726 6.232-7.79 11.897-14.446 12.464-14.871s4.532-6.090 8.923-12.747c5.524-8.356 7.931-13.313 7.931-16.571 0-5.099-3.966-9.631-9.064-10.622-4.391-0.85-21.67-0.991-27.052-0.142zM740.028 89.087c3.541 0.708 4.391 1.7 4.957 5.807 0.991 5.807 0.142 10.198-1.558 9.064-0.708-0.425-1.275 0-1.275 0.708 0 2.974-0.425 3.682-2.833 5.382-1.275 0.991-4.815 5.24-7.79 9.489-2.974 4.391-5.949 7.931-6.657 7.931-0.567 0-1.133 0.708-1.133 1.558 0 0.708 0.85 0.991 1.841 0.567 0.85-0.567 0.142 0.425-1.841 2.266-1.983 1.7-3.399 3.682-3.116 4.249 0.142 0.708-0.567 1.133-1.416 0.85-0.991-0.142-1.7 0.567-1.416 1.416 0.142 0.991-0.567 1.7-1.416 1.416-0.991-0.142-1.7 0.567-1.416 1.416 0.142 0.991-0.708 1.841-2.124 1.841-1.275 0-1.983 0.425-1.416 0.85 0.85 0.85-0.567 2.691-9.206 11.755-2.833 2.974-5.665 5.099-6.090 4.674s-0.708 0.283-0.708 1.7c0 1.275-0.567 2.124-1.275 1.558-0.85-0.425-1.983 0.425-2.833 1.7-0.85 1.416-1.558 2.124-1.558 1.416 0-0.567-2.124 0-4.532 1.275-2.549 1.275-6.515 2.408-8.781 2.266-2.266 0-5.24 0.425-6.515 1.133-4.532 2.408-11.755-10.198-11.614-20.537 0.142-7.507 2.549-18.837 4.674-21.386 1.558-1.983 1.558-2.266 0-1.416-1.133 0.85-0.991 0 0.567-2.549 1.416-1.983 1.841-3.682 1.275-3.682-0.708-0.142 0.283-1.133 2.266-2.266 2.549-1.558 2.833-1.983 0.708-1.416l-2.833 0.85 2.833-2.266c1.558-1.133 2.691-2.974 2.408-3.824-0.283-0.991 0-1.275 0.567-0.567 1.416 1.416 6.373-3.258 5.24-5.099-0.425-0.708-0.142-0.991 0.708-0.425 0.85 0.425 3.541-1.558 5.949-4.391 2.549-2.974 4.107-4.391 3.541-3.258-0.425 1.133 0.283 0.708 1.841-0.991s3.116-2.691 3.682-2.266c0.425 0.567 0.85 0 0.85-0.991s0.425-1.416 0.991-0.991c0.425 0.567 2.408 0 4.391-1.275 2.549-1.7 3.258-1.7 2.408-0.425-0.425 1.133-0.142 0.991 0.991-0.283s4.674-2.266 8.923-2.549c3.824-0.142 7.223-0.567 7.648-0.991 0.85-0.85 17.137-0.425 22.095 0.708zM963.381 85.546c-2.549 0.85-5.24 2.549-5.949 3.682-2.266 3.682-1.558 10.339 1.983 16.429 2.691 4.674 22.095 30.593 24.927 33.142 0.425 0.425 2.549 2.974 4.815 5.665 2.124 2.691 8.356 9.631 13.88 15.155 10.764 11.047 20.253 16.004 30.451 16.004 6.373 0 13.88-4.815 16.288-10.622 2.549-5.949 2.124-23.936-0.85-33.992-5.665-19.545-24.786-37.816-46.030-44.048-9.773-2.974-33.142-3.824-39.515-1.416zM1001.339 89.511c1.133 0.142 2.833 0.991 3.541 2.124 0.85 0.991 3.116 1.983 5.382 2.124s3.824 1.133 3.966 2.549c0 1.275 0.425 1.416 0.708 0.425 0.425-0.991 3.682 1.275 7.931 5.24 3.966 3.824 7.931 6.515 8.64 6.090 0.85-0.425 0.991-0.283 0.567 0.567-0.567 0.708 1.275 3.399 3.966 5.807 2.974 2.549 4.674 5.099 4.107 6.373-0.425 1.133 0.283 2.408 1.558 2.974 1.275 0.425 1.841 1.416 1.416 2.266-0.425 0.708-0.142 1.416 0.708 1.416 0.991 0 1.416 0.991 0.991 2.124-0.567 1.133-0.283 2.408 0.283 2.833 0.708 0.425 0.991 2.124 0.425 3.824-0.567 2.266-0.283 2.691 1.133 1.841 1.275-0.708 1.558-0.567 0.85 0.425-0.567 0.991-0.567 2.408 0 3.399 1.558 2.408 1.558 16.713 0 16.713-0.567 0-0.85 0.991-0.283 2.124 0.425 1.133 0.142 2.124-0.708 2.124s-1.133 0.567-0.708 1.275c0.425 0.708-0.142 1.7-1.416 2.124-1.133 0.425-1.7 1.558-1.133 2.408 0.567 1.133 0.425 1.275-0.567 0.708s-1.7-0.567-1.7 0.142c0 0.567-3.824 1.133-8.498 1.133s-8.498-0.425-8.498-0.85c0-0.708-4.957-2.691-6.798-2.549-0.142 0-4.391-3.966-9.206-8.923-4.815-4.815-9.348-8.64-9.914-8.498-0.708 0.283-0.85-0.142-0.567-0.708 0.425-0.708-1.558-3.682-4.249-6.798-2.833-3.116-4.249-4.391-3.116-2.833 1.841 2.549 1.841 2.691-0.425 0.85-1.416-0.991-2.266-2.408-1.841-2.974 0.425-0.708 0.142-1.558-0.708-2.124-0.85-0.425-4.957-5.807-9.348-11.755s-9.631-12.464-11.614-14.305c-2.124-1.841-2.833-2.833-1.7-2.266 1.416 0.567 1.275 0.142-0.283-1.133-1.416-1.133-2.124-2.408-1.558-2.833 0.425-0.567 0.142-0.991-0.85-0.991s-1.275-0.708-0.85-1.416c0.425-0.85 0.283-1.416-0.567-1.416s-0.85-1.558-0.142-4.107c0.567-2.266 0.85-4.532 0.567-4.957s-0.142-0.567 0.283-0.283c0.425 0.283 2.408 0 4.532-0.708s10.481-0.85 18.695-0.425c8.215 0.425 15.863 0.85 16.996 0.85zM862.256 132.709c0.991 0.425 2.266 0.283 2.691-0.142 0.567-0.425-0.283-0.85-1.841-0.708-1.558 0-1.983 0.425-0.85 0.85zM739.178 196.444c-1.841 2.266-1.7 2.408 0.567 0.567 1.275-0.991 2.408-2.124 2.408-2.408 0-1.133-1.133-0.425-2.974 1.841zM657.315 286.947c0 1.558 0.425 1.983 0.85 0.85 0.425-0.991 0.283-2.266-0.142-2.691-0.425-0.567-0.85 0.283-0.708 1.841zM328.303 427.304c0.991 0.425 2.266 0.283 2.691-0.142 0.567-0.425-0.283-0.85-1.841-0.708-1.558 0-1.983 0.425-0.85 0.85zM1376.381 427.304c0.991 0.425 2.266 0.283 2.691-0.142 0.567-0.425-0.283-0.85-1.841-0.708-1.558 0-1.983 0.425-0.85 0.85zM1162.799 702.070c-4.249 0.85-6.798 2.549-15.296 10.622-6.798 6.515-44.472 39.799-49.288 43.623-1.558 1.133-7.082 5.949-12.605 10.481-10.339 8.781-16.288 12.889-38.099 26.768-14.022 8.923-49.005 26.485-53.254 26.91-1.416 0.142-2.266 0.708-1.841 1.558 0.567 0.708-0.425 0.991-2.124 0.567-1.7-0.567-2.833-0.425-2.549 0.142 0.425 0.567-2.549 1.841-6.657 2.974-4.107 0.991-7.931 2.266-8.781 2.833-1.841 1.275-27.902 7.648-41.073 9.914-5.382 0.991-17.846 3.399-27.618 5.24-15.296 3.116-21.811 3.541-48.863 3.682-35.125 0.283-50.138-1.558-77.898-9.631-3.824-1.133-12.18-3.399-18.412-5.099-17.846-4.674-30.309-8.781-29.884-9.631 0.283-0.425-0.85-0.991-2.549-0.991-1.7-0.142-6.090-1.558-9.631-3.258-3.682-1.558-7.223-2.974-7.931-2.974-1.133 0-7.931-2.974-31.017-13.597-2.549-1.133-4.674-2.833-4.674-3.682s-0.425-1.133-0.991-0.708c-0.991 1.133-12.464-5.099-25.352-13.738-13.313-8.923-41.357-32.15-75.348-62.318-2.691-2.408-3.682-2.549-6.090-1.133-1.558 0.991-3.541 1.416-4.391 0.85s-1.133-0.425-0.708 0.425c1.133 1.841-2.833 3.682-4.957 2.266-0.85-0.425-1.133-0.142-0.567 0.708 0.425 0.85-0.567 1.841-2.266 2.408-8.498 2.408-31.726 14.163-40.79 20.537-1.983 1.416-8.923 5.665-15.58 9.631-26.768 16.004-52.829 42.348-57.078 57.928-0.85 3.258-1.841 5.665-2.408 5.382-1.133-0.708 0.85 23.511 2.266 26.91 1.7 4.249 8.64 9.631 15.58 12.18 3.399 1.133 5.949 2.549 5.524 2.974s0.85 0.425 2.833 0.142c1.983-0.425 3.541-0.142 3.541 0.567s1.983 0.991 4.391 0.708c2.549-0.425 4.107-0.142 3.682 0.708-0.425 0.708 1.416 0.991 4.249 0.708 2.833-0.425 4.674-0.142 4.249 0.567-0.283 0.708 4.532 1.416 10.906 1.7 6.232 0.142 16.854 1.275 23.511 2.408 6.657 0.991 15.863 2.408 20.537 2.974s8.923 1.275 9.631 1.416c0.567 0.142 2.974 0.283 5.382 0.425 2.266 0 3.966 0.708 3.541 1.275-0.425 0.708 1.275 0.85 3.682 0.567 3.258-0.567 4.107-0.283 3.258 1.133-0.708 1.275-0.567 1.558 0.567 0.85 2.408-1.558 10.339 0.425 9.206 2.266-0.567 0.991-0.283 1.133 0.567 0.567 2.408-1.416 11.755 0.567 10.622 2.408-0.425 0.85-0.142 0.991 0.708 0.425 2.124-1.275 41.215 11.897 55.52 18.554 23.794 11.189 42.631 21.103 41.781 21.953-0.425 0.425 0.142 0.708 1.133 0.708s6.373 2.833 12.039 6.515c5.524 3.541 11.897 7.507 14.022 8.498 4.249 2.266 21.245 5.24 29.318 5.24 3.116 0 5.099 0.708 5.099 1.7 0 1.133 0.425 1.275 1.133 0.425 0.567-0.85 11.472-1.416 27.193-1.558 30.168-0.142 56.653-2.124 97.726-7.082 16.004-1.983 30.451-3.258 32.15-2.974 1.841 0.283 2.833 0 2.266-0.85s1.275-1.133 5.24-0.567c3.399 0.425 5.382 0.283 4.391-0.283-2.124-1.416 11.755-3.258 16.854-2.266 2.974 0.708 3.116 0.567 0.708-0.425-2.124-0.991-0.708-1.558 5.807-2.266 4.674-0.567 9.773-0.567 11.331 0 2.266 0.708 2.408 0.567 0.567-0.85-1.7-1.275-1.133-1.558 3.116-1.133 2.833 0.283 4.957 0.142 4.532-0.425-0.85-1.558 14.446-3.258 16.571-1.983 0.991 0.708 1.416 0.567 0.85-0.283s3.966-2.124 11.472-3.116c13.455-1.841 25.352-3.966 44.897-8.356 29.035-6.515 33.992-7.365 35.833-6.657 1.133 0.425 1.416 0 0.85-0.85-0.708-1.133 0.425-1.416 3.399-0.991 2.408 0.283 4.107 0.142 3.824-0.425-0.708-0.85 2.549-1.841 29.035-7.648 5.807-1.275 18.554-4.532 28.185-7.223 9.631-2.549 17.987-4.391 18.695-4.107 0.567 0.425 1.275 0 1.558-0.85 0.283-0.991 2.549-1.7 4.957-1.841 2.408 0 4.107-0.425 3.824-0.708-0.991-0.991 15.155-5.949 16.429-5.099 0.85 0.567 1.416 0.142 1.416-0.567 0-0.85 0.991-1.558 2.124-1.558s9.489-2.549 18.695-5.807c9.206-3.116 18.979-6.373 21.67-7.223 2.691-0.708 5.665-1.983 6.657-2.833 0.991-0.708 2.549-0.85 3.399-0.283 0.991 0.567 1.275 0.425 0.85-0.425-0.85-1.416 6.515-3.966 9.348-3.399 0.708 0.142 0.85-0.283 0.425-0.991s2.549-2.266 6.657-3.682c8.781-3.116 20.537-8.781 29.318-14.305 3.541-2.266 7.223-3.966 8.073-3.966 0.991 0 1.841-0.567 1.841-1.133 0-0.708 3.399-3.399 7.507-5.949 8.073-5.382 14.163-11.472 19.97-20.112 3.966-5.949 4.957-15.58 2.266-22.52-2.974-7.931-21.103-23.653-32.15-27.902-2.124-0.85-4.957-2.266-6.090-3.258-3.966-3.541-35.125-18.695-45.606-22.236-2.833-0.991-4.815-2.408-4.391-3.116 0.425-0.85 0.142-0.991-0.708-0.567-1.7 1.133-6.232-0.283-5.382-1.7 0.425-0.567-0.567-0.991-2.124-1.133-1.416-0.142-6.373-1.841-10.764-3.824-10.764-4.532-30.734-11.614-38.382-13.597-3.258-0.85-5.665-2.124-5.24-2.833 0.567-0.85 0.708-1.275 0.425-1.275-0.283 0.142-1.983 0.567-3.966 0.85z\"\n      ],\n      \"width\": 1700,\n      \"tags\": [\n        \"freepbx\"\n      ],\n      \"defaultCode\": 59654,\n      \"grid\": 20\n    },\n    {\n      \"paths\": [\n        \"M877.529 75.18c-59.625 59.625-60.922 62.218-53.144 125.732 5.185 36.294 2.592 86.846-5.185 114.066-7.777 25.924-9.073 47.959-3.889 47.959 27.22 0 129.62-130.916 143.878-180.172 16.851-63.514 12.962-159.433-6.481-165.914-7.777-2.592-41.478 23.332-75.18 58.329zM715.504 138.694c-9.073 28.516-15.554 62.218-15.554 72.587 0 25.924 53.144 138.694 66.106 138.694 15.554 0 37.59-77.772 37.59-130.916 0-36.294-10.37-60.922-36.294-90.734l-36.294-40.182-15.554 50.552zM67.403 352.567l3.889 42.775h107.585c58.329 0 106.289 1.296 106.289 3.889s-58.329 99.808-129.62 216.466c-71.291 116.658-129.62 219.058-129.62 226.835 0 9.073 60.922 12.962 198.319 10.37l197.023-3.889v-77.772l-248.871-12.962 133.509-217.762c73.884-119.251 134.805-220.354 134.805-222.947 0-3.889-85.549-6.481-189.246-6.481h-187.949l3.889 41.478zM581.995 399.23c-149.063 77.772-174.987 278.684-49.256 383.676 55.737 46.663 94.623 60.922 168.506 62.218 93.327 0 163.322-46.663 211.281-141.286 27.22-54.441 27.22-55.737-15.554-55.737-25.924 0-41.478 10.37-59.625 40.182-27.22 45.367-92.030 89.438-133.509 89.438-16.851 0-29.813-2.592-29.813-6.481s29.813-85.549 64.81-182.765c36.294-97.215 64.81-180.172 64.81-186.653 0-14.258-73.884-38.886-116.658-38.886-19.443 0-67.403 15.554-104.992 36.294zM705.134 469.225c-5.185 16.851-31.109 86.846-57.033 156.841l-47.959 128.324-33.701-40.182c-18.147-23.332-37.59-60.922-42.775-84.253-15.554-88.142 66.106-189.246 154.248-189.246 32.405 0 34.997 3.889 27.22 28.516zM976.041 372.010c-12.962 3.889-16.851 64.81-16.851 238.501v232.020l31.109 7.777c18.147 3.889 38.886 3.889 45.367-1.296 9.073-5.185 14.258-68.699 14.258-164.618v-155.544l44.071-44.071c58.329-58.329 119.251-60.922 178.876-6.481l42.775 37.59 6.481 167.21 6.481 165.914h64.81l3.889-169.803c2.592-158.137 1.296-171.099-25.924-212.577-64.81-95.919-187.949-133.509-282.572-84.253-32.405 16.851-37.59 16.851-44.071 0-6.481-19.443-37.59-23.332-68.699-10.37zM738.835 964.375c0 32.405 6.481 59.625 12.962 59.625 15.554 0 12.962-102.4-2.592-111.473-5.185-3.889-10.37 19.443-10.37 51.848zM1075.848 964.375c0 32.405 6.481 59.625 12.962 59.625 15.554 0 12.962-102.4-2.592-111.473-5.185-3.889-10.37 19.443-10.37 51.848zM995.484 935.858c-9.073 27.22 5.185 88.142 20.739 88.142 20.739 0 20.739-88.142 0-95.919-9.073-3.889-18.147 0-20.739 7.777zM16.851 942.339c-9.073 2.592-16.851 23.332-16.851 44.071 0 31.109 5.185 37.59 32.405 37.59 18.147 0 32.405-6.481 32.405-12.962 0-7.777-9.073-12.962-19.443-12.962s-19.443-9.073-19.443-19.443c0-10.37 9.073-19.443 19.443-19.443s19.443-6.481 19.443-12.962c0-12.962-20.739-15.554-47.959-3.889zM103.696 952.709c-22.035 25.924-10.37 63.514 20.739 68.699 38.886 5.185 62.218-27.22 44.071-60.922-16.851-32.405-42.775-34.997-64.81-7.777zM137.397 994.187c-12.962 12.962-24.628-6.481-12.962-23.332 7.777-14.258 10.37-14.258 15.554 0 2.592 9.073 1.296 19.443-2.592 23.332zM224.243 942.339c-19.443 6.481-23.332 81.661-5.185 81.661 6.481 0 15.554-12.962 18.147-29.813l5.185-28.516 2.592 28.516c2.592 42.775 37.59 37.59 44.071-5.185l5.185-36.294 2.592 34.997c0 20.739 7.777 36.294 15.554 36.294s11.666-18.147 9.073-42.775c-3.889-36.294-9.073-41.478-42.775-42.775-20.739-1.296-46.663 1.296-54.441 3.889zM370.714 942.339c-12.962 12.962-9.073 81.661 3.889 81.661 6.481 0 15.554-15.554 18.147-34.997l5.185-36.294 2.592 34.997c1.296 44.071 36.294 49.256 44.071 6.481l5.185-28.516 2.592 28.516c0 16.851 7.777 29.813 14.258 29.813 18.147 0 15.554-59.625-2.592-73.884-19.443-14.258-80.365-19.443-93.327-7.777zM639.028 942.339c-19.443 7.777-23.332 81.661-3.889 81.661 7.777 0 12.962-11.666 12.962-25.924s5.185-25.924 11.666-25.924c6.481 0 9.073 11.666 5.185 25.924-5.185 18.147 0 25.924 14.258 25.924 24.628 0 28.516-51.848 5.185-75.18-16.851-16.851-20.739-16.851-45.367-6.481zM803.646 952.709c-22.035 27.22-10.37 64.81 23.332 68.699 15.554 2.592 28.516-1.296 28.516-9.073s-9.073-14.258-19.443-14.258c-28.516 0-23.332-27.22 6.481-34.997 14.258-3.889 22.035-11.666 18.147-18.147-10.37-18.147-38.886-14.258-57.033 7.777zM902.157 944.932c-5.185 6.481 0 15.554 7.777 18.147 12.962 5.185 12.962 7.777 1.296 7.777-9.073 1.296-16.851 12.962-16.851 27.22 0 19.443 9.073 25.924 32.405 25.924 33.701 0 41.478-24.628 24.628-69.995-9.073-22.035-37.59-27.22-49.256-9.073zM1140.658 952.709c-23.332 28.516-10.37 64.81 24.628 64.81 41.478 0 63.514-36.294 41.478-63.514-22.035-25.924-45.367-25.924-66.106-1.296zM1174.359 994.187c-12.962 12.962-24.628-6.481-12.962-23.332 7.777-14.258 10.37-14.258 15.554 0 2.592 9.073 1.296 19.443-2.592 23.332zM1261.205 942.339c-19.443 6.481-23.332 81.661-5.185 81.661 6.481 0 15.554-12.962 18.147-29.813l5.185-28.516 2.592 28.516c1.296 40.182 40.182 38.886 40.182 0 0-16.851-6.481-36.294-15.554-45.367-16.851-16.851-20.739-16.851-45.367-6.481zM1363.605 948.82c-10.37 10.37-11.666 19.443-1.296 32.405 10.37 11.666 10.37 16.851 0 16.851-7.777 0-14.258 5.185-14.258 12.962 0 6.481 14.258 12.962 32.405 12.962 23.332 0 32.405-6.481 32.405-24.628 0-14.258-7.777-28.516-15.554-31.109-12.962-5.185-12.962-7.777 0-7.777 9.073-1.296 12.962-7.777 9.073-14.258-10.37-16.851-23.332-16.851-42.775 2.592zM505.519 964.375c0 38.886 16.851 59.625 46.663 59.625 25.924 0 31.109-6.481 31.109-38.886 0-45.367-22.035-51.848-29.813-10.37l-5.185 29.813-2.592-29.813c-1.296-31.109-40.182-41.478-40.182-10.37z\"\n      ],\n      \"width\": 1413,\n      \"tags\": [\n        \"zencommunication\"\n      ],\n      \"defaultCode\": 59655,\n      \"grid\": 20\n    },\n    {\n      \"paths\": [\n        \"M697.217 202.5l-185.217 185.821-97.737-98.341-98.341-97.737h146.606l2.413-30.166 1.81-30.166h-250.375v250.375l57.315-1.207 1.81-74.811 1.81-74.207 234.689 234.689 412.666-412.666-17.496-18.703c-9.653-9.653-18.703-18.1-20.513-18.1s-86.877 83.258-189.441 185.217z\",\n        \"M436.586 518.034c-127.903 14.479-246.152 60.935-349.922 136.349-56.711 41.628-80.241 64.555-85.068 82.654-3.62 15.686 1.81 22.926 65.158 86.273 42.835 42.835 73.604 68.778 80.844 68.778s29.562-13.273 50.075-30.166c20.513-16.29 50.075-37.406 65.761-46.455 51.282-28.959 49.472-25.339 49.472-108.597v-74.811l19.91-8.446c75.414-31.372 243.739-32.579 355.352-3.017l22.926 6.637v74.811c0 84.464 1.207 87.48 47.662 111.613 13.273 6.637 38.613 24.132 56.711 38.613 42.232 34.389 55.504 41.025 73.001 36.199 7.843-1.81 41.628-31.372 75.414-65.158 70.587-72.397 74.207-83.258 39.215-115.837-93.514-88.687-219.606-152.638-352.335-179.787-57.315-11.463-159.879-16.29-214.176-9.653z\"\n      ],\n      \"tags\": [\n        \"call_failfwd\"\n      ],\n      \"defaultCode\": 59656,\n      \"grid\": 20\n    },\n    {\n      \"paths\": [\n        \"M807.374 108.322v75.484h-203.479v210.7l203.479-3.938v157.533l127.995-127.995c70.234-70.234 127.995-130.62 127.995-133.247 0-6.563-244.176-254.022-250.739-254.022-3.283 0-5.252 34.131-5.252 75.484z\",\n        \"M54.499 148.361c-16.409 17.067-17.067 18.378-13.128 86.643 11.815 213.325 101.741 414.179 254.677 565.805 154.908 154.25 351.166 241.551 568.432 254.022 64.982 3.938 66.951 3.283 83.36-13.128 17.067-17.067 17.067-17.722 17.067-125.37 0-139.155 1.313-137.185-98.458-147.687-38.070-3.938-85.987-11.159-106.99-17.067-21.005-5.252-44.635-9.846-52.512-9.846-9.19 0-36.758 22.317-78.111 63.669l-64.327 63.669-21.005-11.815c-129.965-74.172-230.392-174.599-304.564-303.906l-11.815-21.661 63.669-64.327c68.921-70.89 70.234-73.515 53.823-128.651-5.252-17.722-13.128-64.982-17.067-105.021-11.159-104.365-8.532-102.396-147.687-102.396-107.648 0-108.304 0-125.37 17.067z\"\n      ],\n      \"tags\": [\n        \"call_hardfwd\"\n      ],\n      \"defaultCode\": 59657,\n      \"grid\": 20\n    },\n    {\n      \"paths\": [\n        \"M128 0h768c70.692 0 128 57.308 128 128v768c0 70.692-57.308 128-128 128h-768c-70.692 0-128-57.308-128-128v-768c0-70.692 57.308-128 128-128z\",\n        \"M494.413 102.4l-366.413 432.358 194.854 397.030 133.248-159.411-25.28-51.494-96.448 115.379-144.192-293.798 290.765-343.078 49.485 108.749 37.478-44.224zM597.018 327.91l-37.466 44.211 57.37 126.118-141.478 169.254 25.28 51.507 177.587-212.442zM712.115 102.4l-366.426 432.358 194.854 397.030 355.456-425.242zM698.637 199.386l135.987 298.854-282.56 338.022-144.192-293.798 290.765-343.078z\"\n      ],\n      \"attrs\": [\n        {\n          \"fill\": \"rgb(0, 78, 168)\"\n        },\n        {\n          \"fill\": \"rgb(255, 255, 255)\"\n        }\n      ],\n      \"isMulticolor\": true,\n      \"colorPermutations\": {\n        \"2552552551781681\": [\n          {\n            \"f\": 0\n          },\n          {\n            \"f\": 1\n          }\n        ]\n      },\n      \"tags\": [\n        \"transnexus\"\n      ],\n      \"defaultCode\": 59658,\n      \"grid\": 20\n    },\n    {\n      \"paths\": [\n        \"M0 512c0 282.77 229.23 512 512 512s512-229.23 512-512-229.23-512-512-512-512 229.23-512 512zM928 512c0 229.75-186.25 416-416 416s-416-186.25-416-416 186.25-416 416-416 416 186.25 416 416zM706.744 669.256l90.512-90.512-285.256-285.254-285.254 285.256 90.508 90.508 194.746-194.744z\"\n      ],\n      \"tags\": [\n        \"circle-up\"\n      ],\n      \"defaultCode\": 59969,\n      \"grid\": 20\n    },\n    {\n      \"paths\": [\n        \"M1024 512c0-282.77-229.23-512-512-512s-512 229.23-512 512 229.23 512 512 512 512-229.23 512-512zM96 512c0-229.75 186.25-416 416-416s416 186.25 416 416-186.25 416-416 416-416-186.25-416-416zM317.256 354.744l-90.512 90.512 285.256 285.254 285.254-285.256-90.508-90.508-194.746 194.744z\"\n      ],\n      \"tags\": [\n        \"circle-down\"\n      ],\n      \"defaultCode\": 59971,\n      \"grid\": 20\n    }\n  ],\n  \"colorThemes\": [\n    [\n      [\n        0,\n        78,\n        168,\n        1\n      ],\n      [\n        255,\n        255,\n        255,\n        1\n      ]\n    ]\n  ],\n  \"colorThemeIdx\": 0,\n  \"preferences\": {\n    \"showGlyphs\": true,\n    \"showQuickUse\": true,\n    \"showQuickUse2\": true,\n    \"showSVGs\": true,\n    \"fontPref\": {\n      \"prefix\": \"icon-\",\n      \"metadata\": {\n        \"fontFamily\": \"icomoon\"\n      },\n      \"metrics\": {\n        \"emSize\": 1024,\n        \"baseline\": 6.25,\n        \"whitespace\": 50\n      },\n      \"embed\": false\n    },\n    \"imagePref\": {\n      \"prefix\": \"icon-\",\n      \"png\": true,\n      \"useClassSelector\": true,\n      \"color\": 0,\n      \"bgColor\": 16777215,\n      \"classSelector\": \".icon\"\n    },\n    \"historySize\": 50,\n    \"showCodes\": true,\n    \"gridSize\": 16\n  },\n  \"IcoMoonType\": \"icon-set\"\n}"
  },
  {
    "path": "gui/static/js/backupandrestore.js",
    "content": ";(function(window, document) {\n  'use strict';\n\n  // throw an error if required functions not defined\n  if (typeof showNotification === \"undefined\") {\n    throw new Error(\"showNotification() is required and is not defined\");\n  }\n  if (typeof reloadKamRequired === \"undefined\") {\n    throw new Error(\"reloadKamRequired() is required and is not defined\");\n  }\n\n  // throw an error if required globals not defined\n  if (typeof API_BASE_URL === \"undefined\") {\n    throw new Error(\"API_BASE_URL is required and is not defined\");\n  }\n\n  /* global variables */\n  var loading_spinner = $('#reloading_overlay');\n\n  /**\n   * Show a spinner while loading\n   * @param isLoading {boolean}\n   */\n  function changeLoadingState(isLoading) {\n    if (isLoading) {\n      loading_spinner.removeClass('hidden');\n    }\n    else {\n      loading_spinner.addClass('hidden');\n    }\n  }\n\n  function downloadResponse(response, filename) {\n    var a = document.createElement(\"a\");\n    var fp = new Blob([response], {type: 'text/plain'});\n    var url = window.URL.createObjectURL(fp);\n    a.href = url;\n    a.download = filename;\n    document.body.appendChild(a);\n    a.click();\n    window.URL.revokeObjectURL(url);\n    document.body.removeChild(a);\n  }\n\n  /**\n   * Get the current date / time formatted as \"YYYYmmdd-HHMM\"\n   * @returns {string}\n   */\n  function getFormattedDateTime() {\n    var d = new Date();\n    return d.getFullYear().toString() +\n      (d.getMonth() + 1).toString().padStart(2, '0') +\n      d.getDate().toString().padStart(2, '0') +\n      '-' +\n      d.getHours().toString().padStart(2, '0') +\n      d.getMinutes().toString().padStart(2, '0');\n  }\n\n  $('#start-Backup').click(function() {\n    $.ajax({\n      url: API_BASE_URL + 'backupandrestore/backup',\n      type: 'GET',\n      cache: false,\n      processData: false,\n      accepts: {\n        json: \"application/json\",\n        text: \"application/sql\"\n      },\n      beforeSend: function(xhr, settings) {\n        changeLoadingState(true);\n      },\n      success: function(response, text_status, xhr) {\n        showNotification(\"Database backup complete\");\n        var fname = getFormattedDateTime() + '.sql';\n        downloadResponse(response, fname);\n      },\n      error: function(xhr, text_status, error_msg) {\n\t      var string = JSON.stringify(xhr.responseJSON);\n\t      var json_object = JSON.parse(string);\n        showNotification(json_object.msg, true);\n      },\n      complete: function(xhr, text_status) {\n        changeLoadingState(false);\n      },\n    });\n  });\n\n  $('#restore-backup').submit(function(event) {\n    event.preventDefault();\n    var formData = new FormData($(this)[0]);\n\n    $.ajax({\n      url: API_BASE_URL + 'backupandrestore/restore',\n      type: 'POST',\n      data: formData,\n      cache: false,\n      contentType: false,\n      processData: false,\n      beforeSend: function(xhr, settings) {\n        changeLoadingState(true);\n      },\n      success: function(response, text_status, xhr) {\n        showNotification(\"Database restore complete\");\n        reloadKamRequired(true);\n      },\n      error: function(xhr, text_status, error_msg) {\n\t      var string = JSON.stringify(xhr.responseJSON);\n\t      var json_object = JSON.parse(string);\n        showNotification(json_object.msg, true);\n      },\n      complete: function(xhr, text_status) {\n        changeLoadingState(false);\n      },\n    });\n\n    return false;\n  });\n\n})(window, document);\n"
  },
  {
    "path": "gui/static/js/bootstrap-toggle.js",
    "content": "/*! ========================================================================\n * Bootstrap Toggle: bootstrap-toggle.js v2.2.0\n * http://www.bootstraptoggle.com\n * ========================================================================\n * Copyright 2014 Min Hur, The New York Times Company\n * Licensed under MIT\n * ======================================================================== */\n\n\n +function ($) {\n \t'use strict';\n\n\t// TOGGLE PUBLIC CLASS DEFINITION\n\t// ==============================\n\n\tvar Toggle = function (element, options) {\n\t\tthis.$element  = $(element)\n\t\tthis.options   = $.extend({}, this.defaults(), options)\n\t\tthis.render()\n\t}\n\n\tToggle.VERSION  = '2.2.0'\n\n\tToggle.DEFAULTS = {\n\t\ton: 'On',\n\t\toff: 'Off',\n\t\tonstyle: 'primary',\n\t\toffstyle: 'default',\n\t\tsize: 'normal',\n\t\tstyle: '',\n\t\twidth: null,\n\t\theight: null\n\t}\n\n\tToggle.prototype.defaults = function() {\n\t\treturn {\n\t\t\ton: this.$element.attr('data-on') || Toggle.DEFAULTS.on,\n\t\t\toff: this.$element.attr('data-off') || Toggle.DEFAULTS.off,\n\t\t\tonstyle: this.$element.attr('data-onstyle') || Toggle.DEFAULTS.onstyle,\n\t\t\toffstyle: this.$element.attr('data-offstyle') || Toggle.DEFAULTS.offstyle,\n\t\t\tsize: this.$element.attr('data-size') || Toggle.DEFAULTS.size,\n\t\t\tstyle: this.$element.attr('data-style') || Toggle.DEFAULTS.style,\n\t\t\twidth: this.$element.attr('data-width') || Toggle.DEFAULTS.width,\n\t\t\theight: this.$element.attr('data-height') || Toggle.DEFAULTS.height\n\t\t}\n\t}\n\n\tToggle.prototype.render = function () {\n\t\tthis._onstyle = 'btn-' + this.options.onstyle\n\t\tthis._offstyle = 'btn-' + this.options.offstyle\n\t\tvar size = this.options.size === 'large' ? 'btn-lg'\n\t\t\t: this.options.size === 'small' ? 'btn-sm'\n\t\t\t: this.options.size === 'mini' ? 'btn-xs'\n\t\t\t: ''\n\t\tvar $toggleOn = $('<label class=\"btn\">').html(this.options.on)\n\t\t\t.addClass(this._onstyle + ' ' + size)\n\t\tvar $toggleOff = $('<label class=\"btn\">').html(this.options.off)\n\t\t\t.addClass(this._offstyle + ' ' + size + ' active')\n\t\tvar $toggleHandle = $('<span class=\"toggle-handle btn btn-default\">')\n\t\t\t.addClass(size)\n\t\tvar $toggleGroup = $('<div class=\"toggle-group\">')\n\t\t\t.append($toggleOn, $toggleOff, $toggleHandle)\n\t\tvar $toggle = $('<div class=\"toggle btn\" data-toggle=\"toggle\">')\n\t\t\t.addClass( this.$element.prop('checked') ? this._onstyle : this._offstyle+' off' )\n\t\t\t.addClass(size).addClass(this.options.style)\n\n\t\tthis.$element.wrap($toggle)\n\t\t$.extend(this, {\n\t\t\t$toggle: this.$element.parent(),\n\t\t\t$toggleOn: $toggleOn,\n\t\t\t$toggleOff: $toggleOff,\n\t\t\t$toggleGroup: $toggleGroup\n\t\t})\n\t\tthis.$toggle.append($toggleGroup)\n\n\t\tvar width = this.options.width || Math.max($toggleOn.outerWidth(), $toggleOff.outerWidth())+($toggleHandle.outerWidth()/2)\n\t\tvar height = this.options.height || Math.max($toggleOn.outerHeight(), $toggleOff.outerHeight())\n\t\t$toggleOn.addClass('toggle-on')\n\t\t$toggleOff.addClass('toggle-off')\n\t\tthis.$toggle.css({ width: width, height: height })\n\t\tif (this.options.height) {\n\t\t\t$toggleOn.css('line-height', $toggleOn.height() + 'px')\n\t\t\t$toggleOff.css('line-height', $toggleOff.height() + 'px')\n\t\t}\n\t\tthis.update(true)\n\t\tthis.trigger(true)\n\t}\n\n\tToggle.prototype.toggle = function () {\n\t\tif (this.$element.prop('checked')) this.off()\n\t\telse this.on()\n\t}\n\n\tToggle.prototype.on = function (silent) {\n\t\tif (this.$element.prop('disabled')) return false\n\t\tthis.$toggle.removeClass(this._offstyle + ' off').addClass(this._onstyle)\n\t\tthis.$element.prop('checked', true)\n\t\tif (!silent) this.trigger()\n\t}\n\n\tToggle.prototype.off = function (silent) {\n\t\tif (this.$element.prop('disabled')) return false\n\t\tthis.$toggle.removeClass(this._onstyle).addClass(this._offstyle + ' off')\n\t\tthis.$element.prop('checked', false)\n\t\tif (!silent) this.trigger()\n\t}\n\n\tToggle.prototype.enable = function () {\n\t\tthis.$toggle.removeAttr('disabled')\n\t\tthis.$element.prop('disabled', false)\n\t}\n\n\tToggle.prototype.disable = function () {\n\t\tthis.$toggle.attr('disabled', 'disabled')\n\t\tthis.$element.prop('disabled', true)\n\t}\n\n\tToggle.prototype.update = function (silent) {\n\t\tif (this.$element.prop('disabled')) this.disable()\n\t\telse this.enable()\n\t\tif (this.$element.prop('checked')) this.on(silent)\n\t\telse this.off(silent)\n\t}\n\n\tToggle.prototype.trigger = function (silent) {\n\t\tthis.$element.off('change.bs.toggle')\n\t\tif (!silent) this.$element.change()\n\t\tthis.$element.on('change.bs.toggle', $.proxy(function() {\n\t\t\tthis.update()\n\t\t}, this))\n\t}\n\n\tToggle.prototype.destroy = function() {\n\t\tthis.$element.off('change.bs.toggle')\n\t\tthis.$toggleGroup.remove()\n\t\tthis.$element.removeData('bs.toggle')\n\t\tthis.$element.unwrap()\n\t}\n\n\t// TOGGLE PLUGIN DEFINITION\n\t// ========================\n\n\tfunction Plugin(option) {\n\t\treturn this.each(function () {\n\t\t\tvar $this   = $(this)\n\t\t\tvar data    = $this.data('bs.toggle')\n\t\t\tvar options = typeof option == 'object' && option\n\n\t\t\tif (!data) $this.data('bs.toggle', (data = new Toggle(this, options)))\n\t\t\tif (typeof option == 'string' && data[option]) data[option]()\n\t\t})\n\t}\n\n\tvar old = $.fn.bootstrapToggle\n\n\t$.fn.bootstrapToggle             = Plugin\n\t$.fn.bootstrapToggle.Constructor = Toggle\n\n\t// TOGGLE NO CONFLICT\n\t// ==================\n\n\t$.fn.toggle.noConflict = function () {\n\t\t$.fn.bootstrapToggle = old\n\t\treturn this\n\t}\n\n\t// TOGGLE DATA-API\n\t// ===============\n\n\t$(function() {\n\t\t$('input[type=checkbox][data-toggle^=toggle]').bootstrapToggle()\n\t})\n\n\t$(document).on('click.bs.toggle', 'div[data-toggle^=toggle]', function(e) {\n\t\tvar $checkbox = $(this).find('input[type=checkbox]')\n\t\t$checkbox.bootstrapToggle('toggle')\n\t\te.preventDefault()\n\t})\n\n}(jQuery);\n"
  },
  {
    "path": "gui/static/js/bootstrap.js",
    "content": "/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under the MIT license\n */\n\nif (typeof jQuery === 'undefined') {\n  throw new Error('Bootstrap\\'s JavaScript requires jQuery')\n}\n\n+function ($) {\n  'use strict';\n  var version = $.fn.jquery.split(' ')[0].split('.')\n  if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] > 3)) {\n    throw new Error('Bootstrap\\'s JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4')\n  }\n}(jQuery);\n\n/* ========================================================================\n * Bootstrap: transition.js v3.3.7\n * http://getbootstrap.com/javascript/#transitions\n * ========================================================================\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * ======================================================================== */\n\n\n+function ($) {\n  'use strict';\n\n  // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)\n  // ============================================================\n\n  function transitionEnd() {\n    var el = document.createElement('bootstrap')\n\n    var transEndEventNames = {\n      WebkitTransition : 'webkitTransitionEnd',\n      MozTransition    : 'transitionend',\n      OTransition      : 'oTransitionEnd otransitionend',\n      transition       : 'transitionend'\n    }\n\n    for (var name in transEndEventNames) {\n      if (el.style[name] !== undefined) {\n        return { end: transEndEventNames[name] }\n      }\n    }\n\n    return false // explicit for ie8 (  ._.)\n  }\n\n  // http://blog.alexmaccaw.com/css-transitions\n  $.fn.emulateTransitionEnd = function (duration) {\n    var called = false\n    var $el = this\n    $(this).one('bsTransitionEnd', function () { called = true })\n    var callback = function () { if (!called) $($el).trigger($.support.transition.end) }\n    setTimeout(callback, duration)\n    return this\n  }\n\n  $(function () {\n    $.support.transition = transitionEnd()\n\n    if (!$.support.transition) return\n\n    $.event.special.bsTransitionEnd = {\n      bindType: $.support.transition.end,\n      delegateType: $.support.transition.end,\n      handle: function (e) {\n        if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments)\n      }\n    }\n  })\n\n}(jQuery);\n\n/* ========================================================================\n * Bootstrap: alert.js v3.3.7\n * http://getbootstrap.com/javascript/#alerts\n * ========================================================================\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * ======================================================================== */\n\n\n+function ($) {\n  'use strict';\n\n  // ALERT CLASS DEFINITION\n  // ======================\n\n  var dismiss = '[data-dismiss=\"alert\"]'\n  var Alert   = function (el) {\n    $(el).on('click', dismiss, this.close)\n  }\n\n  Alert.VERSION = '3.3.7'\n\n  Alert.TRANSITION_DURATION = 150\n\n  Alert.prototype.close = function (e) {\n    var $this    = $(this)\n    var selector = $this.attr('data-target')\n\n    if (!selector) {\n      selector = $this.attr('href')\n      selector = selector && selector.replace(/.*(?=#[^\\s]*$)/, '') // strip for ie7\n    }\n\n    var $parent = $(selector === '#' ? [] : selector)\n\n    if (e) e.preventDefault()\n\n    if (!$parent.length) {\n      $parent = $this.closest('.alert')\n    }\n\n    $parent.trigger(e = $.Event('close.bs.alert'))\n\n    if (e.isDefaultPrevented()) return\n\n    $parent.removeClass('in')\n\n    function removeElement() {\n      // detach from parent, fire event then clean up data\n      $parent.detach().trigger('closed.bs.alert').remove()\n    }\n\n    $.support.transition && $parent.hasClass('fade') ?\n      $parent\n        .one('bsTransitionEnd', removeElement)\n        .emulateTransitionEnd(Alert.TRANSITION_DURATION) :\n      removeElement()\n  }\n\n\n  // ALERT PLUGIN DEFINITION\n  // =======================\n\n  function Plugin(option) {\n    return this.each(function () {\n      var $this = $(this)\n      var data  = $this.data('bs.alert')\n\n      if (!data) $this.data('bs.alert', (data = new Alert(this)))\n      if (typeof option == 'string') data[option].call($this)\n    })\n  }\n\n  var old = $.fn.alert\n\n  $.fn.alert             = Plugin\n  $.fn.alert.Constructor = Alert\n\n\n  // ALERT NO CONFLICT\n  // =================\n\n  $.fn.alert.noConflict = function () {\n    $.fn.alert = old\n    return this\n  }\n\n\n  // ALERT DATA-API\n  // ==============\n\n  $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close)\n\n}(jQuery);\n\n/* ========================================================================\n * Bootstrap: button.js v3.3.7\n * http://getbootstrap.com/javascript/#buttons\n * ========================================================================\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * ======================================================================== */\n\n\n+function ($) {\n  'use strict';\n\n  // BUTTON PUBLIC CLASS DEFINITION\n  // ==============================\n\n  var Button = function (element, options) {\n    this.$element  = $(element)\n    this.options   = $.extend({}, Button.DEFAULTS, options)\n    this.isLoading = false\n  }\n\n  Button.VERSION  = '3.3.7'\n\n  Button.DEFAULTS = {\n    loadingText: 'loading...'\n  }\n\n  Button.prototype.setState = function (state) {\n    var d    = 'disabled'\n    var $el  = this.$element\n    var val  = $el.is('input') ? 'val' : 'html'\n    var data = $el.data()\n\n    state += 'Text'\n\n    if (data.resetText == null) $el.data('resetText', $el[val]())\n\n    // push to event loop to allow forms to submit\n    setTimeout($.proxy(function () {\n      $el[val](data[state] == null ? this.options[state] : data[state])\n\n      if (state == 'loadingText') {\n        this.isLoading = true\n        $el.addClass(d).attr(d, d).prop(d, true)\n      } else if (this.isLoading) {\n        this.isLoading = false\n        $el.removeClass(d).removeAttr(d).prop(d, false)\n      }\n    }, this), 0)\n  }\n\n  Button.prototype.toggle = function () {\n    var changed = true\n    var $parent = this.$element.closest('[data-toggle=\"buttons\"]')\n\n    if ($parent.length) {\n      var $input = this.$element.find('input')\n      if ($input.prop('type') == 'radio') {\n        if ($input.prop('checked')) changed = false\n        $parent.find('.active').removeClass('active')\n        this.$element.addClass('active')\n      } else if ($input.prop('type') == 'checkbox') {\n        if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false\n        this.$element.toggleClass('active')\n      }\n      $input.prop('checked', this.$element.hasClass('active'))\n      if (changed) $input.trigger('change')\n    } else {\n      this.$element.attr('aria-pressed', !this.$element.hasClass('active'))\n      this.$element.toggleClass('active')\n    }\n  }\n\n\n  // BUTTON PLUGIN DEFINITION\n  // ========================\n\n  function Plugin(option) {\n    return this.each(function () {\n      var $this   = $(this)\n      var data    = $this.data('bs.button')\n      var options = typeof option == 'object' && option\n\n      if (!data) $this.data('bs.button', (data = new Button(this, options)))\n\n      if (option == 'toggle') data.toggle()\n      else if (option) data.setState(option)\n    })\n  }\n\n  var old = $.fn.button\n\n  $.fn.button             = Plugin\n  $.fn.button.Constructor = Button\n\n\n  // BUTTON NO CONFLICT\n  // ==================\n\n  $.fn.button.noConflict = function () {\n    $.fn.button = old\n    return this\n  }\n\n\n  // BUTTON DATA-API\n  // ===============\n\n  $(document)\n    .on('click.bs.button.data-api', '[data-toggle^=\"button\"]', function (e) {\n      var $btn = $(e.target).closest('.btn')\n      Plugin.call($btn, 'toggle')\n      if (!($(e.target).is('input[type=\"radio\"], input[type=\"checkbox\"]'))) {\n        // Prevent double click on radios, and the double selections (so cancellation) on checkboxes\n        e.preventDefault()\n        // The target component still receive the focus\n        if ($btn.is('input,button')) $btn.trigger('focus')\n        else $btn.find('input:visible,button:visible').first().trigger('focus')\n      }\n    })\n    .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^=\"button\"]', function (e) {\n      $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type))\n    })\n\n}(jQuery);\n\n/* ========================================================================\n * Bootstrap: carousel.js v3.3.7\n * http://getbootstrap.com/javascript/#carousel\n * ========================================================================\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * ======================================================================== */\n\n\n+function ($) {\n  'use strict';\n\n  // CAROUSEL CLASS DEFINITION\n  // =========================\n\n  var Carousel = function (element, options) {\n    this.$element    = $(element)\n    this.$indicators = this.$element.find('.carousel-indicators')\n    this.options     = options\n    this.paused      = null\n    this.sliding     = null\n    this.interval    = null\n    this.$active     = null\n    this.$items      = null\n\n    this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this))\n\n    this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element\n      .on('mouseenter.bs.carousel', $.proxy(this.pause, this))\n      .on('mouseleave.bs.carousel', $.proxy(this.cycle, this))\n  }\n\n  Carousel.VERSION  = '3.3.7'\n\n  Carousel.TRANSITION_DURATION = 600\n\n  Carousel.DEFAULTS = {\n    interval: 5000,\n    pause: 'hover',\n    wrap: true,\n    keyboard: true\n  }\n\n  Carousel.prototype.keydown = function (e) {\n    if (/input|textarea/i.test(e.target.tagName)) return\n    switch (e.which) {\n      case 37: this.prev(); break\n      case 39: this.next(); break\n      default: return\n    }\n\n    e.preventDefault()\n  }\n\n  Carousel.prototype.cycle = function (e) {\n    e || (this.paused = false)\n\n    this.interval && clearInterval(this.interval)\n\n    this.options.interval\n      && !this.paused\n      && (this.interval = setInterval($.proxy(this.next, this), this.options.interval))\n\n    return this\n  }\n\n  Carousel.prototype.getItemIndex = function (item) {\n    this.$items = item.parent().children('.item')\n    return this.$items.index(item || this.$active)\n  }\n\n  Carousel.prototype.getItemForDirection = function (direction, active) {\n    var activeIndex = this.getItemIndex(active)\n    var willWrap = (direction == 'prev' && activeIndex === 0)\n                || (direction == 'next' && activeIndex == (this.$items.length - 1))\n    if (willWrap && !this.options.wrap) return active\n    var delta = direction == 'prev' ? -1 : 1\n    var itemIndex = (activeIndex + delta) % this.$items.length\n    return this.$items.eq(itemIndex)\n  }\n\n  Carousel.prototype.to = function (pos) {\n    var that        = this\n    var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active'))\n\n    if (pos > (this.$items.length - 1) || pos < 0) return\n\n    if (this.sliding)       return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, \"slid\"\n    if (activeIndex == pos) return this.pause().cycle()\n\n    return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos))\n  }\n\n  Carousel.prototype.pause = function (e) {\n    e || (this.paused = true)\n\n    if (this.$element.find('.next, .prev').length && $.support.transition) {\n      this.$element.trigger($.support.transition.end)\n      this.cycle(true)\n    }\n\n    this.interval = clearInterval(this.interval)\n\n    return this\n  }\n\n  Carousel.prototype.next = function () {\n    if (this.sliding) return\n    return this.slide('next')\n  }\n\n  Carousel.prototype.prev = function () {\n    if (this.sliding) return\n    return this.slide('prev')\n  }\n\n  Carousel.prototype.slide = function (type, next) {\n    var $active   = this.$element.find('.item.active')\n    var $next     = next || this.getItemForDirection(type, $active)\n    var isCycling = this.interval\n    var direction = type == 'next' ? 'left' : 'right'\n    var that      = this\n\n    if ($next.hasClass('active')) return (this.sliding = false)\n\n    var relatedTarget = $next[0]\n    var slideEvent = $.Event('slide.bs.carousel', {\n      relatedTarget: relatedTarget,\n      direction: direction\n    })\n    this.$element.trigger(slideEvent)\n    if (slideEvent.isDefaultPrevented()) return\n\n    this.sliding = true\n\n    isCycling && this.pause()\n\n    if (this.$indicators.length) {\n      this.$indicators.find('.active').removeClass('active')\n      var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)])\n      $nextIndicator && $nextIndicator.addClass('active')\n    }\n\n    var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, \"slid\"\n    if ($.support.transition && this.$element.hasClass('slide')) {\n      $next.addClass(type)\n      $next[0].offsetWidth // force reflow\n      $active.addClass(direction)\n      $next.addClass(direction)\n      $active\n        .one('bsTransitionEnd', function () {\n          $next.removeClass([type, direction].join(' ')).addClass('active')\n          $active.removeClass(['active', direction].join(' '))\n          that.sliding = false\n          setTimeout(function () {\n            that.$element.trigger(slidEvent)\n          }, 0)\n        })\n        .emulateTransitionEnd(Carousel.TRANSITION_DURATION)\n    } else {\n      $active.removeClass('active')\n      $next.addClass('active')\n      this.sliding = false\n      this.$element.trigger(slidEvent)\n    }\n\n    isCycling && this.cycle()\n\n    return this\n  }\n\n\n  // CAROUSEL PLUGIN DEFINITION\n  // ==========================\n\n  function Plugin(option) {\n    return this.each(function () {\n      var $this   = $(this)\n      var data    = $this.data('bs.carousel')\n      var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option)\n      var action  = typeof option == 'string' ? option : options.slide\n\n      if (!data) $this.data('bs.carousel', (data = new Carousel(this, options)))\n      if (typeof option == 'number') data.to(option)\n      else if (action) data[action]()\n      else if (options.interval) data.pause().cycle()\n    })\n  }\n\n  var old = $.fn.carousel\n\n  $.fn.carousel             = Plugin\n  $.fn.carousel.Constructor = Carousel\n\n\n  // CAROUSEL NO CONFLICT\n  // ====================\n\n  $.fn.carousel.noConflict = function () {\n    $.fn.carousel = old\n    return this\n  }\n\n\n  // CAROUSEL DATA-API\n  // =================\n\n  var clickHandler = function (e) {\n    var href\n    var $this   = $(this)\n    var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\\s]+$)/, '')) // strip for ie7\n    if (!$target.hasClass('carousel')) return\n    var options = $.extend({}, $target.data(), $this.data())\n    var slideIndex = $this.attr('data-slide-to')\n    if (slideIndex) options.interval = false\n\n    Plugin.call($target, options)\n\n    if (slideIndex) {\n      $target.data('bs.carousel').to(slideIndex)\n    }\n\n    e.preventDefault()\n  }\n\n  $(document)\n    .on('click.bs.carousel.data-api', '[data-slide]', clickHandler)\n    .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler)\n\n  $(window).on('load', function () {\n    $('[data-ride=\"carousel\"]').each(function () {\n      var $carousel = $(this)\n      Plugin.call($carousel, $carousel.data())\n    })\n  })\n\n}(jQuery);\n\n/* ========================================================================\n * Bootstrap: collapse.js v3.3.7\n * http://getbootstrap.com/javascript/#collapse\n * ========================================================================\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * ======================================================================== */\n\n/* jshint latedef: false */\n\n+function ($) {\n  'use strict';\n\n  // COLLAPSE PUBLIC CLASS DEFINITION\n  // ================================\n\n  var Collapse = function (element, options) {\n    this.$element      = $(element)\n    this.options       = $.extend({}, Collapse.DEFAULTS, options)\n    this.$trigger      = $('[data-toggle=\"collapse\"][href=\"#' + element.id + '\"],' +\n                           '[data-toggle=\"collapse\"][data-target=\"#' + element.id + '\"]')\n    this.transitioning = null\n\n    if (this.options.parent) {\n      this.$parent = this.getParent()\n    } else {\n      this.addAriaAndCollapsedClass(this.$element, this.$trigger)\n    }\n\n    if (this.options.toggle) this.toggle()\n  }\n\n  Collapse.VERSION  = '3.3.7'\n\n  Collapse.TRANSITION_DURATION = 350\n\n  Collapse.DEFAULTS = {\n    toggle: true\n  }\n\n  Collapse.prototype.dimension = function () {\n    var hasWidth = this.$element.hasClass('width')\n    return hasWidth ? 'width' : 'height'\n  }\n\n  Collapse.prototype.show = function () {\n    if (this.transitioning || this.$element.hasClass('in')) return\n\n    var activesData\n    var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing')\n\n    if (actives && actives.length) {\n      activesData = actives.data('bs.collapse')\n      if (activesData && activesData.transitioning) return\n    }\n\n    var startEvent = $.Event('show.bs.collapse')\n    this.$element.trigger(startEvent)\n    if (startEvent.isDefaultPrevented()) return\n\n    if (actives && actives.length) {\n      Plugin.call(actives, 'hide')\n      activesData || actives.data('bs.collapse', null)\n    }\n\n    var dimension = this.dimension()\n\n    this.$element\n      .removeClass('collapse')\n      .addClass('collapsing')[dimension](0)\n      .attr('aria-expanded', true)\n\n    this.$trigger\n      .removeClass('collapsed')\n      .attr('aria-expanded', true)\n\n    this.transitioning = 1\n\n    var complete = function () {\n      this.$element\n        .removeClass('collapsing')\n        .addClass('collapse in')[dimension]('')\n      this.transitioning = 0\n      this.$element\n        .trigger('shown.bs.collapse')\n    }\n\n    if (!$.support.transition) return complete.call(this)\n\n    var scrollSize = $.camelCase(['scroll', dimension].join('-'))\n\n    this.$element\n      .one('bsTransitionEnd', $.proxy(complete, this))\n      .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize])\n  }\n\n  Collapse.prototype.hide = function () {\n    if (this.transitioning || !this.$element.hasClass('in')) return\n\n    var startEvent = $.Event('hide.bs.collapse')\n    this.$element.trigger(startEvent)\n    if (startEvent.isDefaultPrevented()) return\n\n    var dimension = this.dimension()\n\n    this.$element[dimension](this.$element[dimension]())[0].offsetHeight\n\n    this.$element\n      .addClass('collapsing')\n      .removeClass('collapse in')\n      .attr('aria-expanded', false)\n\n    this.$trigger\n      .addClass('collapsed')\n      .attr('aria-expanded', false)\n\n    this.transitioning = 1\n\n    var complete = function () {\n      this.transitioning = 0\n      this.$element\n        .removeClass('collapsing')\n        .addClass('collapse')\n        .trigger('hidden.bs.collapse')\n    }\n\n    if (!$.support.transition) return complete.call(this)\n\n    this.$element\n      [dimension](0)\n      .one('bsTransitionEnd', $.proxy(complete, this))\n      .emulateTransitionEnd(Collapse.TRANSITION_DURATION)\n  }\n\n  Collapse.prototype.toggle = function () {\n    this[this.$element.hasClass('in') ? 'hide' : 'show']()\n  }\n\n  Collapse.prototype.getParent = function () {\n    return $(this.options.parent)\n      .find('[data-toggle=\"collapse\"][data-parent=\"' + this.options.parent + '\"]')\n      .each($.proxy(function (i, element) {\n        var $element = $(element)\n        this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element)\n      }, this))\n      .end()\n  }\n\n  Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) {\n    var isOpen = $element.hasClass('in')\n\n    $element.attr('aria-expanded', isOpen)\n    $trigger\n      .toggleClass('collapsed', !isOpen)\n      .attr('aria-expanded', isOpen)\n  }\n\n  function getTargetFromTrigger($trigger) {\n    var href\n    var target = $trigger.attr('data-target')\n      || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\\s]+$)/, '') // strip for ie7\n\n    return $(target)\n  }\n\n\n  // COLLAPSE PLUGIN DEFINITION\n  // ==========================\n\n  function Plugin(option) {\n    return this.each(function () {\n      var $this   = $(this)\n      var data    = $this.data('bs.collapse')\n      var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option)\n\n      if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false\n      if (!data) $this.data('bs.collapse', (data = new Collapse(this, options)))\n      if (typeof option == 'string') data[option]()\n    })\n  }\n\n  var old = $.fn.collapse\n\n  $.fn.collapse             = Plugin\n  $.fn.collapse.Constructor = Collapse\n\n\n  // COLLAPSE NO CONFLICT\n  // ====================\n\n  $.fn.collapse.noConflict = function () {\n    $.fn.collapse = old\n    return this\n  }\n\n\n  // COLLAPSE DATA-API\n  // =================\n\n  $(document).on('click.bs.collapse.data-api', '[data-toggle=\"collapse\"]', function (e) {\n    var $this   = $(this)\n\n    if (!$this.attr('data-target')) e.preventDefault()\n\n    var $target = getTargetFromTrigger($this)\n    var data    = $target.data('bs.collapse')\n    var option  = data ? 'toggle' : $this.data()\n\n    Plugin.call($target, option)\n  })\n\n}(jQuery);\n\n/* ========================================================================\n * Bootstrap: dropdown.js v3.3.7\n * http://getbootstrap.com/javascript/#dropdowns\n * ========================================================================\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * ======================================================================== */\n\n\n+function ($) {\n  'use strict';\n\n  // DROPDOWN CLASS DEFINITION\n  // =========================\n\n  var backdrop = '.dropdown-backdrop'\n  var toggle   = '[data-toggle=\"dropdown\"]'\n  var Dropdown = function (element) {\n    $(element).on('click.bs.dropdown', this.toggle)\n  }\n\n  Dropdown.VERSION = '3.3.7'\n\n  function getParent($this) {\n    var selector = $this.attr('data-target')\n\n    if (!selector) {\n      selector = $this.attr('href')\n      selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\\s]*$)/, '') // strip for ie7\n    }\n\n    var $parent = selector && $(selector)\n\n    return $parent && $parent.length ? $parent : $this.parent()\n  }\n\n  function clearMenus(e) {\n    if (e && e.which === 3) return\n    $(backdrop).remove()\n    $(toggle).each(function () {\n      var $this         = $(this)\n      var $parent       = getParent($this)\n      var relatedTarget = { relatedTarget: this }\n\n      if (!$parent.hasClass('open')) return\n\n      if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return\n\n      $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget))\n\n      if (e.isDefaultPrevented()) return\n\n      $this.attr('aria-expanded', 'false')\n      $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget))\n    })\n  }\n\n  Dropdown.prototype.toggle = function (e) {\n    var $this = $(this)\n\n    if ($this.is('.disabled, :disabled')) return\n\n    var $parent  = getParent($this)\n    var isActive = $parent.hasClass('open')\n\n    clearMenus()\n\n    if (!isActive) {\n      if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {\n        // if mobile we use a backdrop because click events don't delegate\n        $(document.createElement('div'))\n          .addClass('dropdown-backdrop')\n          .insertAfter($(this))\n          .on('click', clearMenus)\n      }\n\n      var relatedTarget = { relatedTarget: this }\n      $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget))\n\n      if (e.isDefaultPrevented()) return\n\n      $this\n        .trigger('focus')\n        .attr('aria-expanded', 'true')\n\n      $parent\n        .toggleClass('open')\n        .trigger($.Event('shown.bs.dropdown', relatedTarget))\n    }\n\n    return false\n  }\n\n  Dropdown.prototype.keydown = function (e) {\n    if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return\n\n    var $this = $(this)\n\n    e.preventDefault()\n    e.stopPropagation()\n\n    if ($this.is('.disabled, :disabled')) return\n\n    var $parent  = getParent($this)\n    var isActive = $parent.hasClass('open')\n\n    if (!isActive && e.which != 27 || isActive && e.which == 27) {\n      if (e.which == 27) $parent.find(toggle).trigger('focus')\n      return $this.trigger('click')\n    }\n\n    var desc = ' li:not(.disabled):visible a'\n    var $items = $parent.find('.dropdown-menu' + desc)\n\n    if (!$items.length) return\n\n    var index = $items.index(e.target)\n\n    if (e.which == 38 && index > 0)                 index--         // up\n    if (e.which == 40 && index < $items.length - 1) index++         // down\n    if (!~index)                                    index = 0\n\n    $items.eq(index).trigger('focus')\n  }\n\n\n  // DROPDOWN PLUGIN DEFINITION\n  // ==========================\n\n  function Plugin(option) {\n    return this.each(function () {\n      var $this = $(this)\n      var data  = $this.data('bs.dropdown')\n\n      if (!data) $this.data('bs.dropdown', (data = new Dropdown(this)))\n      if (typeof option == 'string') data[option].call($this)\n    })\n  }\n\n  var old = $.fn.dropdown\n\n  $.fn.dropdown             = Plugin\n  $.fn.dropdown.Constructor = Dropdown\n\n\n  // DROPDOWN NO CONFLICT\n  // ====================\n\n  $.fn.dropdown.noConflict = function () {\n    $.fn.dropdown = old\n    return this\n  }\n\n\n  // APPLY TO STANDARD DROPDOWN ELEMENTS\n  // ===================================\n\n  $(document)\n    .on('click.bs.dropdown.data-api', clearMenus)\n    .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })\n    .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle)\n    .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown)\n    .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown)\n\n}(jQuery);\n\n/* ========================================================================\n * Bootstrap: modal.js v3.3.7\n * http://getbootstrap.com/javascript/#modals\n * ========================================================================\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * ======================================================================== */\n\n\n+function ($) {\n  'use strict';\n\n  // MODAL CLASS DEFINITION\n  // ======================\n\n  var Modal = function (element, options) {\n    this.options             = options\n    this.$body               = $(document.body)\n    this.$element            = $(element)\n    this.$dialog             = this.$element.find('.modal-dialog')\n    this.$backdrop           = null\n    this.isShown             = null\n    this.originalBodyPad     = null\n    this.scrollbarWidth      = 0\n    this.ignoreBackdropClick = false\n\n    if (this.options.remote) {\n      this.$element\n        .find('.modal-content')\n        .load(this.options.remote, $.proxy(function () {\n          this.$element.trigger('loaded.bs.modal')\n        }, this))\n    }\n  }\n\n  Modal.VERSION  = '3.3.7'\n\n  Modal.TRANSITION_DURATION = 300\n  Modal.BACKDROP_TRANSITION_DURATION = 150\n\n  Modal.DEFAULTS = {\n    backdrop: true,\n    keyboard: true,\n    show: true\n  }\n\n  Modal.prototype.toggle = function (_relatedTarget) {\n    return this.isShown ? this.hide() : this.show(_relatedTarget)\n  }\n\n  Modal.prototype.show = function (_relatedTarget) {\n    var that = this\n    var e    = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })\n\n    this.$element.trigger(e)\n\n    if (this.isShown || e.isDefaultPrevented()) return\n\n    this.isShown = true\n\n    this.checkScrollbar()\n    this.setScrollbar()\n    this.$body.addClass('modal-open')\n\n    this.escape()\n    this.resize()\n\n    this.$element.on('click.dismiss.bs.modal', '[data-dismiss=\"modal\"]', $.proxy(this.hide, this))\n\n    this.$dialog.on('mousedown.dismiss.bs.modal', function () {\n      that.$element.one('mouseup.dismiss.bs.modal', function (e) {\n        if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true\n      })\n    })\n\n    this.backdrop(function () {\n      var transition = $.support.transition && that.$element.hasClass('fade')\n\n      if (!that.$element.parent().length) {\n        that.$element.appendTo(that.$body) // don't move modals dom position\n      }\n\n      that.$element\n        .show()\n        .scrollTop(0)\n\n      that.adjustDialog()\n\n      if (transition) {\n        that.$element[0].offsetWidth // force reflow\n      }\n\n      that.$element.addClass('in')\n\n      that.enforceFocus()\n\n      var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })\n\n      transition ?\n        that.$dialog // wait for modal to slide in\n          .one('bsTransitionEnd', function () {\n            that.$element.trigger('focus').trigger(e)\n          })\n          .emulateTransitionEnd(Modal.TRANSITION_DURATION) :\n        that.$element.trigger('focus').trigger(e)\n    })\n  }\n\n  Modal.prototype.hide = function (e) {\n    if (e) e.preventDefault()\n\n    e = $.Event('hide.bs.modal')\n\n    this.$element.trigger(e)\n\n    if (!this.isShown || e.isDefaultPrevented()) return\n\n    this.isShown = false\n\n    this.escape()\n    this.resize()\n\n    $(document).off('focusin.bs.modal')\n\n    this.$element\n      .removeClass('in')\n      .off('click.dismiss.bs.modal')\n      .off('mouseup.dismiss.bs.modal')\n\n    this.$dialog.off('mousedown.dismiss.bs.modal')\n\n    $.support.transition && this.$element.hasClass('fade') ?\n      this.$element\n        .one('bsTransitionEnd', $.proxy(this.hideModal, this))\n        .emulateTransitionEnd(Modal.TRANSITION_DURATION) :\n      this.hideModal()\n  }\n\n  Modal.prototype.enforceFocus = function () {\n    $(document)\n      .off('focusin.bs.modal') // guard against infinite focus loop\n      .on('focusin.bs.modal', $.proxy(function (e) {\n        if (document !== e.target &&\n            this.$element[0] !== e.target &&\n            !this.$element.has(e.target).length) {\n          this.$element.trigger('focus')\n        }\n      }, this))\n  }\n\n  Modal.prototype.escape = function () {\n    if (this.isShown && this.options.keyboard) {\n      this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) {\n        e.which == 27 && this.hide()\n      }, this))\n    } else if (!this.isShown) {\n      this.$element.off('keydown.dismiss.bs.modal')\n    }\n  }\n\n  Modal.prototype.resize = function () {\n    if (this.isShown) {\n      $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this))\n    } else {\n      $(window).off('resize.bs.modal')\n    }\n  }\n\n  Modal.prototype.hideModal = function () {\n    var that = this\n    this.$element.hide()\n    this.backdrop(function () {\n      that.$body.removeClass('modal-open')\n      that.resetAdjustments()\n      that.resetScrollbar()\n      that.$element.trigger('hidden.bs.modal')\n    })\n  }\n\n  Modal.prototype.removeBackdrop = function () {\n    this.$backdrop && this.$backdrop.remove()\n    this.$backdrop = null\n  }\n\n  Modal.prototype.backdrop = function (callback) {\n    var that = this\n    var animate = this.$element.hasClass('fade') ? 'fade' : ''\n\n    if (this.isShown && this.options.backdrop) {\n      var doAnimate = $.support.transition && animate\n\n      this.$backdrop = $(document.createElement('div'))\n        .addClass('modal-backdrop ' + animate)\n        .appendTo(this.$body)\n\n      this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) {\n        if (this.ignoreBackdropClick) {\n          this.ignoreBackdropClick = false\n          return\n        }\n        if (e.target !== e.currentTarget) return\n        this.options.backdrop == 'static'\n          ? this.$element[0].focus()\n          : this.hide()\n      }, this))\n\n      if (doAnimate) this.$backdrop[0].offsetWidth // force reflow\n\n      this.$backdrop.addClass('in')\n\n      if (!callback) return\n\n      doAnimate ?\n        this.$backdrop\n          .one('bsTransitionEnd', callback)\n          .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :\n        callback()\n\n    } else if (!this.isShown && this.$backdrop) {\n      this.$backdrop.removeClass('in')\n\n      var callbackRemove = function () {\n        that.removeBackdrop()\n        callback && callback()\n      }\n      $.support.transition && this.$element.hasClass('fade') ?\n        this.$backdrop\n          .one('bsTransitionEnd', callbackRemove)\n          .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :\n        callbackRemove()\n\n    } else if (callback) {\n      callback()\n    }\n  }\n\n  // these following methods are used to handle overflowing modals\n\n  Modal.prototype.handleUpdate = function () {\n    this.adjustDialog()\n  }\n\n  Modal.prototype.adjustDialog = function () {\n    var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight\n\n    this.$element.css({\n      paddingLeft:  !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '',\n      paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : ''\n    })\n  }\n\n  Modal.prototype.resetAdjustments = function () {\n    this.$element.css({\n      paddingLeft: '',\n      paddingRight: ''\n    })\n  }\n\n  Modal.prototype.checkScrollbar = function () {\n    var fullWindowWidth = window.innerWidth\n    if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8\n      var documentElementRect = document.documentElement.getBoundingClientRect()\n      fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left)\n    }\n    this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth\n    this.scrollbarWidth = this.measureScrollbar()\n  }\n\n  Modal.prototype.setScrollbar = function () {\n    var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10)\n    this.originalBodyPad = document.body.style.paddingRight || ''\n    if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth)\n  }\n\n  Modal.prototype.resetScrollbar = function () {\n    this.$body.css('padding-right', this.originalBodyPad)\n  }\n\n  Modal.prototype.measureScrollbar = function () { // thx walsh\n    var scrollDiv = document.createElement('div')\n    scrollDiv.className = 'modal-scrollbar-measure'\n    this.$body.append(scrollDiv)\n    var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth\n    this.$body[0].removeChild(scrollDiv)\n    return scrollbarWidth\n  }\n\n\n  // MODAL PLUGIN DEFINITION\n  // =======================\n\n  function Plugin(option, _relatedTarget) {\n    return this.each(function () {\n      var $this   = $(this)\n      var data    = $this.data('bs.modal')\n      var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option)\n\n      if (!data) $this.data('bs.modal', (data = new Modal(this, options)))\n      if (typeof option == 'string') data[option](_relatedTarget)\n      else if (options.show) data.show(_relatedTarget)\n    })\n  }\n\n  var old = $.fn.modal\n\n  $.fn.modal             = Plugin\n  $.fn.modal.Constructor = Modal\n\n\n  // MODAL NO CONFLICT\n  // =================\n\n  $.fn.modal.noConflict = function () {\n    $.fn.modal = old\n    return this\n  }\n\n\n  // MODAL DATA-API\n  // ==============\n\n  $(document).on('click.bs.modal.data-api', '[data-toggle=\"modal\"]', function (e) {\n    var $this   = $(this)\n    var href    = $this.attr('href')\n    var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\\s]+$)/, ''))) // strip for ie7\n    var option  = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())\n\n    if ($this.is('a')) e.preventDefault()\n\n    $target.one('show.bs.modal', function (showEvent) {\n      if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown\n      $target.one('hidden.bs.modal', function () {\n        $this.is(':visible') && $this.trigger('focus')\n      })\n    })\n    Plugin.call($target, option, this)\n  })\n\n}(jQuery);\n\n/* ========================================================================\n * Bootstrap: tooltip.js v3.3.7\n * http://getbootstrap.com/javascript/#tooltip\n * Inspired by the original jQuery.tipsy by Jason Frame\n * ========================================================================\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * ======================================================================== */\n\n\n+function ($) {\n  'use strict';\n\n  // TOOLTIP PUBLIC CLASS DEFINITION\n  // ===============================\n\n  var Tooltip = function (element, options) {\n    this.type       = null\n    this.options    = null\n    this.enabled    = null\n    this.timeout    = null\n    this.hoverState = null\n    this.$element   = null\n    this.inState    = null\n\n    this.init('tooltip', element, options)\n  }\n\n  Tooltip.VERSION  = '3.3.7'\n\n  Tooltip.TRANSITION_DURATION = 150\n\n  Tooltip.DEFAULTS = {\n    animation: true,\n    placement: 'top',\n    selector: false,\n    template: '<div class=\"tooltip\" role=\"tooltip\"><div class=\"tooltip-arrow\"></div><div class=\"tooltip-inner\"></div></div>',\n    trigger: 'hover focus',\n    title: '',\n    delay: 0,\n    html: false,\n    container: false,\n    viewport: {\n      selector: 'body',\n      padding: 0\n    }\n  }\n\n  Tooltip.prototype.init = function (type, element, options) {\n    this.enabled   = true\n    this.type      = type\n    this.$element  = $(element)\n    this.options   = this.getOptions(options)\n    this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport))\n    this.inState   = { click: false, hover: false, focus: false }\n\n    if (this.$element[0] instanceof document.constructor && !this.options.selector) {\n      throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!')\n    }\n\n    var triggers = this.options.trigger.split(' ')\n\n    for (var i = triggers.length; i--;) {\n      var trigger = triggers[i]\n\n      if (trigger == 'click') {\n        this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))\n      } else if (trigger != 'manual') {\n        var eventIn  = trigger == 'hover' ? 'mouseenter' : 'focusin'\n        var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'\n\n        this.$element.on(eventIn  + '.' + this.type, this.options.selector, $.proxy(this.enter, this))\n        this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))\n      }\n    }\n\n    this.options.selector ?\n      (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :\n      this.fixTitle()\n  }\n\n  Tooltip.prototype.getDefaults = function () {\n    return Tooltip.DEFAULTS\n  }\n\n  Tooltip.prototype.getOptions = function (options) {\n    options = $.extend({}, this.getDefaults(), this.$element.data(), options)\n\n    if (options.delay && typeof options.delay == 'number') {\n      options.delay = {\n        show: options.delay,\n        hide: options.delay\n      }\n    }\n\n    return options\n  }\n\n  Tooltip.prototype.getDelegateOptions = function () {\n    var options  = {}\n    var defaults = this.getDefaults()\n\n    this._options && $.each(this._options, function (key, value) {\n      if (defaults[key] != value) options[key] = value\n    })\n\n    return options\n  }\n\n  Tooltip.prototype.enter = function (obj) {\n    var self = obj instanceof this.constructor ?\n      obj : $(obj.currentTarget).data('bs.' + this.type)\n\n    if (!self) {\n      self = new this.constructor(obj.currentTarget, this.getDelegateOptions())\n      $(obj.currentTarget).data('bs.' + this.type, self)\n    }\n\n    if (obj instanceof $.Event) {\n      self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true\n    }\n\n    if (self.tip().hasClass('in') || self.hoverState == 'in') {\n      self.hoverState = 'in'\n      return\n    }\n\n    clearTimeout(self.timeout)\n\n    self.hoverState = 'in'\n\n    if (!self.options.delay || !self.options.delay.show) return self.show()\n\n    self.timeout = setTimeout(function () {\n      if (self.hoverState == 'in') self.show()\n    }, self.options.delay.show)\n  }\n\n  Tooltip.prototype.isInStateTrue = function () {\n    for (var key in this.inState) {\n      if (this.inState[key]) return true\n    }\n\n    return false\n  }\n\n  Tooltip.prototype.leave = function (obj) {\n    var self = obj instanceof this.constructor ?\n      obj : $(obj.currentTarget).data('bs.' + this.type)\n\n    if (!self) {\n      self = new this.constructor(obj.currentTarget, this.getDelegateOptions())\n      $(obj.currentTarget).data('bs.' + this.type, self)\n    }\n\n    if (obj instanceof $.Event) {\n      self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false\n    }\n\n    if (self.isInStateTrue()) return\n\n    clearTimeout(self.timeout)\n\n    self.hoverState = 'out'\n\n    if (!self.options.delay || !self.options.delay.hide) return self.hide()\n\n    self.timeout = setTimeout(function () {\n      if (self.hoverState == 'out') self.hide()\n    }, self.options.delay.hide)\n  }\n\n  Tooltip.prototype.show = function () {\n    var e = $.Event('show.bs.' + this.type)\n\n    if (this.hasContent() && this.enabled) {\n      this.$element.trigger(e)\n\n      var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0])\n      if (e.isDefaultPrevented() || !inDom) return\n      var that = this\n\n      var $tip = this.tip()\n\n      var tipId = this.getUID(this.type)\n\n      this.setContent()\n      $tip.attr('id', tipId)\n      this.$element.attr('aria-describedby', tipId)\n\n      if (this.options.animation) $tip.addClass('fade')\n\n      var placement = typeof this.options.placement == 'function' ?\n        this.options.placement.call(this, $tip[0], this.$element[0]) :\n        this.options.placement\n\n      var autoToken = /\\s?auto?\\s?/i\n      var autoPlace = autoToken.test(placement)\n      if (autoPlace) placement = placement.replace(autoToken, '') || 'top'\n\n      $tip\n        .detach()\n        .css({ top: 0, left: 0, display: 'block' })\n        .addClass(placement)\n        .data('bs.' + this.type, this)\n\n      this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)\n      this.$element.trigger('inserted.bs.' + this.type)\n\n      var pos          = this.getPosition()\n      var actualWidth  = $tip[0].offsetWidth\n      var actualHeight = $tip[0].offsetHeight\n\n      if (autoPlace) {\n        var orgPlacement = placement\n        var viewportDim = this.getPosition(this.$viewport)\n\n        placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top'    :\n                    placement == 'top'    && pos.top    - actualHeight < viewportDim.top    ? 'bottom' :\n                    placement == 'right'  && pos.right  + actualWidth  > viewportDim.width  ? 'left'   :\n                    placement == 'left'   && pos.left   - actualWidth  < viewportDim.left   ? 'right'  :\n                    placement\n\n        $tip\n          .removeClass(orgPlacement)\n          .addClass(placement)\n      }\n\n      var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)\n\n      this.applyPlacement(calculatedOffset, placement)\n\n      var complete = function () {\n        var prevHoverState = that.hoverState\n        that.$element.trigger('shown.bs.' + that.type)\n        that.hoverState = null\n\n        if (prevHoverState == 'out') that.leave(that)\n      }\n\n      $.support.transition && this.$tip.hasClass('fade') ?\n        $tip\n          .one('bsTransitionEnd', complete)\n          .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :\n        complete()\n    }\n  }\n\n  Tooltip.prototype.applyPlacement = function (offset, placement) {\n    var $tip   = this.tip()\n    var width  = $tip[0].offsetWidth\n    var height = $tip[0].offsetHeight\n\n    // manually read margins because getBoundingClientRect includes difference\n    var marginTop = parseInt($tip.css('margin-top'), 10)\n    var marginLeft = parseInt($tip.css('margin-left'), 10)\n\n    // we must check for NaN for ie 8/9\n    if (isNaN(marginTop))  marginTop  = 0\n    if (isNaN(marginLeft)) marginLeft = 0\n\n    offset.top  += marginTop\n    offset.left += marginLeft\n\n    // $.fn.offset doesn't round pixel values\n    // so we use setOffset directly with our own function B-0\n    $.offset.setOffset($tip[0], $.extend({\n      using: function (props) {\n        $tip.css({\n          top: Math.round(props.top),\n          left: Math.round(props.left)\n        })\n      }\n    }, offset), 0)\n\n    $tip.addClass('in')\n\n    // check to see if placing tip in new offset caused the tip to resize itself\n    var actualWidth  = $tip[0].offsetWidth\n    var actualHeight = $tip[0].offsetHeight\n\n    if (placement == 'top' && actualHeight != height) {\n      offset.top = offset.top + height - actualHeight\n    }\n\n    var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight)\n\n    if (delta.left) offset.left += delta.left\n    else offset.top += delta.top\n\n    var isVertical          = /top|bottom/.test(placement)\n    var arrowDelta          = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight\n    var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight'\n\n    $tip.offset(offset)\n    this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical)\n  }\n\n  Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) {\n    this.arrow()\n      .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%')\n      .css(isVertical ? 'top' : 'left', '')\n  }\n\n  Tooltip.prototype.setContent = function () {\n    var $tip  = this.tip()\n    var title = this.getTitle()\n\n    $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)\n    $tip.removeClass('fade in top bottom left right')\n  }\n\n  Tooltip.prototype.hide = function (callback) {\n    var that = this\n    var $tip = $(this.$tip)\n    var e    = $.Event('hide.bs.' + this.type)\n\n    function complete() {\n      if (that.hoverState != 'in') $tip.detach()\n      if (that.$element) { // TODO: Check whether guarding this code with this `if` is really necessary.\n        that.$element\n          .removeAttr('aria-describedby')\n          .trigger('hidden.bs.' + that.type)\n      }\n      callback && callback()\n    }\n\n    this.$element.trigger(e)\n\n    if (e.isDefaultPrevented()) return\n\n    $tip.removeClass('in')\n\n    $.support.transition && $tip.hasClass('fade') ?\n      $tip\n        .one('bsTransitionEnd', complete)\n        .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :\n      complete()\n\n    this.hoverState = null\n\n    return this\n  }\n\n  Tooltip.prototype.fixTitle = function () {\n    var $e = this.$element\n    if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') {\n      $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')\n    }\n  }\n\n  Tooltip.prototype.hasContent = function () {\n    return this.getTitle()\n  }\n\n  Tooltip.prototype.getPosition = function ($element) {\n    $element   = $element || this.$element\n\n    var el     = $element[0]\n    var isBody = el.tagName == 'BODY'\n\n    var elRect    = el.getBoundingClientRect()\n    if (elRect.width == null) {\n      // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093\n      elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top })\n    }\n    var isSvg = window.SVGElement && el instanceof window.SVGElement\n    // Avoid using $.offset() on SVGs since it gives incorrect results in jQuery 3.\n    // See https://github.com/twbs/bootstrap/issues/20280\n    var elOffset  = isBody ? { top: 0, left: 0 } : (isSvg ? null : $element.offset())\n    var scroll    = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }\n    var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null\n\n    return $.extend({}, elRect, scroll, outerDims, elOffset)\n  }\n\n  Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {\n    return placement == 'bottom' ? { top: pos.top + pos.height,   left: pos.left + pos.width / 2 - actualWidth / 2 } :\n           placement == 'top'    ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :\n           placement == 'left'   ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :\n        /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }\n\n  }\n\n  Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) {\n    var delta = { top: 0, left: 0 }\n    if (!this.$viewport) return delta\n\n    var viewportPadding = this.options.viewport && this.options.viewport.padding || 0\n    var viewportDimensions = this.getPosition(this.$viewport)\n\n    if (/right|left/.test(placement)) {\n      var topEdgeOffset    = pos.top - viewportPadding - viewportDimensions.scroll\n      var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight\n      if (topEdgeOffset < viewportDimensions.top) { // top overflow\n        delta.top = viewportDimensions.top - topEdgeOffset\n      } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow\n        delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset\n      }\n    } else {\n      var leftEdgeOffset  = pos.left - viewportPadding\n      var rightEdgeOffset = pos.left + viewportPadding + actualWidth\n      if (leftEdgeOffset < viewportDimensions.left) { // left overflow\n        delta.left = viewportDimensions.left - leftEdgeOffset\n      } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow\n        delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset\n      }\n    }\n\n    return delta\n  }\n\n  Tooltip.prototype.getTitle = function () {\n    var title\n    var $e = this.$element\n    var o  = this.options\n\n    title = $e.attr('data-original-title')\n      || (typeof o.title == 'function' ? o.title.call($e[0]) :  o.title)\n\n    return title\n  }\n\n  Tooltip.prototype.getUID = function (prefix) {\n    do prefix += ~~(Math.random() * 1000000)\n    while (document.getElementById(prefix))\n    return prefix\n  }\n\n  Tooltip.prototype.tip = function () {\n    if (!this.$tip) {\n      this.$tip = $(this.options.template)\n      if (this.$tip.length != 1) {\n        throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!')\n      }\n    }\n    return this.$tip\n  }\n\n  Tooltip.prototype.arrow = function () {\n    return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow'))\n  }\n\n  Tooltip.prototype.enable = function () {\n    this.enabled = true\n  }\n\n  Tooltip.prototype.disable = function () {\n    this.enabled = false\n  }\n\n  Tooltip.prototype.toggleEnabled = function () {\n    this.enabled = !this.enabled\n  }\n\n  Tooltip.prototype.toggle = function (e) {\n    var self = this\n    if (e) {\n      self = $(e.currentTarget).data('bs.' + this.type)\n      if (!self) {\n        self = new this.constructor(e.currentTarget, this.getDelegateOptions())\n        $(e.currentTarget).data('bs.' + this.type, self)\n      }\n    }\n\n    if (e) {\n      self.inState.click = !self.inState.click\n      if (self.isInStateTrue()) self.enter(self)\n      else self.leave(self)\n    } else {\n      self.tip().hasClass('in') ? self.leave(self) : self.enter(self)\n    }\n  }\n\n  Tooltip.prototype.destroy = function () {\n    var that = this\n    clearTimeout(this.timeout)\n    this.hide(function () {\n      that.$element.off('.' + that.type).removeData('bs.' + that.type)\n      if (that.$tip) {\n        that.$tip.detach()\n      }\n      that.$tip = null\n      that.$arrow = null\n      that.$viewport = null\n      that.$element = null\n    })\n  }\n\n\n  // TOOLTIP PLUGIN DEFINITION\n  // =========================\n\n  function Plugin(option) {\n    return this.each(function () {\n      var $this   = $(this)\n      var data    = $this.data('bs.tooltip')\n      var options = typeof option == 'object' && option\n\n      if (!data && /destroy|hide/.test(option)) return\n      if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))\n      if (typeof option == 'string') data[option]()\n    })\n  }\n\n  var old = $.fn.tooltip\n\n  $.fn.tooltip             = Plugin\n  $.fn.tooltip.Constructor = Tooltip\n\n\n  // TOOLTIP NO CONFLICT\n  // ===================\n\n  $.fn.tooltip.noConflict = function () {\n    $.fn.tooltip = old\n    return this\n  }\n\n}(jQuery);\n\n/* ========================================================================\n * Bootstrap: popover.js v3.3.7\n * http://getbootstrap.com/javascript/#popovers\n * ========================================================================\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * ======================================================================== */\n\n\n+function ($) {\n  'use strict';\n\n  // POPOVER PUBLIC CLASS DEFINITION\n  // ===============================\n\n  var Popover = function (element, options) {\n    this.init('popover', element, options)\n  }\n\n  if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js')\n\n  Popover.VERSION  = '3.3.7'\n\n  Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, {\n    placement: 'right',\n    trigger: 'click',\n    content: '',\n    template: '<div class=\"popover\" role=\"tooltip\"><div class=\"arrow\"></div><h3 class=\"popover-title\"></h3><div class=\"popover-content\"></div></div>'\n  })\n\n\n  // NOTE: POPOVER EXTENDS tooltip.js\n  // ================================\n\n  Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype)\n\n  Popover.prototype.constructor = Popover\n\n  Popover.prototype.getDefaults = function () {\n    return Popover.DEFAULTS\n  }\n\n  Popover.prototype.setContent = function () {\n    var $tip    = this.tip()\n    var title   = this.getTitle()\n    var content = this.getContent()\n\n    $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)\n    $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events\n      this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text'\n    ](content)\n\n    $tip.removeClass('fade top bottom left right in')\n\n    // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do\n    // this manually by checking the contents.\n    if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide()\n  }\n\n  Popover.prototype.hasContent = function () {\n    return this.getTitle() || this.getContent()\n  }\n\n  Popover.prototype.getContent = function () {\n    var $e = this.$element\n    var o  = this.options\n\n    return $e.attr('data-content')\n      || (typeof o.content == 'function' ?\n            o.content.call($e[0]) :\n            o.content)\n  }\n\n  Popover.prototype.arrow = function () {\n    return (this.$arrow = this.$arrow || this.tip().find('.arrow'))\n  }\n\n\n  // POPOVER PLUGIN DEFINITION\n  // =========================\n\n  function Plugin(option) {\n    return this.each(function () {\n      var $this   = $(this)\n      var data    = $this.data('bs.popover')\n      var options = typeof option == 'object' && option\n\n      if (!data && /destroy|hide/.test(option)) return\n      if (!data) $this.data('bs.popover', (data = new Popover(this, options)))\n      if (typeof option == 'string') data[option]()\n    })\n  }\n\n  var old = $.fn.popover\n\n  $.fn.popover             = Plugin\n  $.fn.popover.Constructor = Popover\n\n\n  // POPOVER NO CONFLICT\n  // ===================\n\n  $.fn.popover.noConflict = function () {\n    $.fn.popover = old\n    return this\n  }\n\n}(jQuery);\n\n/* ========================================================================\n * Bootstrap: scrollspy.js v3.3.7\n * http://getbootstrap.com/javascript/#scrollspy\n * ========================================================================\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * ======================================================================== */\n\n\n+function ($) {\n  'use strict';\n\n  // SCROLLSPY CLASS DEFINITION\n  // ==========================\n\n  function ScrollSpy(element, options) {\n    this.$body          = $(document.body)\n    this.$scrollElement = $(element).is(document.body) ? $(window) : $(element)\n    this.options        = $.extend({}, ScrollSpy.DEFAULTS, options)\n    this.selector       = (this.options.target || '') + ' .nav li > a'\n    this.offsets        = []\n    this.targets        = []\n    this.activeTarget   = null\n    this.scrollHeight   = 0\n\n    this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this))\n    this.refresh()\n    this.process()\n  }\n\n  ScrollSpy.VERSION  = '3.3.7'\n\n  ScrollSpy.DEFAULTS = {\n    offset: 10\n  }\n\n  ScrollSpy.prototype.getScrollHeight = function () {\n    return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight)\n  }\n\n  ScrollSpy.prototype.refresh = function () {\n    var that          = this\n    var offsetMethod  = 'offset'\n    var offsetBase    = 0\n\n    this.offsets      = []\n    this.targets      = []\n    this.scrollHeight = this.getScrollHeight()\n\n    if (!$.isWindow(this.$scrollElement[0])) {\n      offsetMethod = 'position'\n      offsetBase   = this.$scrollElement.scrollTop()\n    }\n\n    this.$body\n      .find(this.selector)\n      .map(function () {\n        var $el   = $(this)\n        var href  = $el.data('target') || $el.attr('href')\n        var $href = /^#./.test(href) && $(href)\n\n        return ($href\n          && $href.length\n          && $href.is(':visible')\n          && [[$href[offsetMethod]().top + offsetBase, href]]) || null\n      })\n      .sort(function (a, b) { return a[0] - b[0] })\n      .each(function () {\n        that.offsets.push(this[0])\n        that.targets.push(this[1])\n      })\n  }\n\n  ScrollSpy.prototype.process = function () {\n    var scrollTop    = this.$scrollElement.scrollTop() + this.options.offset\n    var scrollHeight = this.getScrollHeight()\n    var maxScroll    = this.options.offset + scrollHeight - this.$scrollElement.height()\n    var offsets      = this.offsets\n    var targets      = this.targets\n    var activeTarget = this.activeTarget\n    var i\n\n    if (this.scrollHeight != scrollHeight) {\n      this.refresh()\n    }\n\n    if (scrollTop >= maxScroll) {\n      return activeTarget != (i = targets[targets.length - 1]) && this.activate(i)\n    }\n\n    if (activeTarget && scrollTop < offsets[0]) {\n      this.activeTarget = null\n      return this.clear()\n    }\n\n    for (i = offsets.length; i--;) {\n      activeTarget != targets[i]\n        && scrollTop >= offsets[i]\n        && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1])\n        && this.activate(targets[i])\n    }\n  }\n\n  ScrollSpy.prototype.activate = function (target) {\n    this.activeTarget = target\n\n    this.clear()\n\n    var selector = this.selector +\n      '[data-target=\"' + target + '\"],' +\n      this.selector + '[href=\"' + target + '\"]'\n\n    var active = $(selector)\n      .parents('li')\n      .addClass('active')\n\n    if (active.parent('.dropdown-menu').length) {\n      active = active\n        .closest('li.dropdown')\n        .addClass('active')\n    }\n\n    active.trigger('activate.bs.scrollspy')\n  }\n\n  ScrollSpy.prototype.clear = function () {\n    $(this.selector)\n      .parentsUntil(this.options.target, '.active')\n      .removeClass('active')\n  }\n\n\n  // SCROLLSPY PLUGIN DEFINITION\n  // ===========================\n\n  function Plugin(option) {\n    return this.each(function () {\n      var $this   = $(this)\n      var data    = $this.data('bs.scrollspy')\n      var options = typeof option == 'object' && option\n\n      if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options)))\n      if (typeof option == 'string') data[option]()\n    })\n  }\n\n  var old = $.fn.scrollspy\n\n  $.fn.scrollspy             = Plugin\n  $.fn.scrollspy.Constructor = ScrollSpy\n\n\n  // SCROLLSPY NO CONFLICT\n  // =====================\n\n  $.fn.scrollspy.noConflict = function () {\n    $.fn.scrollspy = old\n    return this\n  }\n\n\n  // SCROLLSPY DATA-API\n  // ==================\n\n  $(window).on('load.bs.scrollspy.data-api', function () {\n    $('[data-spy=\"scroll\"]').each(function () {\n      var $spy = $(this)\n      Plugin.call($spy, $spy.data())\n    })\n  })\n\n}(jQuery);\n\n/* ========================================================================\n * Bootstrap: tab.js v3.3.7\n * http://getbootstrap.com/javascript/#tabs\n * ========================================================================\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * ======================================================================== */\n\n\n+function ($) {\n  'use strict';\n\n  // TAB CLASS DEFINITION\n  // ====================\n\n  var Tab = function (element) {\n    // jscs:disable requireDollarBeforejQueryAssignment\n    this.element = $(element)\n    // jscs:enable requireDollarBeforejQueryAssignment\n  }\n\n  Tab.VERSION = '3.3.7'\n\n  Tab.TRANSITION_DURATION = 150\n\n  Tab.prototype.show = function () {\n    var $this    = this.element\n    var $ul      = $this.closest('ul:not(.dropdown-menu)')\n    var selector = $this.data('target')\n\n    if (!selector) {\n      selector = $this.attr('href')\n      selector = selector && selector.replace(/.*(?=#[^\\s]*$)/, '') // strip for ie7\n    }\n\n    if ($this.parent('li').hasClass('active')) return\n\n    var $previous = $ul.find('.active:last a')\n    var hideEvent = $.Event('hide.bs.tab', {\n      relatedTarget: $this[0]\n    })\n    var showEvent = $.Event('show.bs.tab', {\n      relatedTarget: $previous[0]\n    })\n\n    $previous.trigger(hideEvent)\n    $this.trigger(showEvent)\n\n    if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return\n\n    var $target = $(selector)\n\n    this.activate($this.closest('li'), $ul)\n    this.activate($target, $target.parent(), function () {\n      $previous.trigger({\n        type: 'hidden.bs.tab',\n        relatedTarget: $this[0]\n      })\n      $this.trigger({\n        type: 'shown.bs.tab',\n        relatedTarget: $previous[0]\n      })\n    })\n  }\n\n  Tab.prototype.activate = function (element, container, callback) {\n    var $active    = container.find('> .active')\n    var transition = callback\n      && $.support.transition\n      && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length)\n\n    function next() {\n      $active\n        .removeClass('active')\n        .find('> .dropdown-menu > .active')\n          .removeClass('active')\n        .end()\n        .find('[data-toggle=\"tab\"]')\n          .attr('aria-expanded', false)\n\n      element\n        .addClass('active')\n        .find('[data-toggle=\"tab\"]')\n          .attr('aria-expanded', true)\n\n      if (transition) {\n        element[0].offsetWidth // reflow for transition\n        element.addClass('in')\n      } else {\n        element.removeClass('fade')\n      }\n\n      if (element.parent('.dropdown-menu').length) {\n        element\n          .closest('li.dropdown')\n            .addClass('active')\n          .end()\n          .find('[data-toggle=\"tab\"]')\n            .attr('aria-expanded', true)\n      }\n\n      callback && callback()\n    }\n\n    $active.length && transition ?\n      $active\n        .one('bsTransitionEnd', next)\n        .emulateTransitionEnd(Tab.TRANSITION_DURATION) :\n      next()\n\n    $active.removeClass('in')\n  }\n\n\n  // TAB PLUGIN DEFINITION\n  // =====================\n\n  function Plugin(option) {\n    return this.each(function () {\n      var $this = $(this)\n      var data  = $this.data('bs.tab')\n\n      if (!data) $this.data('bs.tab', (data = new Tab(this)))\n      if (typeof option == 'string') data[option]()\n    })\n  }\n\n  var old = $.fn.tab\n\n  $.fn.tab             = Plugin\n  $.fn.tab.Constructor = Tab\n\n\n  // TAB NO CONFLICT\n  // ===============\n\n  $.fn.tab.noConflict = function () {\n    $.fn.tab = old\n    return this\n  }\n\n\n  // TAB DATA-API\n  // ============\n\n  var clickHandler = function (e) {\n    e.preventDefault()\n    Plugin.call($(this), 'show')\n  }\n\n  $(document)\n    .on('click.bs.tab.data-api', '[data-toggle=\"tab\"]', clickHandler)\n    .on('click.bs.tab.data-api', '[data-toggle=\"pill\"]', clickHandler)\n\n}(jQuery);\n\n/* ========================================================================\n * Bootstrap: affix.js v3.3.7\n * http://getbootstrap.com/javascript/#affix\n * ========================================================================\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * ======================================================================== */\n\n\n+function ($) {\n  'use strict';\n\n  // AFFIX CLASS DEFINITION\n  // ======================\n\n  var Affix = function (element, options) {\n    this.options = $.extend({}, Affix.DEFAULTS, options)\n\n    this.$target = $(this.options.target)\n      .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this))\n      .on('click.bs.affix.data-api',  $.proxy(this.checkPositionWithEventLoop, this))\n\n    this.$element     = $(element)\n    this.affixed      = null\n    this.unpin        = null\n    this.pinnedOffset = null\n\n    this.checkPosition()\n  }\n\n  Affix.VERSION  = '3.3.7'\n\n  Affix.RESET    = 'affix affix-top affix-bottom'\n\n  Affix.DEFAULTS = {\n    offset: 0,\n    target: window\n  }\n\n  Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) {\n    var scrollTop    = this.$target.scrollTop()\n    var position     = this.$element.offset()\n    var targetHeight = this.$target.height()\n\n    if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false\n\n    if (this.affixed == 'bottom') {\n      if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom'\n      return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom'\n    }\n\n    var initializing   = this.affixed == null\n    var colliderTop    = initializing ? scrollTop : position.top\n    var colliderHeight = initializing ? targetHeight : height\n\n    if (offsetTop != null && scrollTop <= offsetTop) return 'top'\n    if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom'\n\n    return false\n  }\n\n  Affix.prototype.getPinnedOffset = function () {\n    if (this.pinnedOffset) return this.pinnedOffset\n    this.$element.removeClass(Affix.RESET).addClass('affix')\n    var scrollTop = this.$target.scrollTop()\n    var position  = this.$element.offset()\n    return (this.pinnedOffset = position.top - scrollTop)\n  }\n\n  Affix.prototype.checkPositionWithEventLoop = function () {\n    setTimeout($.proxy(this.checkPosition, this), 1)\n  }\n\n  Affix.prototype.checkPosition = function () {\n    if (!this.$element.is(':visible')) return\n\n    var height       = this.$element.height()\n    var offset       = this.options.offset\n    var offsetTop    = offset.top\n    var offsetBottom = offset.bottom\n    var scrollHeight = Math.max($(document).height(), $(document.body).height())\n\n    if (typeof offset != 'object')         offsetBottom = offsetTop = offset\n    if (typeof offsetTop == 'function')    offsetTop    = offset.top(this.$element)\n    if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element)\n\n    var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom)\n\n    if (this.affixed != affix) {\n      if (this.unpin != null) this.$element.css('top', '')\n\n      var affixType = 'affix' + (affix ? '-' + affix : '')\n      var e         = $.Event(affixType + '.bs.affix')\n\n      this.$element.trigger(e)\n\n      if (e.isDefaultPrevented()) return\n\n      this.affixed = affix\n      this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null\n\n      this.$element\n        .removeClass(Affix.RESET)\n        .addClass(affixType)\n        .trigger(affixType.replace('affix', 'affixed') + '.bs.affix')\n    }\n\n    if (affix == 'bottom') {\n      this.$element.offset({\n        top: scrollHeight - height - offsetBottom\n      })\n    }\n  }\n\n\n  // AFFIX PLUGIN DEFINITION\n  // =======================\n\n  function Plugin(option) {\n    return this.each(function () {\n      var $this   = $(this)\n      var data    = $this.data('bs.affix')\n      var options = typeof option == 'object' && option\n\n      if (!data) $this.data('bs.affix', (data = new Affix(this, options)))\n      if (typeof option == 'string') data[option]()\n    })\n  }\n\n  var old = $.fn.affix\n\n  $.fn.affix             = Plugin\n  $.fn.affix.Constructor = Affix\n\n\n  // AFFIX NO CONFLICT\n  // =================\n\n  $.fn.affix.noConflict = function () {\n    $.fn.affix = old\n    return this\n  }\n\n\n  // AFFIX DATA-API\n  // ==============\n\n  $(window).on('load', function () {\n    $('[data-spy=\"affix\"]').each(function () {\n      var $spy = $(this)\n      var data = $spy.data()\n\n      data.offset = data.offset || {}\n\n      if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom\n      if (data.offsetTop    != null) data.offset.top    = data.offsetTop\n\n      Plugin.call($spy, data)\n    })\n  })\n\n}(jQuery);\n"
  },
  {
    "path": "gui/static/js/carriergroups.js",
    "content": ";(function(window, document) {\n  'use strict';\n\n  // throw an error if required functions not defined\n  if (typeof validateFields === \"undefined\") {\n    throw new Error(\"validateFields() is required and is not defined\");\n  }\n  if (typeof showNotification === \"undefined\") {\n    throw new Error(\"showNotification() is required and is not defined\");\n  }\n  if (typeof toggleElemDisabled === \"undefined\") {\n    throw new Error(\"toggleElemDisabled() is required and is not defined\");\n  }\n\n  // throw an error if required globals not defined\n  if (typeof API_BASE_URL === \"undefined\") {\n    throw new Error(\"API_BASE_URL is required and is not defined\");\n  }\n  // throw an error if required globals not defined\n  if (typeof GUI_BASE_URL === \"undefined\") {\n    throw new Error(\"GUI_BASE_URL is required and is not defined\");\n  }\n\n  // globals\n  var gwgroupid;\n  var gwgroup_table = $('').DataTable();\n  var plugin_name_sel = $('#plugin_name');\n  var plugin_name_regex = new RegExp('^([a-zA-Z0-9.-])+$');\n\n  // when using twilio plugin we need to do some extra validation for\n  function validatePluginData(fields) {\n    if (plugin_name_sel.val() === \"Twilio\") {\n      if (!plugin_name_regex.test(fields.get('name').val())) {\n        return {\n          result: false,\n          err_node: fields.get('name'),\n          err_msg: \"Endpoint Group Name must be a valid hostname for the Twilio plugin\"\n        };\n      }\n      if (fields.get('plugin_account_sid').val() === '') {\n        return {\n          result: false,\n          err_node: fields.get('plugin_account_sid'),\n          err_msg: \"Account SID is required for the Twilio plugin\"\n        };\n      }\n      if (fields.get('plugin_account_token').val() === '') {\n        return {\n          result: false,\n          err_node: fields.get('plugin_account_token'),\n          err_msg: \"Account Token is required for the Twilio plugin\"\n        };\n      }\n    }\n\n    return {\n      result: true\n    };\n  }\n\n  // Add EndpointGroup\n  function addCarrierGroup(action) {\n    var selector, modal_body, url;\n\n    // The default action is a POST (creating a new EndpointGroup)\n    if (typeof action === \"undefined\") {\n      action = \"POST\";\n    }\n\n    // set the query parameters\n    if (action === \"POST\") {\n      action = \"POST\";\n      selector = \"#add-group\";\n      modal_body = $(selector + ' .modal-body');\n      url = API_BASE_URL + \"carriergroups\";\n    }\n    else if (action === \"PUT\") {\n      selector = \"#edit-group\";\n      modal_body = $(selector + ' .modal-body');\n      gwgroupid = modal_body.find(\".gwgroup\").val();\n      url = API_BASE_URL + \"carriergroups/\" + gwgroupid;\n    }\n    else {\n      throw new Error(\"addGatewayGroup(): action must be either POST or PUT\");\n    }\n\n    var requestPayload = {};\n\n    requestPayload.name = modal_body.find('.name').val();\n    if (action === \"PUT\") {\n      requestPayload.name = modal_body.find('.new_name').val();\n    }\n\n    if (plugin_name_sel.val() === \"Twilio\") {\n      requestPayload.plugin = {\n        name: plugin_name_sel.val(),\n        account_sid: modal_body.find(\".plugin_account_sid\").val(),\n        account_token: modal_body.find(\".plugin_account_token\").val(),\n      };\n    }\n\n    var auth = {};\n    if (action === \"POST\") {\n      auth.type = modal_body.find(\".authtype\").val();\n      if (auth.type == \"userpwd\") {\n        auth.pass = modal_body.find(\".auth_password\").val();\n      }\n    }\n    else if (action === \"PUT\") {\n      auth.type = modal_body.find(\".authtype\").val();\n      if (auth.type == \"userpwd\") {\n        auth.pass = modal_body.find(\".auth_password\").val();\n      }\n    }\n    auth.r_username = modal_body.find(\".r_username\").val();\n    auth.auth_username = modal_body.find(\".auth_username\").val();\n    auth.auth_password = auth.pass\n    auth.auth_domain = modal_body.find(\".auth_domain\").val();\n    auth.auth_proxy = modal_body.find(\".auth_proxy\").val();\n    requestPayload.auth = auth;\n\n    requestPayload.lb_enabled = modal_body.find('.lb_enabled').val();\n    requestPayload.strip = modal_body.find(\".strip\").val();\n    requestPayload.prefix = modal_body.find(\".prefix\").val();\n\n    // Put into JSON Message and send over\n    $.ajax({\n      type: action,\n      url: url,\n      dataType: \"json\",\n      contentType: \"application/json; charset=utf-8\",\n      data: JSON.stringify(requestPayload),\n      success: function(response, textStatus, jqXHR) {\n        var btn;\n        var gwgroupid_int = response.data[0].gwgroupid;\n\n        // Update the Add Button and the table\n        if (action === \"POST\") {\n          btn = $('#add .modal-footer').find('#addButton');\n          btn.removeClass(\"btn-primary\");\n        }\n        else {\n          btn = $('#edit .modal-footer').find('#updateButton');\n          btn.removeClass(\"btn-warning\");\n        }\n\n        btn.addClass(\"btn-success\");\n        btn.html(\"<span class='glyphicon glyphicon-check'></span> Saved!\");\n        btn.attr(\"disabled\", true);\n\n        if (action === \"POST\") {\n          gwgroup_table.row.add({\n            \"name\": requestPayload.name,\n            \"gwgroupid\": gwgroupid_int\n          }).draw();\n          location.reload(true);\n        }\n        else {\n          /*\n          gwgroup_table.row(function(idx, data, node) {\n            return data.gwgroupid === gwgroupid_int;\n          }).data({\n            \"name\": requestPayload.name,\n            \"gwgroupid\": gwgroupid_int\n          }).draw(); */\n          location.reload(true);\n        }\n      }\n    })\n  }\n\n  function updateCarrierGroup() {\n    addCarrierGroup(\"PUT\");\n  }\n\n  /* validate fields before submitting api request */\n  $('#addButton').click(function(ev) {\n    /* prevent form default submit */\n    ev.preventDefault();\n\n    if (validateFields('#add-group', validatePluginData)) {\n      addCarrierGroup();\n      // hide the modal after 1.5 sec\n      setTimeout(function() {\n        var add_modal = $('#add-group');\n        if (add_modal.is(':visible')) {\n          add_modal.modal('hide');\n        }\n      }, 1500);\n    }\n  });\n\n  /* validate fields before submitting api request */\n  $('#updateGroupButton').click(function(ev) {\n    /* prevent form default submit */\n    ev.preventDefault();\n\n    if (validateFields('#edit-group')) {\n      updateCarrierGroup();\n      // hide the modal after 1.5 sec\n      setTimeout(function() {\n        var edit_modal = $('#edit-group');\n        if (edit_modal.is(':visible')) {\n          edit_modal.modal('hide');\n        }\n      }, 1500);\n    }\n\n    /* prevent page reload */\n    return false;\n  });\n\n  function setCarrierGroupHandlers() {\n    var carriergroups_tbody = $('#carrier-groups tbody');\n\n    $('#carrier-nav').click(function(e) {\n      var target_link = $(e.target);\n\n      /* fix target if we hit an ancestor elem containing it */\n      var target_type = $(e.target).get(0).nodeName.toLowerCase();\n      if (target_type !== \"a\") {\n        /* no routing necessary if these are clicked */\n        if (['div', 'ul'].indexOf(target_type) > -1) {\n          return false;\n        }\n        target_link = target_link.find('a');\n      }\n\n      var other_links = $(e.currentTarget).find('.nav-tabs a').not(target_link);\n      var modal_body = $(e.currentTarget.parentNode);\n\n      /* handle dynamic routes (links) */\n      if (target_link.data(\"type\") === \"link\") {\n        /* add dynamic routes here for each link */\n        if (target_link.attr(\"name\") === \"carriers-link\") {\n          var gwid = modal_body.find('.gwid').val();\n          var gwgroup = modal_body.find('.gwgroup').val();\n\n          if (gwgroup !== undefined) {\n            target_link.attr('href', target_link.attr('href') + \"/group/\" + gwgroup);\n          }\n          else if (gwid !== undefined) {\n            target_link.attr('href', target_link.attr('href') + \"/\" + gwid);\n          }\n        }\n\n        /* add styling to the links */\n        target_link.addClass('current-navlink');\n        $.each(other_links, function(i, elem) {\n          $(elem).removeClass('current-navlink');\n        });\n\n        /* make sure we follow the link after returning */\n        return true;\n      }\n\n      /* handle dynamic modals (using toggles) */\n      e.preventDefault();\n      var modal_toggle_divs = modal_body.children('div[name*=\"toggle\"]');\n      for (var i = 0; i < modal_toggle_divs.length; i++) {\n        if ($(modal_toggle_divs[i]).attr('name') === target_link.attr('name')) {\n          $(modal_toggle_divs[i]).removeClass(\"hidden\");\n        }\n        else {\n          $(modal_toggle_divs[i]).addClass(\"hidden\");\n        }\n      }\n\n      // add styling to the toggles\n      target_link.addClass('current-navlink');\n      $.each(other_links, function(i, elem) {\n        $(elem).removeClass('current-navlink');\n      });\n\n      // make sure we don't follow the link\n      return false;\n    });\n\n    /* listener for plguin dropdown changes */\n    plugin_name_sel.change(function() {\n      var modal_body = $('#add-group .modal-body');\n\n      // TODO: Use the carriergroup plugin architecture to get meta data about the plugin\n      if ($(this).val() === '') {\n        modal_body.find('#plugin_creds').addClass('hidden');\n      }\n      else {\n        modal_body.find('#plugin_creds').removeClass('hidden');\n      }\n    });\n\n    /* listener for load balancing toggle */\n    $('.modal-body .toggle-loadbalancing').change(function() {\n      var modal = $(this).closest('div.modal');\n      var modal_body = modal.find('.modal-body');\n\n      if ($(this).is(\":checked\") || $(this).prop(\"checked\")) {\n        modal_body.find('.lb_enabled').val(1);\n      }\n      else {\n        modal_body.find('.lb_enabled').val(0);\n      }\n    });\n\n    $('#open-CarrierGroupAdd').click(function() {\n      /** Clear out the modal */\n      var modal_body = $('#add-group .modal-body');\n      modal_body.find(\".name\").val('');\n      modal_body.find(\".gwlist\").val('');\n      modal_body.find(\".authtype\").val('');\n      modal_body.find(\".r_username\").val('');\n      modal_body.find(\".auth_username\").val('');\n      modal_body.find(\".auth_password\").val('');\n      modal_body.find(\".auth_domain\").val('');\n      modal_body.find(\".auth_proxy\").val('');\n\n      /* reset plugin selections */\n      plugin_name_sel.val('').change();\n\n      /* reset toggle buttons */\n      modal_body.find(\"input.toggle-loadbalancing\").bootstrapToggle('off');\n\n      // update gwgroup for all modals\n      $('.modal-body').find(\".gwgroup\").each(function() {\n        $(this).val('');\n      });\n\n      /* ip auth enabled by default, Set the radio button to true */\n      modal_body.find('.authtype[data-toggle=\"ip_enabled\"]').trigger('click');\n    });\n\n    carriergroups_tbody.on('click', '#open-Update', function() {\n      var row_index = $(this).parent().parent().parent().index() + 1;\n      var c = document.getElementById('carrier-groups');\n      var gwgroup = $(c).find('tr:eq(' + row_index + ') td:eq(1)').text();\n      var name = $(c).find('tr:eq(' + row_index + ') td:eq(2)').text();\n      var gwlist = $(c).find('tr:eq(' + row_index + ') td:eq(3)').text();\n      var authtype = $(c).find('tr:eq(' + row_index + ') td:eq(4)').text();\n      var r_username = $(c).find('tr:eq(' + row_index + ') td:eq(5)').text();\n      var auth_password = $(c).find('tr:eq(' + row_index + ') td:eq(6)').text();\n      var auth_domain = $(c).find('tr:eq(' + row_index + ') td:eq(7)').text();\n      var auth_username = $(c).find('tr:eq(' + row_index + ') td:eq(8)').text();\n      var auth_proxy = $(c).find('tr:eq(' + row_index + ') td:eq(9)').text();\n      var lb_enabled = $(c).find('tr:eq(' + row_index + ') td:eq(10)').text();\n\n      // grab modals to change\n      var modal_body = $('#edit-group .modal-body');\n      var modal_bodies = $('.modal-body');\n\n      /* update modal fields */\n      modal_body.find(\".name\").val(name);\n      modal_body.find(\".new_name\").val(name);\n      modal_body.find(\".gwlist\").val(gwlist);\n      modal_body.find(\".authtype\").val(authtype);\n      modal_body.find(\".r_username\").val(r_username);\n      modal_body.find(\".auth_password\").val(auth_password);\n      modal_body.find(\".auth_domain\").val(auth_domain);\n      modal_body.find(\".auth_username\").val(auth_username);\n      modal_body.find(\".auth_proxy\").val(auth_proxy);\n\n      /* update toggle buttons */\n      if (lb_enabled === '1') {\n        modal_body.find(\"input.toggle-loadbalancing\").bootstrapToggle('on');\n      }\n      else {\n        modal_body.find(\"input.toggle-loadbalancing\").bootstrapToggle('off');\n      }\n\n      // update gwgroup for all modals\n      modal_bodies.find(\".gwgroup\").each(function() {\n        $(this).val(gwgroup);\n      });\n\n      if (authtype !== \"\") {\n        /* userpwd auth enabled, Set the radio button to true */\n        modal_body.find('.authtype[data-toggle=\"userpwd_enabled\"]').trigger('click');\n      }\n      else {\n        /* ip auth enabled, Set the radio button to true */\n        modal_body.find('.authtype[data-toggle=\"ip_enabled\"]').trigger('click');\n      }\n\n      /* only show carriers from current group */\n      $('#carriers > tbody > tr').each(function() {\n        if (gwlist.split(',').indexOf($(this).data('gwid').toString()) > -1) {\n          $(this).removeClass('hidden');\n        }\n        else {\n          $(this).addClass('hidden');\n        }\n      });\n\n      /* start carrier-nav on first tab */\n      $('#carrier-nav > .nav-tabs').find('a').first().trigger('click');\n    });\n\n    carriergroups_tbody.on('click', '#open-Delete', function() {\n      var row_index = $(this).parent().parent().parent().index() + 1;\n      var c = document.getElementById('carrier-groups');\n      var gwgroup = $(c).find('tr:eq(' + row_index + ') td:eq(1)').text();\n      var name = $(c).find('tr:eq(' + row_index + ') td:eq(2)').text();\n      var gwlist = $(c).find('tr:eq(' + row_index + ') td:eq(3)').text();\n\n      /* update modal fields */\n      var modal_body = $('#delete-group .modal-body');\n      modal_body.find(\".gwgroup\").val(gwgroup);\n      modal_body.find(\".name\").val(name);\n      modal_body.find(\".gwlist\").val(gwlist);\n    });\n  }\n\n  function setCarrierHandlers() {\n    var carriers_tbody = $('#carriers tbody');\n\n    $('#open-CarrierAdd').on('click', function() {\n      /** Clear out the modal */\n      var modal_body = $('#add .modal-body');\n      modal_body.find(\".gwid\").val('');\n      modal_body.find(\".name\").val('');\n      modal_body.find(\".ip_addr\").val('');\n      modal_body.find(\".strip\").val('');\n      modal_body.find(\".prefix\").val('');\n      modal_body.find(\".rweight\").val('');\n\n\n      /* make sure ip_addr not disabled */\n      modal_body.find('.ip_addr').prop('disabled', false);\n    });\n\n\n    carriers_tbody.on('click', '#open-Update', function() {\n      var row_index = $(this).parent().parent().parent().index() + 1;\n      var c = document.getElementById('carriers');\n      var gwid = $(c).find('tr:eq(' + row_index + ') td:eq(1)').text();\n      var name = $(c).find('tr:eq(' + row_index + ') td:eq(2)').text();\n      var ip_addr = $(c).find('tr:eq(' + row_index + ') td:eq(3)').text();\n      var strip = $(c).find('tr:eq(' + row_index + ') td:eq(4)').text();\n      var prefix = $(c).find('tr:eq(' + row_index + ') td:eq(5)').text();\n      var rweight = $(c).find('tr:eq(' + row_index + ') td:eq(6)').text();\n\n      /** Clear out the modal */\n      var modal_body = $('#edit .modal-body');\n      modal_body.find(\".gwid\").val('');\n      modal_body.find(\".name\").val('');\n      modal_body.find(\".ip_addr\").val('');\n      modal_body.find(\".strip\").val('');\n      modal_body.find(\".prefix\").val('');\n      modal_body.find(\".rweight\").val('');\n\n      /* update modal fields */\n      modal_body.find(\".gwid\").val(gwid);\n      modal_body.find(\".name\").val(name);\n      modal_body.find(\".ip_addr\").val(ip_addr);\n      modal_body.find(\".strip\").val(strip);\n      modal_body.find(\".prefix\").val(prefix);\n      modal_body.find(\".rweight\").val(rweight);\n    });\n\n    carriers_tbody.on('click', '#open-Delete', function() {\n      var row_index = $(this).parent().parent().parent().index() + 1;\n      var c = document.getElementById('carriers');\n      var gwid = $(c).find('tr:eq(' + row_index + ') td:eq(1)').text();\n      var name = $(c).find('tr:eq(' + row_index + ') td:eq(2)').text();\n      var ip_addr = $(c).find('tr:eq(' + row_index + ') td:eq(3)').text();\n      var related_rules = JSON.parse($(c).find('tr:eq(' + row_index + ') td:eq(7)').text());\n\n      var modal_body = $('#delete .modal-body');\n\n      /* remove previous rules if they were created */\n      modal_body.find('div.alert.alert-warning').remove();\n\n      /* check if related dr_rules exist */\n      if (Object.keys(related_rules).length > 0) {\n        /* create an alert and append it to the DOM */\n        var html_string = '<div class=\"alert alert-warning centered\" role=\"alert\">' +\n          '<h4>Deleting this rule will cause the following Global Outbound Routes to be deleted</h4>' +\n          '<hr>' + '<div class=\"table-responsive\">' +\n          '<table class=\"table table-centered\" style=\"margin-bottom: 0;\">' +\n          '<thead><tr><th>RULE ID</th><th>NAME</th></tr></thead><tbody>';\n        for (var key in related_rules) {\n          html_string += '<tr><td>' + key + '</td><td>' + related_rules[key] + '</td></tr>';\n        }\n        html_string += '</tbody></table></div></div>';\n        /* let jquery parse the string as html */\n        var html_nodes = jQuery.parseHTML(html_string);\n        modal_body.find(\"div.alert.alert-danger\").after(html_nodes);\n      }\n\n      /* update modal fields */\n      modal_body.find(\".gwid\").val(gwid);\n      modal_body.find(\".name\").val(name);\n      modal_body.find(\".ip_addr\").val(ip_addr);\n      modal_body.find(\".related_rules\").val(JSON.stringify(related_rules));\n    });\n  }\n\n  function setFormHandler(selector, successCallback) {\n    $(selector).submit(function(e) {\n      /* prevent form default submit */\n      e.preventDefault();\n      /* store reference to target for callback */\n      var self = this;\n\n      var request_url = $(this).attr(\"action\");\n      var request_method = $(this).attr(\"method\");\n      var form_data = $(this).serialize();\n\n      $.ajax({\n        url: request_url,\n        type: request_method,\n        data: form_data,\n        success: function(data) {\n          successCallback(data, self)\n        },\n        error: function(xhr, msg, err) {\n          return true; // follow redirects for error page\n        },\n        complete: function() {\n          $(e.target).closest('div.modal').modal('hide');\n        }\n      });\n\n      /* make sure we don't reload page */\n      return false;\n    });\n  }\n\n  /* any handlers depending on DOM elements go here */\n  $(document).ready(function() {\n    /* update the carriers table */\n    $.ajax({\n      url: GUI_BASE_URL + \"carriers\",\n      method: 'GET',\n      headers: {\n        'Content-Type': 'text/html,text/css,application/javascript,text/plain,*/*'\n      },\n      success: function(data) {\n        $('#carriers-table').html(data);\n      }\n    });\n\n    /* DataTable init */\n    $('#carrier-groups').DataTable({\n      \"columnDefs\": [\n        {\"orderable\": true, \"targets\": [1, 2, 3]},\n        {\"orderable\": false, \"targets\": [0, 4, 5, 6, 7, 8, 9, 10, 11]},\n      ],\n      \"order\": [[1, 'asc']]\n    });\n\n    /* update view when carriers updated */\n    setFormHandler('.gwform', function(data, target) {\n      $('#carriers-table').html(data);\n\n      var modal = $(target).closest('div.modal');\n      var gwid, gwgroup, td_gwlist, gwlist_arr, gwlist;\n      if (modal.attr('id') === 'add') {\n        gwid = $(data).find('tr.new_gw').data('gwid');\n        gwgroup = modal.find('.gwgroup').val();\n        td_gwlist = $('#carrier-groups').find('tr[data-gwgroup=\"' + gwgroup + '\"] > td.gwlist');\n        gwlist = td_gwlist.text();\n        gwlist_arr = gwlist === '' ? [] : gwlist.split(',');\n        gwlist_arr.push(gwid);\n        gwlist = gwlist_arr.join(',');\n        td_gwlist.text(gwlist);\n      }\n      else if (modal.attr('id') === 'delete') {\n        gwid = modal.find('.gwid').val();\n        gwgroup = modal.find('.gwgroup').val();\n        td_gwlist = $('#carrier-groups').find('tr[data-gwgroup=\"' + gwgroup + '\"] > td.gwlist');\n        gwlist = td_gwlist.text();\n        gwlist_arr = gwlist === '' ? [] : gwlist.split(',');\n        gwlist_arr.splice(gwlist_arr.indexOf(gwid), 1);\n        gwlist = gwlist_arr.join(',');\n        td_gwlist.text(gwlist);\n      }\n    });\n\n    setCarrierGroupHandlers();\n  });\n\n  /* any handlers that MAY rely on async calls put here */\n  $(document).ajaxStop(function() {\n    setCarrierHandlers();\n  });\n\n})(window, document);\n"
  },
  {
    "path": "gui/static/js/cdrs.js",
    "content": ";(function (window, document) {\n  'use strict';\n\n  // throw an error if required globals not defined\n  if (typeof API_BASE_URL === \"undefined\") {\n    throw new Error(\"API_BASE_URL is required and is not defined\");\n  }\n  if (typeof delayedCallback === \"undefined\") {\n    throw new Error(\"delayedCallback() is required and is not defined\");\n  }\n\n  // globals for this script\n  var epgroup_select = $(\"#endpointgroups\");\n  var loading_spinner = $('#loading-spinner');\n  var showallcalls_inp = $('#toggle_completed_calls');\n  var cdr_table = null;\n\n  /**\n   * Show a spinner while loading\n   * @param isLoading {boolean}\n   */\n  function changeLoadingState(isLoading) {\n    if (isLoading) {\n      loading_spinner.removeClass('hidden');\n    }\n    else {\n      loading_spinner.addClass('hidden');\n    }\n  }\n\n  function getFilteredCdrIds() {\n    var value = $('#cdrs').DataTable().columns( { search: 'applied' } ).data()[0];\n    // Check if values were selected using the search  field. \n    if (value != \",\") {\n    \treturn value;\n    }\n    else {\n\t    return '';\n    }\n  }\n\n  function loadCDRDataTable(gwgroupid) {\n    changeLoadingState(true);\n\n    // load CDR data\n    if ($.fn.dataTable.isDataTable(cdr_table)) {\n      // Clear the contents of the table\n      // cdr_table.clear();\n      // cdr_table.draw();\n      cdr_table.ajax.url(API_BASE_URL + \"cdrs/endpointgroups/\" + gwgroupid);\n      cdr_table.ajax.reload();\n    }\n    // datatable init\n    else {\n      cdr_table = $('#cdrs').DataTable({\n        \"pagingType\": \"full_numbers\",\n        \"processing\": false,\n        \"serverSide\": true,\n        \"ajax\": {\n          \"url\": API_BASE_URL + \"cdrs/endpointgroups/\" + gwgroupid,\n          \"data\": function (d) {\n            d.nonCompletedCalls = showallcalls_inp.val() === '1';\n          },\n          \"dataFilter\": function (data) {\n            if (data) {\n              var json = jQuery.parseJSON(data);\n              json.recordsTotal = json.total_rows;\n              json.recordsFiltered = json.filtered_rows;\n              return JSON.stringify(json);\n            }\n\n            return JSON.stringify({\n              data: [],\n              recordsTotal: 0,\n              recordsFiltered: 0\n            });\n          }\n        },\n        \"columns\": [\n          {\"data\": \"cdr_id\", \"orderable\": false},\n          {\"data\": \"call_start_time\"},\n          {\"data\": \"call_duration\"},\n          {\"data\": \"call_direction\"},\n          {\"data\": \"src_gwgroupid\", \"visible\": false, \"searchable\": false},\n          {\"data\": \"src_gwgroupname\"},\n          {\"data\": \"dst_gwgroupid\", \"visible\": false, \"searchable\": false},\n          {\"data\": \"dst_gwgroupname\"},\n          {\"data\": \"src_username\"},\n          {\"data\": \"dst_username\"},\n          {\"data\": \"src_address\"},\n          {\"data\": \"dst_address\"},\n          {\"data\": \"call_id\", \"orderable\": false}\n        ],\n        \"order\": [[1, 'desc']],\n        \"pageLength\": 100\n      });\n\n      // make searchbox less spammy\n      var searchbox = $('#cdrs input[type=\"search\"]');\n      searchbox.unbind();\n      searchbox.bind('input', delayedCallback(\n        function(ev) {\n          cdr_table.search(this.value).draw();\n        }, 500)\n      );\n    }\n\n    changeLoadingState(false);\n  }\n\n  $(document).ready(function () {\n    // get endpoint group data\n    $.ajax({\n      type: \"GET\",\n      url: API_BASE_URL + \"endpointgroups\",\n      dataType: \"json\",\n      contentType: \"application/json; charset=utf-8\",\n      success: function (response, textStatus, jqXHR) {\n        for (var i = 0; i < response.data.length; i++) {\n          epgroup_select.append(\"<option value='\" + response.data[i].gwgroupid + \"'>\" + response.data[i].name + \"</option>\");\n        }\n      }\n    })\n\n    // default is enabled\n    showallcalls_inp.bootstrapToggle('on');\n\n    /* listener for completed calls toggle */\n    showallcalls_inp.change(function() {\n      var self = $(this);\n\n      if (self.is(\":checked\") || self.prop(\"checked\")) {\n        self.val(1);\n      }\n      else {\n        self.val(0);\n      }\n      loadCDRDataTable(epgroup_select.find('option:selected').val());\n    });\n\n    // change table when endpoint group selected\n    epgroup_select.change(function () {\n      loadCDRDataTable(epgroup_select.find('option:selected').val());\n    })\n\n    $('#downloadCDR').click(function () {\n      var gwgroupid = $(\"#endpointgroups\").val();\n      window.location.href = API_BASE_URL + 'cdrs/endpointgroups/' + gwgroupid + '?type=csv&filter=' + getFilteredCdrIds().join(',');\n    });\n\n    \n    // reload table when the refresh button is clicked\n    $('#refreshCDR').click(function () {\n      loadCDRDataTable(epgroup_select.find('option:selected').val());\n    });\n  });\n\n})(window, document);\n"
  },
  {
    "path": "gui/static/js/certificates.js",
    "content": ";(function(window, document) {\n  'use strict';\n\n  // throw an error if required functions not defined\n  if (typeof validateFields === \"undefined\") {\n    throw new Error(\"validateFields() is required and is not defined\");\n  }\n  if (typeof showNotification === \"undefined\") {\n    throw new Error(\"showNotification() is required and is not defined\");\n  }\n  if (typeof toggleElemDisabled === \"undefined\") {\n    throw new Error(\"toggleElemDisabled() is required and is not defined\");\n  }\n\n  // throw an error if required globals not defined\n  if (typeof API_BASE_URL === \"undefined\") {\n    throw new Error(\"API_BASE_URL is required and is not defined\");\n  }\n\n\tvar ENTITY=\"certificates\";\n  var id;\n\tvar table;\n\n\n  function clear(modal_selector) {\n    /** Clear out the modal */\n    var modal_body = $(modal_selector).find('.modal-body');\n\n    var btn;\n    if (modal_selector == \"#add\") {\n      btn = $('#add .modal-footer').find('#addButton');\n      btn.html(\"<span class='glyphicon glyphicon-ok-sign'></span> Add\");\n      btn.removeClass(\"btn-success\");\n      btn.addClass(\"btn-primary\");\n      modal_body.find('#domain').val(\"\");\n    }\n    else {\n      btn = $('#edit .modal-footer').find('#updateButton');\n      btn.html(\"<span class='glyphicon glyphicon-ok-sign'></span> Update\");\n      btn.removeClass(\"btn-success\");\n      btn.addClass(\"btn-warning\");\n    }\n    btn.attr('disabled', false);\n\n  }\n\n\tfunction addEntity(action) {\n\t\tvar selector, modal_body\n\t\tvar requestPayload = {};\n\t\t\n\n\t\t// The default action is a POST\n\t\tif (typeof action === \"undefined\") {\n\t\t\taction = \"POST\";\n\t\t}\n\n\t\tif (action === \"POST\") {\n\t\t\taction = \"POST\";\n\t\t\tselector = \"#add\";\n\t\t\tmodal_body = $(selector + ' .modal-body');\n      requestPayload.domain = modal_body.find(\"#domain\").val()\n\n\t\t}\n    else if (action === \"PUT\") {\n\t\t\taction = \"PUT\";\n\t\t\tselector = \"#edit\";\n\t\t\tmodal_body = $(selector + ' .modal-body');\n      requestPayload.domain = modal_body.find(\"#domain2\").val()\n\n\t\t}\n\n\n\n\t\tif (modal_body.find(\"#certtype_generate\").is(':checked') || (modal_body.find(\"#certtype_generate2\").is(':checked'))) {\n\t\t\t\trequestPayload.type = \"generated\"\n        addGenerated(requestPayload,action)\n\t\t}\n\t\telse {\n\t\t\t\trequestPayload.type = \"uploaded\"\n        addUploaded(requestPayload,action)\n\t\t}\n}\n\n\nfunction addUploaded (requestPayload,action) {\n\n  var formData = new FormData(document.querySelector('#addCertificateForm'))\n\n  $.ajax({\n    url: API_BASE_URL + ENTITY + \"/upload/\" + requestPayload.domain,\n    type: 'POST',\n    data: formData,\n    async: false,\n    cache: false,\n    contentType: false,\n    processData: false,\n    success: function(response, text_status, xhr) {\n      var btn;\n      var id_int = response.data[0].id;\n\n      // Update the Add Button and the table\n      if (action === \"POST\") {\n        btn = $('#add .modal-footer').find('#addButton');\n        btn.removeClass(\"btn-primary\");\n      }\n      else {\n        btn = $('#edit .modal-footer').find('#updateButton');\n        btn.removeClass(\"btn-warning\");\n      }\n\n      btn.addClass(\"btn-success\");\n      btn.html(\"<span class='glyphicon glyphicon-check'></span>Saved!\");\n      btn.attr(\"disabled\", true);\n\n      showNotification(\"Certificates were uploaded\");\n\n      if (action === \"POST\") {\n        table.row.add({\n          \"id\": id_int,\n          \"domain\": requestPayload.domain,\n          \"type\": requestPayload.type,\n          \"assigned_domains\": ''\n        }).draw();\n      }\n    },\n    error: function(xhr, text_status, error_msg) {\n      showNotification(\"Certificates were NOT uploaded\", true);\n    }\n  });\n\n  return false;\n\n}\n\nfunction addGenerated(requestPayload,action) {\n\t\t// Put into JSON Message and send over\n      if (action === \"POST\")\n          var btn = $('#add .modal-footer').find('#addButton').prop('disabled', true);\n      else {\n        var btn = $('#edit .modal-footer').find('#updateButton').prop('disabled', true);\n      }\n\n    $.ajax({\n      type: action,\n      url: API_BASE_URL + ENTITY,\n      dataType: \"json\",\n      contentType: \"application/json; charset=utf-8\",\n      data: JSON.stringify(requestPayload),\n      success: function(response, textStatus, jqXHR) {\n\n        var id_int = response.data[0].id;\n\n        setTimeout(function() {\n  \t\t\tvar add_modal = $('#add');\n  \t\t\t\tif (add_modal.is(':visible')) {\n  \t\t\t\t\tadd_modal.modal('hide');\n  \t\t\t\t}\n  \t\t\t}, 1500);\n\n        // Update the Add Button and the table\n        if (action === \"POST\") {\n\n          btn = $('#add .modal-footer').find('#addButton');\n          btn.removeClass(\"btn-primary\");\n        }\n        else {\n          btn = $('#edit .modal-footer').find('#updateButton');\n          btn.removeClass(\"btn-warning\");\n        }\n\n        btn.addClass(\"btn-success\");\n        btn.html(\"<span class='glyphicon glyphicon-check'></span>Saved!\");\n        btn.attr(\"disabled\", true);\n\n        if (action === \"POST\") {\n          table.row.add({\n            \"id\": id_int,\n            \"domain\": requestPayload.domain,\n            \"type\": requestPayload.type,\n            \"assigned_domains\": ''\n          }).draw();\n        }\n      /*  else {\n          table.row(function(idx, data, node) {\n            return data.id === id_int;\n          }).data({\n            \"domain\": requestPayload.domain,\n            \"id\": id_int\n          }).draw();\n        }*/\n      }\n    });\n\t}\n\nfunction deleteEntity() {\n    var id_int = parseInt(id, 10);\n\n    $.ajax({\n      type: \"DELETE\",\n      url: API_BASE_URL + ENTITY + \"/\" + id,\n      dataType: \"json\",\n      contentType: \"application/json; charset=utf-8\",\n      success: function(response, textStatus, jqXHR) {\n        $('#delete').modal('hide');\n        $('#edit').modal('hide');\n        table.row(function (idx, data, node) {\n            //return data.id === id_int;\n            return data.domain === id;\n        }).remove().draw();\n\n        showNotification(\"Certificate was deleted\");\n      }\n    });\n  }\n\n\t$(document).ready(function() {\n\t\t// datatable init\n\t\ttable = $('#' + ENTITY).DataTable({\n\t\t\t\"ajax\": {\n\t\t\t\t\"url\": API_BASE_URL + ENTITY\n\t\t\t},\n\t\t\t\"columns\": [\n\t\t\t\t{\"data\": \"id\"},\n\t\t\t\t{\"data\": \"domain\"},\n\t\t\t\t{\"data\": \"type\"},\n\t\t\t\t{\"data\": \"assigned_domains\"}\n\t\t\t\t//{ \"data\": \"gwlist\", visible: false },\n\t\t\t],\n\t\t\t\"order\": [[1, 'asc']]\n\t\t});\n\n\t\t// table editing by clicking on the row\n\t\t$('#' + ENTITY + ' tbody').on('click', 'tr', function() {\n\t\t\t//Turn off selected on any other rows\n\t\t\t$('#' + ENTITY).find('tr').removeClass('selected');\n\n\t\t\tif ($(this).hasClass('selected')) {\n\t\t\t\t$(this).removeClass('selected');\n\t\t\t}\n\t\t\telse {\n\t\t\t\t//table.$('tr.selected').removeClass('selected');\n\t\t\t\t$(this).addClass('selected');\n\t\t\t\tid = $(this).find('td').eq(1).text()\n\t\t\t\tif (id != \"\") {\n\t\t\t\t      $('#edit').modal('show');\n      }\n\t\t\t}\n\t\t});\n\n\n$('#open-Add').click(function() {\n      clear('#add');\n});\n\n/* validate fields before submitting api request */\n$('#addButton').click(function() {\n\t\tif (validateFields('#add')) {\n\t\t\taddEntity('POST');\n\n\t\t}\n});\n\n$('#updateButton').click(function() {\n\t\tif (validateFields('#edit')) {\n\t\t\taddEntity('PUT');\n\t\t}\n});\n\n/* handler for deleting endpoint group */\n$('#deleteButton').click(function() {\n  deleteEntity();\n});\n\n$(\"#domain\").keyup(function () {\n\tvar value = document.getElementById(\"domain\").value;\n\tconsole.log(value);\n\tif (value.includes(\"*\")) {\n\n\t\tvar command = \"certbot certonly --manual -d \" +\n      value + ' --server https://acme-v02.api.letsencrypt.org/directory' +\n      '--force-renewal --preferred-chain \"ISRG Root X1\"';\n\t\t$(\"#terminalCommand\").text(command);\n\t\t$(\"#terminalDiv\").removeClass(\"hide\");\n    $(\"#certtype_generated\").prop('checked', true);\n      var btn = $('#add .modal-footer').find('#addButton');\n      btn.attr('disabled', true);\n\t}\n\telse{\n\n\t\t$(\"#terminalDiv\").addClass(\"hide\");\n\n\t}\n\n})\n\n$(\"#domain2\").keyup(function () {\n\tvar value = document.getElementById(\"domain\").value;\n\tconsole.log(value);\n\tif (value.includes(\"*\")) {\n\n    var command = \"certbot certonly --manual -d \" +\n      value + ' --server https://acme-v02.api.letsencrypt.org/directory' +\n      '--force-renewal --preferred-chain \"ISRG Root X1\"';\n\t\t$(\"#terminalCommand2\").text(command);\n\t\t$(\"#terminalDiv2\").removeClass(\"hide\");\n    $(\"#certtype_generated2\").prop('checked', true);\n      var btn = $('#edit .modal-footer').find('#editButton');\n      btn.attr('disabled', true);\n\t}\n\telse {\n\n\t\t$(\"#terminalDiv\").addClass(\"hide\");\n    \n\t}\n\n})\n\n\n$(\"#certtype_generate2\").change(function () {\n\n\t$(\"#generate2\").removeClass(\"hide\");\n\t$(\"#uploaded2\").addClass(\"hide\");\n})\n\n$(\"#certtype_upload2\").change(function () {\n\n\t$(\"#generate2\").addClass(\"hide\");\n\t$(\"#uploaded2\").removeClass(\"hide\");\n})\n\n$(\"#certtype_generate\").change(function () {\n\n\t$(\"#generate\").removeClass(\"hide\");\n\t$(\"#uploaded\").addClass(\"hide\");\n})\n\n$(\"#certtype_upload\").change(function () {\n\n\t$(\"#generate\").addClass(\"hide\");\n\t$(\"#uploaded\").removeClass(\"hide\");\n\t\t\t\n})\n\n$(\"#replace_default_cert\").change(function () {\n\n\tif ($(\"#replace_default_cert\").is(':checked')) {\n\t\t$(\"#domain\").val(\"default\");\n\t\t$(\"#domain\").attr('disabled',true);\n\t}\n\telse {\n\t\t$(\"#domain\").val(\"\");\n\t\t$(\"#domain\").attr('disabled',false);\n\n\t}\n\n})\n\n\n$('#add').on('show.bs.modal', function() {\n\t\n\t$(\"#replace_default_cert\").attr('checked',true);\n\t$(\"#replace_default_cert\").trigger(\"change\");\n\t$(\"#certtype_upload\").trigger(\"change\");\n})\n\n\n$('#edit').on('show.bs.modal', function() {\n  clear('#edit');\n\n  // Show the auth tab by default when the modal shows\n  var modal_body = $('#edit .modal-body');\n\n  // Put into JSON Message and send over\n  $.ajax({\n    type: \"GET\",\n    url: API_BASE_URL + ENTITY + \"/\" + id,\n    dataType: \"json\",\n    contentType: \"application/json; charset=utf-8\",\n    success: function(response, textStatus, jqXHR) {\n\n      modal_body.find(\"#domain2\").val(response.data[0].domain)\n      if (response.data[0].type == \"generated\") {\n        modal_body.find(\"#certtype_generate2\").prop('checked', true);\n      }\n      else if (response.data[0].type == \"uploaded\") {\n        modal_body.find(\"#certtype_upload2\").prop('checked', true);\n        $(\"#uploaded2\").removeClass(\"hide\");\n\n      }\n    },\n      error: function(response, text_status, error_msg) {\n        showNotification(\"Could not obtain certificate\", true);\n      }\n  })\n});\n\n$(document).ajaxStart(function(){\n  // Show image container\n  $(\"#loader\").show();\n});\n$(document).ajaxComplete(function(){\n  // Hide image container\n  $(\"#loader\").hide();\n\n});\n\n$(\".close\").click(function () {\n\n\tdocument.getElementById(\"domain\").value = \"\";\n\t$(\"#terminalDiv\").addClass(\"hide\");\n\t$(\"#terminalCommand\").text(\"\");\n\t$(\"#certtype_generate\").prop('selected', true);\n})\n\n})\n\n})(window, document);\n"
  },
  {
    "path": "gui/static/js/combobox.js",
    "content": "/*\n* THIS WORK IS PROVIDED \"AS IS,\" AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.\n* COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT.\n* The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining to the work without specific, written prior permission. Title to copyright in this work will at all times remain with copyright holders.\n*\n* Original Version [listbox-combobox.js]: https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/js/listbox-combobox.js\n* Copyright © [2015] World Wide Web Consortium, (Massachusetts Institute of Technology, European Research Consortium for Informatics and Mathematics, Keio University, Beihang). All Rights Reserved. This work is distributed under the W3C® Software License [1] 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.\n* http://www.w3.org/Consortium/Legal/copyright-software\n*\n* Changes made by the dOpenSource Team\n* Copyright © [2018] W3C®, dOpenSource\n*/\n\n;(function(window, document) {\n  'use strict';\n\n  /**\n   * @namespace aria\n   */\n  var aria = aria || {};\n\n  /**\n   * Check aria widget for class\n   * @param element\n   * @param className\n   * @returns {boolean}\n   */\n  aria.hasClass = function (element, className) {\n    return (new RegExp('(\\\\s|^)' + className + '(\\\\s|$)')).test(element.className);\n  };\n\n  /**\n   * Add class to aria widget\n   * @param element\n   * @param className\n   */\n  aria.addClass = function (element, className) {\n    if (!aria.hasClass(element, className)) {\n      element.className += ' ' + className;\n    }\n  };\n\n  /**\n   * Remove class from aria widget\n   * @param element\n   * @param className\n   */\n  aria.removeClass = function (element, className) {\n    var classRegex = new RegExp('(\\\\s|^)' + className + '(\\\\s|$)');\n    element.className = element.className.replace(classRegex, ' ').trim();\n  };\n\n  /**\n   * Shortcuts for JS event key codes\n   * @type {{UP: number, DOWN: number, ESC: number, ENTER: number, BACKSPACE: number, TAB: number}}\n   */\n  var KeyCodes = {\n    'UP': 38,\n    'DOWN': 40,\n    'ESC': 27,\n    'ENTER': 13,\n    'BACKSPACE': 8,\n    'TAB': 9\n  };\n\n  /**\n   * @constructor\n   *\n   * @desc\n   *    Combobox object representing the state and interactions for a combobox widget\n   *\n   * @param comboboxNode\n   *    The DOM node pointing to the combobox\n   * @param input\n   *    The input node\n   * @param listbox\n   *    The listbox node to load results in\n   * @param searchFn\n   *    The search function. The function accepts a search string and returns an\n   *    array of results.\n   * @param shouldAutoSelect\n   *    Whether to autoselect the current item when focus toggles\n   * @param onShow\n   *    Callback function on show\n   * @param onHide\n   *    Callback function on hide\n   */\n  aria.ListboxCombobox = function(comboboxNode, input, listbox, searchFn, shouldAutoSelect, onShow, onHide) {\n    this.combobox = comboboxNode;\n    this.input = input;\n    this.listbox = listbox;\n    this.searchFn = searchFn;\n    this.shouldAutoSelect = shouldAutoSelect;\n    this.onShow = onShow || function() {};\n    this.onHide = onHide || function() {};\n    this.activeIndex = -1;\n    this.resultsCount = 0;\n    this.shown = false;\n    this.hasInlineAutocomplete = input.getAttribute('aria-autocomplete') === 'both';\n\n    this.setupEvents();\n  };\n\n  aria.ListboxCombobox.prototype.setupEvents = function () {\n    document.body.addEventListener('click', this.checkHide.bind(this));\n    this.input.addEventListener('keyup', this.checkKey.bind(this));\n    this.input.addEventListener('keydown', this.setActiveItem.bind(this));\n    this.input.addEventListener('focus', this.checkShow.bind(this));\n    this.input.addEventListener('blur', this.checkSelection.bind(this));\n    this.listbox.addEventListener('click', this.clickItem.bind(this));\n  };\n\n  aria.ListboxCombobox.prototype.checkKey = function(evt) {\n    var key = evt.which || evt.keyCode;\n\n    switch (key) {\n      case KeyCodes.UP:\n      case KeyCodes.DOWN:\n      case KeyCodes.ESC:\n      case KeyCodes.ENTER:\n        evt.preventDefault();\n        return;\n      default:\n        this.updateResults(false);\n    }\n\n    if (this.hasInlineAutocomplete) {\n      switch (key) {\n        case KeyCodes.BACKSPACE:\n          return;\n        default:\n          this.autocompleteItem();\n      }\n    }\n  };\n\n  aria.ListboxCombobox.prototype.updateResults = function(shouldShowAll) {\n    var searchString = this.input.value;\n    var results = this.searchFn(searchString);\n\n    this.hideListbox();\n\n    if (!shouldShowAll && !searchString) {\n      results = [];\n    }\n\n    if (results.length) {\n      for (var i = 0; i < results.length; i++) {\n        var resultItem = document.createElement('li');\n        resultItem.className = 'result';\n        resultItem.setAttribute('role', 'option');\n        resultItem.setAttribute('id', 'result-item-' + i);\n        resultItem.innerText = results[i];\n        if (this.shouldAutoSelect && i === 0) {\n          resultItem.setAttribute('aria-selected', 'true');\n          aria.addClass(resultItem, 'focused');\n          this.activeIndex = 0;\n        }\n        this.listbox.appendChild(resultItem);\n      }\n      aria.removeClass(this.listbox, 'hidden');\n      this.combobox.setAttribute('aria-expanded', 'true');\n      this.resultsCount = results.length;\n      this.shown = true;\n      this.onShow();\n    }\n  };\n\n  aria.ListboxCombobox.prototype.setActiveItem = function(evt) {\n    var key = evt.which || evt.keyCode;\n    var activeIndex = this.activeIndex;\n\n    if (key === KeyCodes.ESC) {\n      this.hideListbox();\n      setTimeout((function() {\n        // On Firefox, input does not get cleared here unless wrapped in a setTimeout\n        this.input.value = '';\n      }).bind(this), 1);\n      return;\n    }\n\n    if (this.resultsCount < 1) {\n      if (this.hasInlineAutocomplete && (key === KeyCodes.DOWN || key === KeyCodes.UP)) {\n        this.updateResults(true);\n      }\n      else {\n        return;\n      }\n    }\n\n    var prevActive = this.getItemAt(activeIndex);\n    var activeItem;\n\n    switch (key) {\n      case KeyCodes.UP:\n        if (activeIndex <= 0) {\n          activeIndex = this.resultsCount - 1;\n        }\n        else {\n          activeIndex--;\n        }\n        break;\n      case KeyCodes.DOWN:\n        if (activeIndex === -1 || activeIndex >= this.resultsCount - 1) {\n          activeIndex = 0;\n        }\n        else {\n          activeIndex++;\n        }\n        break;\n      case KeyCodes.ENTER:\n        activeItem = this.getItemAt(activeIndex);\n        this.selectItem(activeItem);\n        return;\n      case KeyCodes.TAB:\n        this.checkSelection();\n        this.hideListbox();\n        return;\n      default:\n        return;\n    }\n\n    evt.preventDefault();\n\n    activeItem = this.getItemAt(activeIndex);\n    this.activeIndex = activeIndex;\n\n    if (prevActive) {\n      aria.removeClass(prevActive, 'focused');\n      prevActive.setAttribute('aria-selected', 'false');\n    }\n\n    if (activeItem) {\n      this.input.setAttribute(\n        'aria-activedescendant',\n        'result-item-' + activeIndex\n      );\n      aria.addClass(activeItem, 'focused');\n      activeItem.setAttribute('aria-selected', 'true');\n      if (this.hasInlineAutocomplete) {\n        this.input.value = activeItem.innerText;\n      }\n    }\n    else {\n      this.input.setAttribute(\n        'aria-activedescendant',\n        ''\n      );\n    }\n  };\n\n  aria.ListboxCombobox.prototype.getItemAt = function(index) {\n    return document.getElementById('result-item-' + index);\n  };\n\n  aria.ListboxCombobox.prototype.clickItem = function(evt) {\n    if (evt.target && evt.target.nodeName === 'LI') {\n      this.selectItem(evt.target);\n    }\n  };\n\n  aria.ListboxCombobox.prototype.selectItem = function(item) {\n    if (item) {\n      this.input.value = item.innerText;\n      this.hideListbox();\n    }\n  };\n\n  aria.ListboxCombobox.prototype.checkShow = function(evt) {\n    if (this.shown) {\n      return;\n    }\n    this.updateResults(false);\n  };\n\n  aria.ListboxCombobox.prototype.checkHide = function(evt) {\n    if (evt.target === this.input || this.combobox.contains(evt.target)) {\n      var arrow = $(this.combobox).find('.did-combobox-arrow').get(0);\n      if (evt.target === arrow || this.combobox.contains(arrow)) {\n        if (this.combobox.shown) {\n          this.hideListbox();\n          this.combobox.shown = false;\n        }\n        else {\n          this.updateResults(true);\n          this.combobox.shown = true;\n        }\n      }\n      return;\n    }\n    this.hideListbox();\n  };\n\n  aria.ListboxCombobox.prototype.hideListbox = function() {\n    this.shown = false;\n    this.activeIndex = -1;\n    this.listbox.innerHTML = '';\n    aria.addClass(this.listbox, 'hidden');\n    this.combobox.setAttribute('aria-expanded', 'false');\n    this.resultsCount = 0;\n    this.input.setAttribute('aria-activedescendant', '');\n    this.onHide();\n  };\n\n  aria.ListboxCombobox.prototype.checkSelection = function() {\n    if (this.activeIndex < 0) {\n      return;\n    }\n    var activeItem = this.getItemAt(this.activeIndex);\n    this.selectItem(activeItem);\n  };\n\n  aria.ListboxCombobox.prototype.autocompleteItem = function() {\n    var autocompletedItem = this.listbox.querySelector('.focused');\n    var inputText = this.input.value;\n\n    if (!autocompletedItem || !inputText) {\n      return;\n    }\n\n    var autocomplete = autocompletedItem.innerText;\n    if (inputText !== autocomplete) {\n      this.input.value = autocomplete;\n      this.input.setSelectionRange(inputText.length, autocomplete.length);\n    }\n  };\n\n  /* export the namespace */\n  window.aria = aria;\n\n})(window, document);\n"
  },
  {
    "path": "gui/static/js/dashboard.js",
    "content": ";(function(window, document) {\n  'use strict';\n\n  function getKamailioStats(elem) {\n    $.ajax({\n      type: \"GET\",\n      url: \"/api/v1/kamailio/stats\",\n      dataType: \"json\",\n      success: function(response, text_status, xhr) {\n        var stats = response.data[0];\n        // set defaults if bad response\n        stats.current = stats.current !== undefined ? stats.current : 0;\n        stats.waiting = stats.waiting !== undefined ? stats.waiting : 0;\n        stats.total = stats.total !== undefined ? stats.total : 0;\n        $(\"#dashboard_current_calls\").text(stats.current);\n        $(\"#dashboard_calls_inqueue\").text(stats.waiting);\n        $(\"#dashboard_total_calls_processed\").text(stats.total);\n      }\n    });\n  }\n\n  $(document).ready(function() {\n    getKamailioStats();\n  });\n\n})(window, document);\n"
  },
  {
    "path": "gui/static/js/datatables.js",
    "content": "/*\n * This combined file was created by the DataTables downloader builder:\n *   https://datatables.net/download\n *\n * To rebuild or modify this file with the latest versions of the included\n * software please visit:\n *   https://datatables.net/download/#bs/dt-1.10.21\n *\n * Included libraries:\n *   DataTables 1.10.21\n */\n\n/*! DataTables 1.10.21\n * ©2008-2020 SpryMedia Ltd - datatables.net/license\n */\n\n/**\n * @summary     DataTables\n * @description Paginate, search and order HTML tables\n * @version     1.10.21\n * @file        jquery.dataTables.js\n * @author      SpryMedia Ltd\n * @contact     www.datatables.net\n * @copyright   Copyright 2008-2020 SpryMedia Ltd.\n *\n * This source file is free software, available under the following license:\n *   MIT license - http://datatables.net/license\n *\n * This source file is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.\n *\n * For details please refer to: http://www.datatables.net\n */\n\n/*jslint evil: true, undef: true, browser: true */\n/*globals $,require,jQuery,define,_selector_run,_selector_opts,_selector_first,_selector_row_indexes,_ext,_Api,_api_register,_api_registerPlural,_re_new_lines,_re_html,_re_formatted_numeric,_re_escape_regex,_empty,_intVal,_numToDecimal,_isNumber,_isHtml,_htmlNumeric,_pluck,_pluck_order,_range,_stripHtml,_unique,_fnBuildAjax,_fnAjaxUpdate,_fnAjaxParameters,_fnAjaxUpdateDraw,_fnAjaxDataSrc,_fnAddColumn,_fnColumnOptions,_fnAdjustColumnSizing,_fnVisibleToColumnIndex,_fnColumnIndexToVisible,_fnVisbleColumns,_fnGetColumns,_fnColumnTypes,_fnApplyColumnDefs,_fnHungarianMap,_fnCamelToHungarian,_fnLanguageCompat,_fnBrowserDetect,_fnAddData,_fnAddTr,_fnNodeToDataIndex,_fnNodeToColumnIndex,_fnGetCellData,_fnSetCellData,_fnSplitObjNotation,_fnGetObjectDataFn,_fnSetObjectDataFn,_fnGetDataMaster,_fnClearTable,_fnDeleteIndex,_fnInvalidate,_fnGetRowElements,_fnCreateTr,_fnBuildHead,_fnDrawHead,_fnDraw,_fnReDraw,_fnAddOptionsHtml,_fnDetectHeader,_fnGetUniqueThs,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnFilterCreateSearch,_fnEscapeRegex,_fnFilterData,_fnFeatureHtmlInfo,_fnUpdateInfo,_fnInfoMacros,_fnInitialise,_fnInitComplete,_fnLengthChange,_fnFeatureHtmlLength,_fnFeatureHtmlPaginate,_fnPageChange,_fnFeatureHtmlProcessing,_fnProcessingDisplay,_fnFeatureHtmlTable,_fnScrollDraw,_fnApplyToChildren,_fnCalculateColumnWidths,_fnThrottle,_fnConvertToWidth,_fnGetWidestNode,_fnGetMaxLenString,_fnStringToCss,_fnSortFlatten,_fnSort,_fnSortAria,_fnSortListener,_fnSortAttachListener,_fnSortingClasses,_fnSortData,_fnSaveState,_fnLoadState,_fnSettingsFromNode,_fnLog,_fnMap,_fnBindAction,_fnCallbackReg,_fnCallbackFire,_fnLengthOverflow,_fnRenderer,_fnDataSource,_fnRowAttributes*/\n\n(function( factory ) {\n\t\"use strict\";\n\n\tif ( typeof define === 'function' && define.amd ) {\n\t\t// AMD\n\t\tdefine( ['jquery'], function ( $ ) {\n\t\t\treturn factory( $, window, document );\n\t\t} );\n\t}\n\telse if ( typeof exports === 'object' ) {\n\t\t// CommonJS\n\t\tmodule.exports = function (root, $) {\n\t\t\tif ( ! root ) {\n\t\t\t\t// CommonJS environments without a window global must pass a\n\t\t\t\t// root. This will give an error otherwise\n\t\t\t\troot = window;\n\t\t\t}\n\n\t\t\tif ( ! $ ) {\n\t\t\t\t$ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window\n\t\t\t\t\trequire('jquery') :\n\t\t\t\t\trequire('jquery')( root );\n\t\t\t}\n\n\t\t\treturn factory( $, root, root.document );\n\t\t};\n\t}\n\telse {\n\t\t// Browser\n\t\tfactory( jQuery, window, document );\n\t}\n}\n(function( $, window, document, undefined ) {\n\t\"use strict\";\n\n\t/**\n\t * DataTables is a plug-in for the jQuery Javascript library. It is a highly\n\t * flexible tool, based upon the foundations of progressive enhancement,\n\t * which will add advanced interaction controls to any HTML table. For a\n\t * full list of features please refer to\n\t * [DataTables.net](href=\"http://datatables.net).\n\t *\n\t * Note that the `DataTable` object is not a global variable but is aliased\n\t * to `jQuery.fn.DataTable` and `jQuery.fn.dataTable` through which it may\n\t * be  accessed.\n\t *\n\t *  @class\n\t *  @param {object} [init={}] Configuration object for DataTables. Options\n\t *    are defined by {@link DataTable.defaults}\n\t *  @requires jQuery 1.7+\n\t *\n\t *  @example\n\t *    // Basic initialisation\n\t *    $(document).ready( function {\n\t *      $('#example').dataTable();\n\t *    } );\n\t *\n\t *  @example\n\t *    // Initialisation with configuration options - in this case, disable\n\t *    // pagination and sorting.\n\t *    $(document).ready( function {\n\t *      $('#example').dataTable( {\n\t *        \"paginate\": false,\n\t *        \"sort\": false\n\t *      } );\n\t *    } );\n\t */\n\tvar DataTable = function ( options )\n\t{\n\t\t/**\n\t\t * Perform a jQuery selector action on the table's TR elements (from the tbody) and\n\t\t * return the resulting jQuery object.\n\t\t *  @param {string|node|jQuery} sSelector jQuery selector or node collection to act on\n\t\t *  @param {object} [oOpts] Optional parameters for modifying the rows to be included\n\t\t *  @param {string} [oOpts.filter=none] Select TR elements that meet the current filter\n\t\t *    criterion (\"applied\") or all TR elements (i.e. no filter).\n\t\t *  @param {string} [oOpts.order=current] Order of the TR elements in the processed array.\n\t\t *    Can be either 'current', whereby the current sorting of the table is used, or\n\t\t *    'original' whereby the original order the data was read into the table is used.\n\t\t *  @param {string} [oOpts.page=all] Limit the selection to the currently displayed page\n\t\t *    (\"current\") or not (\"all\"). If 'current' is given, then order is assumed to be\n\t\t *    'current' and filter is 'applied', regardless of what they might be given as.\n\t\t *  @returns {object} jQuery object, filtered by the given selector.\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *\n\t\t *      // Highlight every second row\n\t\t *      oTable.$('tr:odd').css('backgroundColor', 'blue');\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *\n\t\t *      // Filter to rows with 'Webkit' in them, add a background colour and then\n\t\t *      // remove the filter, thus highlighting the 'Webkit' rows only.\n\t\t *      oTable.fnFilter('Webkit');\n\t\t *      oTable.$('tr', {\"search\": \"applied\"}).css('backgroundColor', 'blue');\n\t\t *      oTable.fnFilter('');\n\t\t *    } );\n\t\t */\n\t\tthis.$ = function ( sSelector, oOpts )\n\t\t{\n\t\t\treturn this.api(true).$( sSelector, oOpts );\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Almost identical to $ in operation, but in this case returns the data for the matched\n\t\t * rows - as such, the jQuery selector used should match TR row nodes or TD/TH cell nodes\n\t\t * rather than any descendants, so the data can be obtained for the row/cell. If matching\n\t\t * rows are found, the data returned is the original data array/object that was used to\n\t\t * create the row (or a generated array if from a DOM source).\n\t\t *\n\t\t * This method is often useful in-combination with $ where both functions are given the\n\t\t * same parameters and the array indexes will match identically.\n\t\t *  @param {string|node|jQuery} sSelector jQuery selector or node collection to act on\n\t\t *  @param {object} [oOpts] Optional parameters for modifying the rows to be included\n\t\t *  @param {string} [oOpts.filter=none] Select elements that meet the current filter\n\t\t *    criterion (\"applied\") or all elements (i.e. no filter).\n\t\t *  @param {string} [oOpts.order=current] Order of the data in the processed array.\n\t\t *    Can be either 'current', whereby the current sorting of the table is used, or\n\t\t *    'original' whereby the original order the data was read into the table is used.\n\t\t *  @param {string} [oOpts.page=all] Limit the selection to the currently displayed page\n\t\t *    (\"current\") or not (\"all\"). If 'current' is given, then order is assumed to be\n\t\t *    'current' and filter is 'applied', regardless of what they might be given as.\n\t\t *  @returns {array} Data for the matched elements. If any elements, as a result of the\n\t\t *    selector, were not TR, TD or TH elements in the DataTable, they will have a null\n\t\t *    entry in the array.\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *\n\t\t *      // Get the data from the first row in the table\n\t\t *      var data = oTable._('tr:first');\n\t\t *\n\t\t *      // Do something useful with the data\n\t\t *      alert( \"First cell is: \"+data[0] );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *\n\t\t *      // Filter to 'Webkit' and get all data for\n\t\t *      oTable.fnFilter('Webkit');\n\t\t *      var data = oTable._('tr', {\"search\": \"applied\"});\n\t\t *\n\t\t *      // Do something with the data\n\t\t *      alert( data.length+\" rows matched the search\" );\n\t\t *    } );\n\t\t */\n\t\tthis._ = function ( sSelector, oOpts )\n\t\t{\n\t\t\treturn this.api(true).rows( sSelector, oOpts ).data();\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Create a DataTables Api instance, with the currently selected tables for\n\t\t * the Api's context.\n\t\t * @param {boolean} [traditional=false] Set the API instance's context to be\n\t\t *   only the table referred to by the `DataTable.ext.iApiIndex` option, as was\n\t\t *   used in the API presented by DataTables 1.9- (i.e. the traditional mode),\n\t\t *   or if all tables captured in the jQuery object should be used.\n\t\t * @return {DataTables.Api}\n\t\t */\n\t\tthis.api = function ( traditional )\n\t\t{\n\t\t\treturn traditional ?\n\t\t\t\tnew _Api(\n\t\t\t\t\t_fnSettingsFromNode( this[ _ext.iApiIndex ] )\n\t\t\t\t) :\n\t\t\t\tnew _Api( this );\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Add a single new row or multiple rows of data to the table. Please note\n\t\t * that this is suitable for client-side processing only - if you are using\n\t\t * server-side processing (i.e. \"bServerSide\": true), then to add data, you\n\t\t * must add it to the data source, i.e. the server-side, through an Ajax call.\n\t\t *  @param {array|object} data The data to be added to the table. This can be:\n\t\t *    <ul>\n\t\t *      <li>1D array of data - add a single row with the data provided</li>\n\t\t *      <li>2D array of arrays - add multiple rows in a single call</li>\n\t\t *      <li>object - data object when using <i>mData</i></li>\n\t\t *      <li>array of objects - multiple data objects when using <i>mData</i></li>\n\t\t *    </ul>\n\t\t *  @param {bool} [redraw=true] redraw the table or not\n\t\t *  @returns {array} An array of integers, representing the list of indexes in\n\t\t *    <i>aoData</i> ({@link DataTable.models.oSettings}) that have been added to\n\t\t *    the table.\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    // Global var for counter\n\t\t *    var giCount = 2;\n\t\t *\n\t\t *    $(document).ready(function() {\n\t\t *      $('#example').dataTable();\n\t\t *    } );\n\t\t *\n\t\t *    function fnClickAddRow() {\n\t\t *      $('#example').dataTable().fnAddData( [\n\t\t *        giCount+\".1\",\n\t\t *        giCount+\".2\",\n\t\t *        giCount+\".3\",\n\t\t *        giCount+\".4\" ]\n\t\t *      );\n\t\t *\n\t\t *      giCount++;\n\t\t *    }\n\t\t */\n\t\tthis.fnAddData = function( data, redraw )\n\t\t{\n\t\t\tvar api = this.api( true );\n\t\t\n\t\t\t/* Check if we want to add multiple rows or not */\n\t\t\tvar rows = $.isArray(data) && ( $.isArray(data[0]) || $.isPlainObject(data[0]) ) ?\n\t\t\t\tapi.rows.add( data ) :\n\t\t\t\tapi.row.add( data );\n\t\t\n\t\t\tif ( redraw === undefined || redraw ) {\n\t\t\t\tapi.draw();\n\t\t\t}\n\t\t\n\t\t\treturn rows.flatten().toArray();\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * This function will make DataTables recalculate the column sizes, based on the data\n\t\t * contained in the table and the sizes applied to the columns (in the DOM, CSS or\n\t\t * through the sWidth parameter). This can be useful when the width of the table's\n\t\t * parent element changes (for example a window resize).\n\t\t *  @param {boolean} [bRedraw=true] Redraw the table or not, you will typically want to\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable( {\n\t\t *        \"sScrollY\": \"200px\",\n\t\t *        \"bPaginate\": false\n\t\t *      } );\n\t\t *\n\t\t *      $(window).on('resize', function () {\n\t\t *        oTable.fnAdjustColumnSizing();\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\tthis.fnAdjustColumnSizing = function ( bRedraw )\n\t\t{\n\t\t\tvar api = this.api( true ).columns.adjust();\n\t\t\tvar settings = api.settings()[0];\n\t\t\tvar scroll = settings.oScroll;\n\t\t\n\t\t\tif ( bRedraw === undefined || bRedraw ) {\n\t\t\t\tapi.draw( false );\n\t\t\t}\n\t\t\telse if ( scroll.sX !== \"\" || scroll.sY !== \"\" ) {\n\t\t\t\t/* If not redrawing, but scrolling, we want to apply the new column sizes anyway */\n\t\t\t\t_fnScrollDraw( settings );\n\t\t\t}\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Quickly and simply clear a table\n\t\t *  @param {bool} [bRedraw=true] redraw the table or not\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *\n\t\t *      // Immediately 'nuke' the current rows (perhaps waiting for an Ajax callback...)\n\t\t *      oTable.fnClearTable();\n\t\t *    } );\n\t\t */\n\t\tthis.fnClearTable = function( bRedraw )\n\t\t{\n\t\t\tvar api = this.api( true ).clear();\n\t\t\n\t\t\tif ( bRedraw === undefined || bRedraw ) {\n\t\t\t\tapi.draw();\n\t\t\t}\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * The exact opposite of 'opening' a row, this function will close any rows which\n\t\t * are currently 'open'.\n\t\t *  @param {node} nTr the table row to 'close'\n\t\t *  @returns {int} 0 on success, or 1 if failed (can't find the row)\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable;\n\t\t *\n\t\t *      // 'open' an information row when a row is clicked on\n\t\t *      $('#example tbody tr').click( function () {\n\t\t *        if ( oTable.fnIsOpen(this) ) {\n\t\t *          oTable.fnClose( this );\n\t\t *        } else {\n\t\t *          oTable.fnOpen( this, \"Temporary row opened\", \"info_row\" );\n\t\t *        }\n\t\t *      } );\n\t\t *\n\t\t *      oTable = $('#example').dataTable();\n\t\t *    } );\n\t\t */\n\t\tthis.fnClose = function( nTr )\n\t\t{\n\t\t\tthis.api( true ).row( nTr ).child.hide();\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Remove a row for the table\n\t\t *  @param {mixed} target The index of the row from aoData to be deleted, or\n\t\t *    the TR element you want to delete\n\t\t *  @param {function|null} [callBack] Callback function\n\t\t *  @param {bool} [redraw=true] Redraw the table or not\n\t\t *  @returns {array} The row that was deleted\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *\n\t\t *      // Immediately remove the first row\n\t\t *      oTable.fnDeleteRow( 0 );\n\t\t *    } );\n\t\t */\n\t\tthis.fnDeleteRow = function( target, callback, redraw )\n\t\t{\n\t\t\tvar api = this.api( true );\n\t\t\tvar rows = api.rows( target );\n\t\t\tvar settings = rows.settings()[0];\n\t\t\tvar data = settings.aoData[ rows[0][0] ];\n\t\t\n\t\t\trows.remove();\n\t\t\n\t\t\tif ( callback ) {\n\t\t\t\tcallback.call( this, settings, data );\n\t\t\t}\n\t\t\n\t\t\tif ( redraw === undefined || redraw ) {\n\t\t\t\tapi.draw();\n\t\t\t}\n\t\t\n\t\t\treturn data;\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Restore the table to it's original state in the DOM by removing all of DataTables\n\t\t * enhancements, alterations to the DOM structure of the table and event listeners.\n\t\t *  @param {boolean} [remove=false] Completely remove the table from the DOM\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      // This example is fairly pointless in reality, but shows how fnDestroy can be used\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *      oTable.fnDestroy();\n\t\t *    } );\n\t\t */\n\t\tthis.fnDestroy = function ( remove )\n\t\t{\n\t\t\tthis.api( true ).destroy( remove );\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Redraw the table\n\t\t *  @param {bool} [complete=true] Re-filter and resort (if enabled) the table before the draw.\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *\n\t\t *      // Re-draw the table - you wouldn't want to do it here, but it's an example :-)\n\t\t *      oTable.fnDraw();\n\t\t *    } );\n\t\t */\n\t\tthis.fnDraw = function( complete )\n\t\t{\n\t\t\t// Note that this isn't an exact match to the old call to _fnDraw - it takes\n\t\t\t// into account the new data, but can hold position.\n\t\t\tthis.api( true ).draw( complete );\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Filter the input based on data\n\t\t *  @param {string} sInput String to filter the table on\n\t\t *  @param {int|null} [iColumn] Column to limit filtering to\n\t\t *  @param {bool} [bRegex=false] Treat as regular expression or not\n\t\t *  @param {bool} [bSmart=true] Perform smart filtering or not\n\t\t *  @param {bool} [bShowGlobal=true] Show the input global filter in it's input box(es)\n\t\t *  @param {bool} [bCaseInsensitive=true] Do case-insensitive matching (true) or not (false)\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *\n\t\t *      // Sometime later - filter...\n\t\t *      oTable.fnFilter( 'test string' );\n\t\t *    } );\n\t\t */\n\t\tthis.fnFilter = function( sInput, iColumn, bRegex, bSmart, bShowGlobal, bCaseInsensitive )\n\t\t{\n\t\t\tvar api = this.api( true );\n\t\t\n\t\t\tif ( iColumn === null || iColumn === undefined ) {\n\t\t\t\tapi.search( sInput, bRegex, bSmart, bCaseInsensitive );\n\t\t\t}\n\t\t\telse {\n\t\t\t\tapi.column( iColumn ).search( sInput, bRegex, bSmart, bCaseInsensitive );\n\t\t\t}\n\t\t\n\t\t\tapi.draw();\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Get the data for the whole table, an individual row or an individual cell based on the\n\t\t * provided parameters.\n\t\t *  @param {int|node} [src] A TR row node, TD/TH cell node or an integer. If given as\n\t\t *    a TR node then the data source for the whole row will be returned. If given as a\n\t\t *    TD/TH cell node then iCol will be automatically calculated and the data for the\n\t\t *    cell returned. If given as an integer, then this is treated as the aoData internal\n\t\t *    data index for the row (see fnGetPosition) and the data for that row used.\n\t\t *  @param {int} [col] Optional column index that you want the data of.\n\t\t *  @returns {array|object|string} If mRow is undefined, then the data for all rows is\n\t\t *    returned. If mRow is defined, just data for that row, and is iCol is\n\t\t *    defined, only data for the designated cell is returned.\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    // Row data\n\t\t *    $(document).ready(function() {\n\t\t *      oTable = $('#example').dataTable();\n\t\t *\n\t\t *      oTable.$('tr').click( function () {\n\t\t *        var data = oTable.fnGetData( this );\n\t\t *        // ... do something with the array / object of data for the row\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Individual cell data\n\t\t *    $(document).ready(function() {\n\t\t *      oTable = $('#example').dataTable();\n\t\t *\n\t\t *      oTable.$('td').click( function () {\n\t\t *        var sData = oTable.fnGetData( this );\n\t\t *        alert( 'The cell clicked on had the value of '+sData );\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\tthis.fnGetData = function( src, col )\n\t\t{\n\t\t\tvar api = this.api( true );\n\t\t\n\t\t\tif ( src !== undefined ) {\n\t\t\t\tvar type = src.nodeName ? src.nodeName.toLowerCase() : '';\n\t\t\n\t\t\t\treturn col !== undefined || type == 'td' || type == 'th' ?\n\t\t\t\t\tapi.cell( src, col ).data() :\n\t\t\t\t\tapi.row( src ).data() || null;\n\t\t\t}\n\t\t\n\t\t\treturn api.data().toArray();\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Get an array of the TR nodes that are used in the table's body. Note that you will\n\t\t * typically want to use the '$' API method in preference to this as it is more\n\t\t * flexible.\n\t\t *  @param {int} [iRow] Optional row index for the TR element you want\n\t\t *  @returns {array|node} If iRow is undefined, returns an array of all TR elements\n\t\t *    in the table's body, or iRow is defined, just the TR element requested.\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *\n\t\t *      // Get the nodes from the table\n\t\t *      var nNodes = oTable.fnGetNodes( );\n\t\t *    } );\n\t\t */\n\t\tthis.fnGetNodes = function( iRow )\n\t\t{\n\t\t\tvar api = this.api( true );\n\t\t\n\t\t\treturn iRow !== undefined ?\n\t\t\t\tapi.row( iRow ).node() :\n\t\t\t\tapi.rows().nodes().flatten().toArray();\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Get the array indexes of a particular cell from it's DOM element\n\t\t * and column index including hidden columns\n\t\t *  @param {node} node this can either be a TR, TD or TH in the table's body\n\t\t *  @returns {int} If nNode is given as a TR, then a single index is returned, or\n\t\t *    if given as a cell, an array of [row index, column index (visible),\n\t\t *    column index (all)] is given.\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      $('#example tbody td').click( function () {\n\t\t *        // Get the position of the current data from the node\n\t\t *        var aPos = oTable.fnGetPosition( this );\n\t\t *\n\t\t *        // Get the data array for this row\n\t\t *        var aData = oTable.fnGetData( aPos[0] );\n\t\t *\n\t\t *        // Update the data array and return the value\n\t\t *        aData[ aPos[1] ] = 'clicked';\n\t\t *        this.innerHTML = 'clicked';\n\t\t *      } );\n\t\t *\n\t\t *      // Init DataTables\n\t\t *      oTable = $('#example').dataTable();\n\t\t *    } );\n\t\t */\n\t\tthis.fnGetPosition = function( node )\n\t\t{\n\t\t\tvar api = this.api( true );\n\t\t\tvar nodeName = node.nodeName.toUpperCase();\n\t\t\n\t\t\tif ( nodeName == 'TR' ) {\n\t\t\t\treturn api.row( node ).index();\n\t\t\t}\n\t\t\telse if ( nodeName == 'TD' || nodeName == 'TH' ) {\n\t\t\t\tvar cell = api.cell( node ).index();\n\t\t\n\t\t\t\treturn [\n\t\t\t\t\tcell.row,\n\t\t\t\t\tcell.columnVisible,\n\t\t\t\t\tcell.column\n\t\t\t\t];\n\t\t\t}\n\t\t\treturn null;\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Check to see if a row is 'open' or not.\n\t\t *  @param {node} nTr the table row to check\n\t\t *  @returns {boolean} true if the row is currently open, false otherwise\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable;\n\t\t *\n\t\t *      // 'open' an information row when a row is clicked on\n\t\t *      $('#example tbody tr').click( function () {\n\t\t *        if ( oTable.fnIsOpen(this) ) {\n\t\t *          oTable.fnClose( this );\n\t\t *        } else {\n\t\t *          oTable.fnOpen( this, \"Temporary row opened\", \"info_row\" );\n\t\t *        }\n\t\t *      } );\n\t\t *\n\t\t *      oTable = $('#example').dataTable();\n\t\t *    } );\n\t\t */\n\t\tthis.fnIsOpen = function( nTr )\n\t\t{\n\t\t\treturn this.api( true ).row( nTr ).child.isShown();\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * This function will place a new row directly after a row which is currently\n\t\t * on display on the page, with the HTML contents that is passed into the\n\t\t * function. This can be used, for example, to ask for confirmation that a\n\t\t * particular record should be deleted.\n\t\t *  @param {node} nTr The table row to 'open'\n\t\t *  @param {string|node|jQuery} mHtml The HTML to put into the row\n\t\t *  @param {string} sClass Class to give the new TD cell\n\t\t *  @returns {node} The row opened. Note that if the table row passed in as the\n\t\t *    first parameter, is not found in the table, this method will silently\n\t\t *    return.\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable;\n\t\t *\n\t\t *      // 'open' an information row when a row is clicked on\n\t\t *      $('#example tbody tr').click( function () {\n\t\t *        if ( oTable.fnIsOpen(this) ) {\n\t\t *          oTable.fnClose( this );\n\t\t *        } else {\n\t\t *          oTable.fnOpen( this, \"Temporary row opened\", \"info_row\" );\n\t\t *        }\n\t\t *      } );\n\t\t *\n\t\t *      oTable = $('#example').dataTable();\n\t\t *    } );\n\t\t */\n\t\tthis.fnOpen = function( nTr, mHtml, sClass )\n\t\t{\n\t\t\treturn this.api( true )\n\t\t\t\t.row( nTr )\n\t\t\t\t.child( mHtml, sClass )\n\t\t\t\t.show()\n\t\t\t\t.child()[0];\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Change the pagination - provides the internal logic for pagination in a simple API\n\t\t * function. With this function you can have a DataTables table go to the next,\n\t\t * previous, first or last pages.\n\t\t *  @param {string|int} mAction Paging action to take: \"first\", \"previous\", \"next\" or \"last\"\n\t\t *    or page number to jump to (integer), note that page 0 is the first page.\n\t\t *  @param {bool} [bRedraw=true] Redraw the table or not\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *      oTable.fnPageChange( 'next' );\n\t\t *    } );\n\t\t */\n\t\tthis.fnPageChange = function ( mAction, bRedraw )\n\t\t{\n\t\t\tvar api = this.api( true ).page( mAction );\n\t\t\n\t\t\tif ( bRedraw === undefined || bRedraw ) {\n\t\t\t\tapi.draw(false);\n\t\t\t}\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Show a particular column\n\t\t *  @param {int} iCol The column whose display should be changed\n\t\t *  @param {bool} bShow Show (true) or hide (false) the column\n\t\t *  @param {bool} [bRedraw=true] Redraw the table or not\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *\n\t\t *      // Hide the second column after initialisation\n\t\t *      oTable.fnSetColumnVis( 1, false );\n\t\t *    } );\n\t\t */\n\t\tthis.fnSetColumnVis = function ( iCol, bShow, bRedraw )\n\t\t{\n\t\t\tvar api = this.api( true ).column( iCol ).visible( bShow );\n\t\t\n\t\t\tif ( bRedraw === undefined || bRedraw ) {\n\t\t\t\tapi.columns.adjust().draw();\n\t\t\t}\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Get the settings for a particular table for external manipulation\n\t\t *  @returns {object} DataTables settings object. See\n\t\t *    {@link DataTable.models.oSettings}\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *      var oSettings = oTable.fnSettings();\n\t\t *\n\t\t *      // Show an example parameter from the settings\n\t\t *      alert( oSettings._iDisplayStart );\n\t\t *    } );\n\t\t */\n\t\tthis.fnSettings = function()\n\t\t{\n\t\t\treturn _fnSettingsFromNode( this[_ext.iApiIndex] );\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Sort the table by a particular column\n\t\t *  @param {int} iCol the data index to sort on. Note that this will not match the\n\t\t *    'display index' if you have hidden data entries\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *\n\t\t *      // Sort immediately with columns 0 and 1\n\t\t *      oTable.fnSort( [ [0,'asc'], [1,'asc'] ] );\n\t\t *    } );\n\t\t */\n\t\tthis.fnSort = function( aaSort )\n\t\t{\n\t\t\tthis.api( true ).order( aaSort ).draw();\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Attach a sort listener to an element for a given column\n\t\t *  @param {node} nNode the element to attach the sort listener to\n\t\t *  @param {int} iColumn the column that a click on this node will sort on\n\t\t *  @param {function} [fnCallback] callback function when sort is run\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *\n\t\t *      // Sort on column 1, when 'sorter' is clicked on\n\t\t *      oTable.fnSortListener( document.getElementById('sorter'), 1 );\n\t\t *    } );\n\t\t */\n\t\tthis.fnSortListener = function( nNode, iColumn, fnCallback )\n\t\t{\n\t\t\tthis.api( true ).order.listener( nNode, iColumn, fnCallback );\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Update a table cell or row - this method will accept either a single value to\n\t\t * update the cell with, an array of values with one element for each column or\n\t\t * an object in the same format as the original data source. The function is\n\t\t * self-referencing in order to make the multi column updates easier.\n\t\t *  @param {object|array|string} mData Data to update the cell/row with\n\t\t *  @param {node|int} mRow TR element you want to update or the aoData index\n\t\t *  @param {int} [iColumn] The column to update, give as null or undefined to\n\t\t *    update a whole row.\n\t\t *  @param {bool} [bRedraw=true] Redraw the table or not\n\t\t *  @param {bool} [bAction=true] Perform pre-draw actions or not\n\t\t *  @returns {int} 0 on success, 1 on error\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *      oTable.fnUpdate( 'Example update', 0, 0 ); // Single cell\n\t\t *      oTable.fnUpdate( ['a', 'b', 'c', 'd', 'e'], $('tbody tr')[0] ); // Row\n\t\t *    } );\n\t\t */\n\t\tthis.fnUpdate = function( mData, mRow, iColumn, bRedraw, bAction )\n\t\t{\n\t\t\tvar api = this.api( true );\n\t\t\n\t\t\tif ( iColumn === undefined || iColumn === null ) {\n\t\t\t\tapi.row( mRow ).data( mData );\n\t\t\t}\n\t\t\telse {\n\t\t\t\tapi.cell( mRow, iColumn ).data( mData );\n\t\t\t}\n\t\t\n\t\t\tif ( bAction === undefined || bAction ) {\n\t\t\t\tapi.columns.adjust();\n\t\t\t}\n\t\t\n\t\t\tif ( bRedraw === undefined || bRedraw ) {\n\t\t\t\tapi.draw();\n\t\t\t}\n\t\t\treturn 0;\n\t\t};\n\t\t\n\t\t\n\t\t/**\n\t\t * Provide a common method for plug-ins to check the version of DataTables being used, in order\n\t\t * to ensure compatibility.\n\t\t *  @param {string} sVersion Version string to check for, in the format \"X.Y.Z\". Note that the\n\t\t *    formats \"X\" and \"X.Y\" are also acceptable.\n\t\t *  @returns {boolean} true if this version of DataTables is greater or equal to the required\n\t\t *    version, or false if this version of DataTales is not suitable\n\t\t *  @method\n\t\t *  @dtopt API\n\t\t *  @deprecated Since v1.10\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready(function() {\n\t\t *      var oTable = $('#example').dataTable();\n\t\t *      alert( oTable.fnVersionCheck( '1.9.0' ) );\n\t\t *    } );\n\t\t */\n\t\tthis.fnVersionCheck = _ext.fnVersionCheck;\n\t\t\n\n\t\tvar _that = this;\n\t\tvar emptyInit = options === undefined;\n\t\tvar len = this.length;\n\n\t\tif ( emptyInit ) {\n\t\t\toptions = {};\n\t\t}\n\n\t\tthis.oApi = this.internal = _ext.internal;\n\n\t\t// Extend with old style plug-in API methods\n\t\tfor ( var fn in DataTable.ext.internal ) {\n\t\t\tif ( fn ) {\n\t\t\t\tthis[fn] = _fnExternApiFunc(fn);\n\t\t\t}\n\t\t}\n\n\t\tthis.each(function() {\n\t\t\t// For each initialisation we want to give it a clean initialisation\n\t\t\t// object that can be bashed around\n\t\t\tvar o = {};\n\t\t\tvar oInit = len > 1 ? // optimisation for single table case\n\t\t\t\t_fnExtend( o, options, true ) :\n\t\t\t\toptions;\n\n\t\t\t/*global oInit,_that,emptyInit*/\n\t\t\tvar i=0, iLen, j, jLen, k, kLen;\n\t\t\tvar sId = this.getAttribute( 'id' );\n\t\t\tvar bInitHandedOff = false;\n\t\t\tvar defaults = DataTable.defaults;\n\t\t\tvar $this = $(this);\n\t\t\t\n\t\t\t\n\t\t\t/* Sanity check */\n\t\t\tif ( this.nodeName.toLowerCase() != 'table' )\n\t\t\t{\n\t\t\t\t_fnLog( null, 0, 'Non-table node initialisation ('+this.nodeName+')', 2 );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t\n\t\t\t/* Backwards compatibility for the defaults */\n\t\t\t_fnCompatOpts( defaults );\n\t\t\t_fnCompatCols( defaults.column );\n\t\t\t\n\t\t\t/* Convert the camel-case defaults to Hungarian */\n\t\t\t_fnCamelToHungarian( defaults, defaults, true );\n\t\t\t_fnCamelToHungarian( defaults.column, defaults.column, true );\n\t\t\t\n\t\t\t/* Setting up the initialisation object */\n\t\t\t_fnCamelToHungarian( defaults, $.extend( oInit, $this.data() ), true );\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t/* Check to see if we are re-initialising a table */\n\t\t\tvar allSettings = DataTable.settings;\n\t\t\tfor ( i=0, iLen=allSettings.length ; i<iLen ; i++ )\n\t\t\t{\n\t\t\t\tvar s = allSettings[i];\n\t\t\t\n\t\t\t\t/* Base check on table node */\n\t\t\t\tif (\n\t\t\t\t\ts.nTable == this ||\n\t\t\t\t\t(s.nTHead && s.nTHead.parentNode == this) ||\n\t\t\t\t\t(s.nTFoot && s.nTFoot.parentNode == this)\n\t\t\t\t) {\n\t\t\t\t\tvar bRetrieve = oInit.bRetrieve !== undefined ? oInit.bRetrieve : defaults.bRetrieve;\n\t\t\t\t\tvar bDestroy = oInit.bDestroy !== undefined ? oInit.bDestroy : defaults.bDestroy;\n\t\t\t\n\t\t\t\t\tif ( emptyInit || bRetrieve )\n\t\t\t\t\t{\n\t\t\t\t\t\treturn s.oInstance;\n\t\t\t\t\t}\n\t\t\t\t\telse if ( bDestroy )\n\t\t\t\t\t{\n\t\t\t\t\t\ts.oInstance.fnDestroy();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\t_fnLog( s, 0, 'Cannot reinitialise DataTable', 3 );\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\n\t\t\t\t/* If the element we are initialising has the same ID as a table which was previously\n\t\t\t\t * initialised, but the table nodes don't match (from before) then we destroy the old\n\t\t\t\t * instance by simply deleting it. This is under the assumption that the table has been\n\t\t\t\t * destroyed by other methods. Anyone using non-id selectors will need to do this manually\n\t\t\t\t */\n\t\t\t\tif ( s.sTableId == this.id )\n\t\t\t\t{\n\t\t\t\t\tallSettings.splice( i, 1 );\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t/* Ensure the table has an ID - required for accessibility */\n\t\t\tif ( sId === null || sId === \"\" )\n\t\t\t{\n\t\t\t\tsId = \"DataTables_Table_\"+(DataTable.ext._unique++);\n\t\t\t\tthis.id = sId;\n\t\t\t}\n\t\t\t\n\t\t\t/* Create the settings object for this table and set some of the default parameters */\n\t\t\tvar oSettings = $.extend( true, {}, DataTable.models.oSettings, {\n\t\t\t\t\"sDestroyWidth\": $this[0].style.width,\n\t\t\t\t\"sInstance\":     sId,\n\t\t\t\t\"sTableId\":      sId\n\t\t\t} );\n\t\t\toSettings.nTable = this;\n\t\t\toSettings.oApi   = _that.internal;\n\t\t\toSettings.oInit  = oInit;\n\t\t\t\n\t\t\tallSettings.push( oSettings );\n\t\t\t\n\t\t\t// Need to add the instance after the instance after the settings object has been added\n\t\t\t// to the settings array, so we can self reference the table instance if more than one\n\t\t\toSettings.oInstance = (_that.length===1) ? _that : $this.dataTable();\n\t\t\t\n\t\t\t// Backwards compatibility, before we apply all the defaults\n\t\t\t_fnCompatOpts( oInit );\n\t\t\t_fnLanguageCompat( oInit.oLanguage );\n\t\t\t\n\t\t\t// If the length menu is given, but the init display length is not, use the length menu\n\t\t\tif ( oInit.aLengthMenu && ! oInit.iDisplayLength )\n\t\t\t{\n\t\t\t\toInit.iDisplayLength = $.isArray( oInit.aLengthMenu[0] ) ?\n\t\t\t\t\toInit.aLengthMenu[0][0] : oInit.aLengthMenu[0];\n\t\t\t}\n\t\t\t\n\t\t\t// Apply the defaults and init options to make a single init object will all\n\t\t\t// options defined from defaults and instance options.\n\t\t\toInit = _fnExtend( $.extend( true, {}, defaults ), oInit );\n\t\t\t\n\t\t\t\n\t\t\t// Map the initialisation options onto the settings object\n\t\t\t_fnMap( oSettings.oFeatures, oInit, [\n\t\t\t\t\"bPaginate\",\n\t\t\t\t\"bLengthChange\",\n\t\t\t\t\"bFilter\",\n\t\t\t\t\"bSort\",\n\t\t\t\t\"bSortMulti\",\n\t\t\t\t\"bInfo\",\n\t\t\t\t\"bProcessing\",\n\t\t\t\t\"bAutoWidth\",\n\t\t\t\t\"bSortClasses\",\n\t\t\t\t\"bServerSide\",\n\t\t\t\t\"bDeferRender\"\n\t\t\t] );\n\t\t\t_fnMap( oSettings, oInit, [\n\t\t\t\t\"asStripeClasses\",\n\t\t\t\t\"ajax\",\n\t\t\t\t\"fnServerData\",\n\t\t\t\t\"fnFormatNumber\",\n\t\t\t\t\"sServerMethod\",\n\t\t\t\t\"aaSorting\",\n\t\t\t\t\"aaSortingFixed\",\n\t\t\t\t\"aLengthMenu\",\n\t\t\t\t\"sPaginationType\",\n\t\t\t\t\"sAjaxSource\",\n\t\t\t\t\"sAjaxDataProp\",\n\t\t\t\t\"iStateDuration\",\n\t\t\t\t\"sDom\",\n\t\t\t\t\"bSortCellsTop\",\n\t\t\t\t\"iTabIndex\",\n\t\t\t\t\"fnStateLoadCallback\",\n\t\t\t\t\"fnStateSaveCallback\",\n\t\t\t\t\"renderer\",\n\t\t\t\t\"searchDelay\",\n\t\t\t\t\"rowId\",\n\t\t\t\t[ \"iCookieDuration\", \"iStateDuration\" ], // backwards compat\n\t\t\t\t[ \"oSearch\", \"oPreviousSearch\" ],\n\t\t\t\t[ \"aoSearchCols\", \"aoPreSearchCols\" ],\n\t\t\t\t[ \"iDisplayLength\", \"_iDisplayLength\" ]\n\t\t\t] );\n\t\t\t_fnMap( oSettings.oScroll, oInit, [\n\t\t\t\t[ \"sScrollX\", \"sX\" ],\n\t\t\t\t[ \"sScrollXInner\", \"sXInner\" ],\n\t\t\t\t[ \"sScrollY\", \"sY\" ],\n\t\t\t\t[ \"bScrollCollapse\", \"bCollapse\" ]\n\t\t\t] );\n\t\t\t_fnMap( oSettings.oLanguage, oInit, \"fnInfoCallback\" );\n\t\t\t\n\t\t\t/* Callback functions which are array driven */\n\t\t\t_fnCallbackReg( oSettings, 'aoDrawCallback',       oInit.fnDrawCallback,      'user' );\n\t\t\t_fnCallbackReg( oSettings, 'aoServerParams',       oInit.fnServerParams,      'user' );\n\t\t\t_fnCallbackReg( oSettings, 'aoStateSaveParams',    oInit.fnStateSaveParams,   'user' );\n\t\t\t_fnCallbackReg( oSettings, 'aoStateLoadParams',    oInit.fnStateLoadParams,   'user' );\n\t\t\t_fnCallbackReg( oSettings, 'aoStateLoaded',        oInit.fnStateLoaded,       'user' );\n\t\t\t_fnCallbackReg( oSettings, 'aoRowCallback',        oInit.fnRowCallback,       'user' );\n\t\t\t_fnCallbackReg( oSettings, 'aoRowCreatedCallback', oInit.fnCreatedRow,        'user' );\n\t\t\t_fnCallbackReg( oSettings, 'aoHeaderCallback',     oInit.fnHeaderCallback,    'user' );\n\t\t\t_fnCallbackReg( oSettings, 'aoFooterCallback',     oInit.fnFooterCallback,    'user' );\n\t\t\t_fnCallbackReg( oSettings, 'aoInitComplete',       oInit.fnInitComplete,      'user' );\n\t\t\t_fnCallbackReg( oSettings, 'aoPreDrawCallback',    oInit.fnPreDrawCallback,   'user' );\n\t\t\t\n\t\t\toSettings.rowIdFn = _fnGetObjectDataFn( oInit.rowId );\n\t\t\t\n\t\t\t/* Browser support detection */\n\t\t\t_fnBrowserDetect( oSettings );\n\t\t\t\n\t\t\tvar oClasses = oSettings.oClasses;\n\t\t\t\n\t\t\t$.extend( oClasses, DataTable.ext.classes, oInit.oClasses );\n\t\t\t$this.addClass( oClasses.sTable );\n\t\t\t\n\t\t\t\n\t\t\tif ( oSettings.iInitDisplayStart === undefined )\n\t\t\t{\n\t\t\t\t/* Display start point, taking into account the save saving */\n\t\t\t\toSettings.iInitDisplayStart = oInit.iDisplayStart;\n\t\t\t\toSettings._iDisplayStart = oInit.iDisplayStart;\n\t\t\t}\n\t\t\t\n\t\t\tif ( oInit.iDeferLoading !== null )\n\t\t\t{\n\t\t\t\toSettings.bDeferLoading = true;\n\t\t\t\tvar tmp = $.isArray( oInit.iDeferLoading );\n\t\t\t\toSettings._iRecordsDisplay = tmp ? oInit.iDeferLoading[0] : oInit.iDeferLoading;\n\t\t\t\toSettings._iRecordsTotal = tmp ? oInit.iDeferLoading[1] : oInit.iDeferLoading;\n\t\t\t}\n\t\t\t\n\t\t\t/* Language definitions */\n\t\t\tvar oLanguage = oSettings.oLanguage;\n\t\t\t$.extend( true, oLanguage, oInit.oLanguage );\n\t\t\t\n\t\t\tif ( oLanguage.sUrl )\n\t\t\t{\n\t\t\t\t/* Get the language definitions from a file - because this Ajax call makes the language\n\t\t\t\t * get async to the remainder of this function we use bInitHandedOff to indicate that\n\t\t\t\t * _fnInitialise will be fired by the returned Ajax handler, rather than the constructor\n\t\t\t\t */\n\t\t\t\t$.ajax( {\n\t\t\t\t\tdataType: 'json',\n\t\t\t\t\turl: oLanguage.sUrl,\n\t\t\t\t\tsuccess: function ( json ) {\n\t\t\t\t\t\t_fnLanguageCompat( json );\n\t\t\t\t\t\t_fnCamelToHungarian( defaults.oLanguage, json );\n\t\t\t\t\t\t$.extend( true, oLanguage, json );\n\t\t\t\t\t\t_fnInitialise( oSettings );\n\t\t\t\t\t},\n\t\t\t\t\terror: function () {\n\t\t\t\t\t\t// Error occurred loading language file, continue on as best we can\n\t\t\t\t\t\t_fnInitialise( oSettings );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t\tbInitHandedOff = true;\n\t\t\t}\n\t\t\t\n\t\t\t/*\n\t\t\t * Stripes\n\t\t\t */\n\t\t\tif ( oInit.asStripeClasses === null )\n\t\t\t{\n\t\t\t\toSettings.asStripeClasses =[\n\t\t\t\t\toClasses.sStripeOdd,\n\t\t\t\t\toClasses.sStripeEven\n\t\t\t\t];\n\t\t\t}\n\t\t\t\n\t\t\t/* Remove row stripe classes if they are already on the table row */\n\t\t\tvar stripeClasses = oSettings.asStripeClasses;\n\t\t\tvar rowOne = $this.children('tbody').find('tr').eq(0);\n\t\t\tif ( $.inArray( true, $.map( stripeClasses, function(el, i) {\n\t\t\t\treturn rowOne.hasClass(el);\n\t\t\t} ) ) !== -1 ) {\n\t\t\t\t$('tbody tr', this).removeClass( stripeClasses.join(' ') );\n\t\t\t\toSettings.asDestroyStripes = stripeClasses.slice();\n\t\t\t}\n\t\t\t\n\t\t\t/*\n\t\t\t * Columns\n\t\t\t * See if we should load columns automatically or use defined ones\n\t\t\t */\n\t\t\tvar anThs = [];\n\t\t\tvar aoColumnsInit;\n\t\t\tvar nThead = this.getElementsByTagName('thead');\n\t\t\tif ( nThead.length !== 0 )\n\t\t\t{\n\t\t\t\t_fnDetectHeader( oSettings.aoHeader, nThead[0] );\n\t\t\t\tanThs = _fnGetUniqueThs( oSettings );\n\t\t\t}\n\t\t\t\n\t\t\t/* If not given a column array, generate one with nulls */\n\t\t\tif ( oInit.aoColumns === null )\n\t\t\t{\n\t\t\t\taoColumnsInit = [];\n\t\t\t\tfor ( i=0, iLen=anThs.length ; i<iLen ; i++ )\n\t\t\t\t{\n\t\t\t\t\taoColumnsInit.push( null );\n\t\t\t\t}\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\taoColumnsInit = oInit.aoColumns;\n\t\t\t}\n\t\t\t\n\t\t\t/* Add the columns */\n\t\t\tfor ( i=0, iLen=aoColumnsInit.length ; i<iLen ; i++ )\n\t\t\t{\n\t\t\t\t_fnAddColumn( oSettings, anThs ? anThs[i] : null );\n\t\t\t}\n\t\t\t\n\t\t\t/* Apply the column definitions */\n\t\t\t_fnApplyColumnDefs( oSettings, oInit.aoColumnDefs, aoColumnsInit, function (iCol, oDef) {\n\t\t\t\t_fnColumnOptions( oSettings, iCol, oDef );\n\t\t\t} );\n\t\t\t\n\t\t\t/* HTML5 attribute detection - build an mData object automatically if the\n\t\t\t * attributes are found\n\t\t\t */\n\t\t\tif ( rowOne.length ) {\n\t\t\t\tvar a = function ( cell, name ) {\n\t\t\t\t\treturn cell.getAttribute( 'data-'+name ) !== null ? name : null;\n\t\t\t\t};\n\t\t\t\n\t\t\t\t$( rowOne[0] ).children('th, td').each( function (i, cell) {\n\t\t\t\t\tvar col = oSettings.aoColumns[i];\n\t\t\t\n\t\t\t\t\tif ( col.mData === i ) {\n\t\t\t\t\t\tvar sort = a( cell, 'sort' ) || a( cell, 'order' );\n\t\t\t\t\t\tvar filter = a( cell, 'filter' ) || a( cell, 'search' );\n\t\t\t\n\t\t\t\t\t\tif ( sort !== null || filter !== null ) {\n\t\t\t\t\t\t\tcol.mData = {\n\t\t\t\t\t\t\t\t_:      i+'.display',\n\t\t\t\t\t\t\t\tsort:   sort !== null   ? i+'.@data-'+sort   : undefined,\n\t\t\t\t\t\t\t\ttype:   sort !== null   ? i+'.@data-'+sort   : undefined,\n\t\t\t\t\t\t\t\tfilter: filter !== null ? i+'.@data-'+filter : undefined\n\t\t\t\t\t\t\t};\n\t\t\t\n\t\t\t\t\t\t\t_fnColumnOptions( oSettings, i );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t}\n\t\t\t\n\t\t\tvar features = oSettings.oFeatures;\n\t\t\tvar loadedInit = function () {\n\t\t\t\t/*\n\t\t\t\t * Sorting\n\t\t\t\t * @todo For modularisation (1.11) this needs to do into a sort start up handler\n\t\t\t\t */\n\t\t\t\n\t\t\t\t// If aaSorting is not defined, then we use the first indicator in asSorting\n\t\t\t\t// in case that has been altered, so the default sort reflects that option\n\t\t\t\tif ( oInit.aaSorting === undefined ) {\n\t\t\t\t\tvar sorting = oSettings.aaSorting;\n\t\t\t\t\tfor ( i=0, iLen=sorting.length ; i<iLen ; i++ ) {\n\t\t\t\t\t\tsorting[i][1] = oSettings.aoColumns[ i ].asSorting[0];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\n\t\t\t\t/* Do a first pass on the sorting classes (allows any size changes to be taken into\n\t\t\t\t * account, and also will apply sorting disabled classes if disabled\n\t\t\t\t */\n\t\t\t\t_fnSortingClasses( oSettings );\n\t\t\t\n\t\t\t\tif ( features.bSort ) {\n\t\t\t\t\t_fnCallbackReg( oSettings, 'aoDrawCallback', function () {\n\t\t\t\t\t\tif ( oSettings.bSorted ) {\n\t\t\t\t\t\t\tvar aSort = _fnSortFlatten( oSettings );\n\t\t\t\t\t\t\tvar sortedColumns = {};\n\t\t\t\n\t\t\t\t\t\t\t$.each( aSort, function (i, val) {\n\t\t\t\t\t\t\t\tsortedColumns[ val.src ] = val.dir;\n\t\t\t\t\t\t\t} );\n\t\t\t\n\t\t\t\t\t\t\t_fnCallbackFire( oSettings, null, 'order', [oSettings, aSort, sortedColumns] );\n\t\t\t\t\t\t\t_fnSortAria( oSettings );\n\t\t\t\t\t\t}\n\t\t\t\t\t} );\n\t\t\t\t}\n\t\t\t\n\t\t\t\t_fnCallbackReg( oSettings, 'aoDrawCallback', function () {\n\t\t\t\t\tif ( oSettings.bSorted || _fnDataSource( oSettings ) === 'ssp' || features.bDeferRender ) {\n\t\t\t\t\t\t_fnSortingClasses( oSettings );\n\t\t\t\t\t}\n\t\t\t\t}, 'sc' );\n\t\t\t\n\t\t\t\n\t\t\t\t/*\n\t\t\t\t * Final init\n\t\t\t\t * Cache the header, body and footer as required, creating them if needed\n\t\t\t\t */\n\t\t\t\n\t\t\t\t// Work around for Webkit bug 83867 - store the caption-side before removing from doc\n\t\t\t\tvar captions = $this.children('caption').each( function () {\n\t\t\t\t\tthis._captionSide = $(this).css('caption-side');\n\t\t\t\t} );\n\t\t\t\n\t\t\t\tvar thead = $this.children('thead');\n\t\t\t\tif ( thead.length === 0 ) {\n\t\t\t\t\tthead = $('<thead/>').appendTo($this);\n\t\t\t\t}\n\t\t\t\toSettings.nTHead = thead[0];\n\t\t\t\n\t\t\t\tvar tbody = $this.children('tbody');\n\t\t\t\tif ( tbody.length === 0 ) {\n\t\t\t\t\ttbody = $('<tbody/>').appendTo($this);\n\t\t\t\t}\n\t\t\t\toSettings.nTBody = tbody[0];\n\t\t\t\n\t\t\t\tvar tfoot = $this.children('tfoot');\n\t\t\t\tif ( tfoot.length === 0 && captions.length > 0 && (oSettings.oScroll.sX !== \"\" || oSettings.oScroll.sY !== \"\") ) {\n\t\t\t\t\t// If we are a scrolling table, and no footer has been given, then we need to create\n\t\t\t\t\t// a tfoot element for the caption element to be appended to\n\t\t\t\t\ttfoot = $('<tfoot/>').appendTo($this);\n\t\t\t\t}\n\t\t\t\n\t\t\t\tif ( tfoot.length === 0 || tfoot.children().length === 0 ) {\n\t\t\t\t\t$this.addClass( oClasses.sNoFooter );\n\t\t\t\t}\n\t\t\t\telse if ( tfoot.length > 0 ) {\n\t\t\t\t\toSettings.nTFoot = tfoot[0];\n\t\t\t\t\t_fnDetectHeader( oSettings.aoFooter, oSettings.nTFoot );\n\t\t\t\t}\n\t\t\t\n\t\t\t\t/* Check if there is data passing into the constructor */\n\t\t\t\tif ( oInit.aaData ) {\n\t\t\t\t\tfor ( i=0 ; i<oInit.aaData.length ; i++ ) {\n\t\t\t\t\t\t_fnAddData( oSettings, oInit.aaData[ i ] );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if ( oSettings.bDeferLoading || _fnDataSource( oSettings ) == 'dom' ) {\n\t\t\t\t\t/* Grab the data from the page - only do this when deferred loading or no Ajax\n\t\t\t\t\t * source since there is no point in reading the DOM data if we are then going\n\t\t\t\t\t * to replace it with Ajax data\n\t\t\t\t\t */\n\t\t\t\t\t_fnAddTr( oSettings, $(oSettings.nTBody).children('tr') );\n\t\t\t\t}\n\t\t\t\n\t\t\t\t/* Copy the data index array */\n\t\t\t\toSettings.aiDisplay = oSettings.aiDisplayMaster.slice();\n\t\t\t\n\t\t\t\t/* Initialisation complete - table can be drawn */\n\t\t\t\toSettings.bInitialised = true;\n\t\t\t\n\t\t\t\t/* Check if we need to initialise the table (it might not have been handed off to the\n\t\t\t\t * language processor)\n\t\t\t\t */\n\t\t\t\tif ( bInitHandedOff === false ) {\n\t\t\t\t\t_fnInitialise( oSettings );\n\t\t\t\t}\n\t\t\t};\n\t\t\t\n\t\t\t/* Must be done after everything which can be overridden by the state saving! */\n\t\t\tif ( oInit.bStateSave )\n\t\t\t{\n\t\t\t\tfeatures.bStateSave = true;\n\t\t\t\t_fnCallbackReg( oSettings, 'aoDrawCallback', _fnSaveState, 'state_save' );\n\t\t\t\t_fnLoadState( oSettings, oInit, loadedInit );\n\t\t\t}\n\t\t\telse {\n\t\t\t\tloadedInit();\n\t\t\t}\n\t\t\t\n\t\t} );\n\t\t_that = null;\n\t\treturn this;\n\t};\n\n\t\n\t/*\n\t * It is useful to have variables which are scoped locally so only the\n\t * DataTables functions can access them and they don't leak into global space.\n\t * At the same time these functions are often useful over multiple files in the\n\t * core and API, so we list, or at least document, all variables which are used\n\t * by DataTables as private variables here. This also ensures that there is no\n\t * clashing of variable names and that they can easily referenced for reuse.\n\t */\n\t\n\t\n\t// Defined else where\n\t//  _selector_run\n\t//  _selector_opts\n\t//  _selector_first\n\t//  _selector_row_indexes\n\t\n\tvar _ext; // DataTable.ext\n\tvar _Api; // DataTable.Api\n\tvar _api_register; // DataTable.Api.register\n\tvar _api_registerPlural; // DataTable.Api.registerPlural\n\t\n\tvar _re_dic = {};\n\tvar _re_new_lines = /[\\r\\n\\u2028]/g;\n\tvar _re_html = /<.*?>/g;\n\t\n\t// This is not strict ISO8601 - Date.parse() is quite lax, although\n\t// implementations differ between browsers.\n\tvar _re_date = /^\\d{2,4}[\\.\\/\\-]\\d{1,2}[\\.\\/\\-]\\d{1,2}([T ]{1}\\d{1,2}[:\\.]\\d{2}([\\.:]\\d{2})?)?$/;\n\t\n\t// Escape regular expression special characters\n\tvar _re_escape_regex = new RegExp( '(\\\\' + [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\\\', '$', '^', '-' ].join('|\\\\') + ')', 'g' );\n\t\n\t// http://en.wikipedia.org/wiki/Foreign_exchange_market\n\t// - \\u20BD - Russian ruble.\n\t// - \\u20a9 - South Korean Won\n\t// - \\u20BA - Turkish Lira\n\t// - \\u20B9 - Indian Rupee\n\t// - R - Brazil (R$) and South Africa\n\t// - fr - Swiss Franc\n\t// - kr - Swedish krona, Norwegian krone and Danish krone\n\t// - \\u2009 is thin space and \\u202F is narrow no-break space, both used in many\n\t// - Ƀ - Bitcoin\n\t// - Ξ - Ethereum\n\t//   standards as thousands separators.\n\tvar _re_formatted_numeric = /[',$£€¥%\\u2009\\u202F\\u20BD\\u20a9\\u20BArfkɃΞ]/gi;\n\t\n\t\n\tvar _empty = function ( d ) {\n\t\treturn !d || d === true || d === '-' ? true : false;\n\t};\n\t\n\t\n\tvar _intVal = function ( s ) {\n\t\tvar integer = parseInt( s, 10 );\n\t\treturn !isNaN(integer) && isFinite(s) ? integer : null;\n\t};\n\t\n\t// Convert from a formatted number with characters other than `.` as the\n\t// decimal place, to a Javascript number\n\tvar _numToDecimal = function ( num, decimalPoint ) {\n\t\t// Cache created regular expressions for speed as this function is called often\n\t\tif ( ! _re_dic[ decimalPoint ] ) {\n\t\t\t_re_dic[ decimalPoint ] = new RegExp( _fnEscapeRegex( decimalPoint ), 'g' );\n\t\t}\n\t\treturn typeof num === 'string' && decimalPoint !== '.' ?\n\t\t\tnum.replace( /\\./g, '' ).replace( _re_dic[ decimalPoint ], '.' ) :\n\t\t\tnum;\n\t};\n\t\n\t\n\tvar _isNumber = function ( d, decimalPoint, formatted ) {\n\t\tvar strType = typeof d === 'string';\n\t\n\t\t// If empty return immediately so there must be a number if it is a\n\t\t// formatted string (this stops the string \"k\", or \"kr\", etc being detected\n\t\t// as a formatted number for currency\n\t\tif ( _empty( d ) ) {\n\t\t\treturn true;\n\t\t}\n\t\n\t\tif ( decimalPoint && strType ) {\n\t\t\td = _numToDecimal( d, decimalPoint );\n\t\t}\n\t\n\t\tif ( formatted && strType ) {\n\t\t\td = d.replace( _re_formatted_numeric, '' );\n\t\t}\n\t\n\t\treturn !isNaN( parseFloat(d) ) && isFinite( d );\n\t};\n\t\n\t\n\t// A string without HTML in it can be considered to be HTML still\n\tvar _isHtml = function ( d ) {\n\t\treturn _empty( d ) || typeof d === 'string';\n\t};\n\t\n\t\n\tvar _htmlNumeric = function ( d, decimalPoint, formatted ) {\n\t\tif ( _empty( d ) ) {\n\t\t\treturn true;\n\t\t}\n\t\n\t\tvar html = _isHtml( d );\n\t\treturn ! html ?\n\t\t\tnull :\n\t\t\t_isNumber( _stripHtml( d ), decimalPoint, formatted ) ?\n\t\t\t\ttrue :\n\t\t\t\tnull;\n\t};\n\t\n\t\n\tvar _pluck = function ( a, prop, prop2 ) {\n\t\tvar out = [];\n\t\tvar i=0, ien=a.length;\n\t\n\t\t// Could have the test in the loop for slightly smaller code, but speed\n\t\t// is essential here\n\t\tif ( prop2 !== undefined ) {\n\t\t\tfor ( ; i<ien ; i++ ) {\n\t\t\t\tif ( a[i] && a[i][ prop ] ) {\n\t\t\t\t\tout.push( a[i][ prop ][ prop2 ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tfor ( ; i<ien ; i++ ) {\n\t\t\t\tif ( a[i] ) {\n\t\t\t\t\tout.push( a[i][ prop ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\treturn out;\n\t};\n\t\n\t\n\t// Basically the same as _pluck, but rather than looping over `a` we use `order`\n\t// as the indexes to pick from `a`\n\tvar _pluck_order = function ( a, order, prop, prop2 )\n\t{\n\t\tvar out = [];\n\t\tvar i=0, ien=order.length;\n\t\n\t\t// Could have the test in the loop for slightly smaller code, but speed\n\t\t// is essential here\n\t\tif ( prop2 !== undefined ) {\n\t\t\tfor ( ; i<ien ; i++ ) {\n\t\t\t\tif ( a[ order[i] ][ prop ] ) {\n\t\t\t\t\tout.push( a[ order[i] ][ prop ][ prop2 ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tfor ( ; i<ien ; i++ ) {\n\t\t\t\tout.push( a[ order[i] ][ prop ] );\n\t\t\t}\n\t\t}\n\t\n\t\treturn out;\n\t};\n\t\n\t\n\tvar _range = function ( len, start )\n\t{\n\t\tvar out = [];\n\t\tvar end;\n\t\n\t\tif ( start === undefined ) {\n\t\t\tstart = 0;\n\t\t\tend = len;\n\t\t}\n\t\telse {\n\t\t\tend = start;\n\t\t\tstart = len;\n\t\t}\n\t\n\t\tfor ( var i=start ; i<end ; i++ ) {\n\t\t\tout.push( i );\n\t\t}\n\t\n\t\treturn out;\n\t};\n\t\n\t\n\tvar _removeEmpty = function ( a )\n\t{\n\t\tvar out = [];\n\t\n\t\tfor ( var i=0, ien=a.length ; i<ien ; i++ ) {\n\t\t\tif ( a[i] ) { // careful - will remove all falsy values!\n\t\t\t\tout.push( a[i] );\n\t\t\t}\n\t\t}\n\t\n\t\treturn out;\n\t};\n\t\n\t\n\tvar _stripHtml = function ( d ) {\n\t\treturn d.replace( _re_html, '' );\n\t};\n\t\n\t\n\t/**\n\t * Determine if all values in the array are unique. This means we can short\n\t * cut the _unique method at the cost of a single loop. A sorted array is used\n\t * to easily check the values.\n\t *\n\t * @param  {array} src Source array\n\t * @return {boolean} true if all unique, false otherwise\n\t * @ignore\n\t */\n\tvar _areAllUnique = function ( src ) {\n\t\tif ( src.length < 2 ) {\n\t\t\treturn true;\n\t\t}\n\t\n\t\tvar sorted = src.slice().sort();\n\t\tvar last = sorted[0];\n\t\n\t\tfor ( var i=1, ien=sorted.length ; i<ien ; i++ ) {\n\t\t\tif ( sorted[i] === last ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\n\t\t\tlast = sorted[i];\n\t\t}\n\t\n\t\treturn true;\n\t};\n\t\n\t\n\t/**\n\t * Find the unique elements in a source array.\n\t *\n\t * @param  {array} src Source array\n\t * @return {array} Array of unique items\n\t * @ignore\n\t */\n\tvar _unique = function ( src )\n\t{\n\t\tif ( _areAllUnique( src ) ) {\n\t\t\treturn src.slice();\n\t\t}\n\t\n\t\t// A faster unique method is to use object keys to identify used values,\n\t\t// but this doesn't work with arrays or objects, which we must also\n\t\t// consider. See jsperf.com/compare-array-unique-versions/4 for more\n\t\t// information.\n\t\tvar\n\t\t\tout = [],\n\t\t\tval,\n\t\t\ti, ien=src.length,\n\t\t\tj, k=0;\n\t\n\t\tagain: for ( i=0 ; i<ien ; i++ ) {\n\t\t\tval = src[i];\n\t\n\t\t\tfor ( j=0 ; j<k ; j++ ) {\n\t\t\t\tif ( out[j] === val ) {\n\t\t\t\t\tcontinue again;\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\tout.push( val );\n\t\t\tk++;\n\t\t}\n\t\n\t\treturn out;\n\t};\n\t\n\t\n\t/**\n\t * DataTables utility methods\n\t * \n\t * This namespace provides helper methods that DataTables uses internally to\n\t * create a DataTable, but which are not exclusively used only for DataTables.\n\t * These methods can be used by extension authors to save the duplication of\n\t * code.\n\t *\n\t *  @namespace\n\t */\n\tDataTable.util = {\n\t\t/**\n\t\t * Throttle the calls to a function. Arguments and context are maintained\n\t\t * for the throttled function.\n\t\t *\n\t\t * @param {function} fn Function to be called\n\t\t * @param {integer} freq Call frequency in mS\n\t\t * @return {function} Wrapped function\n\t\t */\n\t\tthrottle: function ( fn, freq ) {\n\t\t\tvar\n\t\t\t\tfrequency = freq !== undefined ? freq : 200,\n\t\t\t\tlast,\n\t\t\t\ttimer;\n\t\n\t\t\treturn function () {\n\t\t\t\tvar\n\t\t\t\t\tthat = this,\n\t\t\t\t\tnow  = +new Date(),\n\t\t\t\t\targs = arguments;\n\t\n\t\t\t\tif ( last && now < last + frequency ) {\n\t\t\t\t\tclearTimeout( timer );\n\t\n\t\t\t\t\ttimer = setTimeout( function () {\n\t\t\t\t\t\tlast = undefined;\n\t\t\t\t\t\tfn.apply( that, args );\n\t\t\t\t\t}, frequency );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tlast = now;\n\t\t\t\t\tfn.apply( that, args );\n\t\t\t\t}\n\t\t\t};\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * Escape a string such that it can be used in a regular expression\n\t\t *\n\t\t *  @param {string} val string to escape\n\t\t *  @returns {string} escaped string\n\t\t */\n\t\tescapeRegex: function ( val ) {\n\t\t\treturn val.replace( _re_escape_regex, '\\\\$1' );\n\t\t}\n\t};\n\t\n\t\n\t\n\t/**\n\t * Create a mapping object that allows camel case parameters to be looked up\n\t * for their Hungarian counterparts. The mapping is stored in a private\n\t * parameter called `_hungarianMap` which can be accessed on the source object.\n\t *  @param {object} o\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnHungarianMap ( o )\n\t{\n\t\tvar\n\t\t\thungarian = 'a aa ai ao as b fn i m o s ',\n\t\t\tmatch,\n\t\t\tnewKey,\n\t\t\tmap = {};\n\t\n\t\t$.each( o, function (key, val) {\n\t\t\tmatch = key.match(/^([^A-Z]+?)([A-Z])/);\n\t\n\t\t\tif ( match && hungarian.indexOf(match[1]+' ') !== -1 )\n\t\t\t{\n\t\t\t\tnewKey = key.replace( match[0], match[2].toLowerCase() );\n\t\t\t\tmap[ newKey ] = key;\n\t\n\t\t\t\tif ( match[1] === 'o' )\n\t\t\t\t{\n\t\t\t\t\t_fnHungarianMap( o[key] );\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t\n\t\to._hungarianMap = map;\n\t}\n\t\n\t\n\t/**\n\t * Convert from camel case parameters to Hungarian, based on a Hungarian map\n\t * created by _fnHungarianMap.\n\t *  @param {object} src The model object which holds all parameters that can be\n\t *    mapped.\n\t *  @param {object} user The object to convert from camel case to Hungarian.\n\t *  @param {boolean} force When set to `true`, properties which already have a\n\t *    Hungarian value in the `user` object will be overwritten. Otherwise they\n\t *    won't be.\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnCamelToHungarian ( src, user, force )\n\t{\n\t\tif ( ! src._hungarianMap ) {\n\t\t\t_fnHungarianMap( src );\n\t\t}\n\t\n\t\tvar hungarianKey;\n\t\n\t\t$.each( user, function (key, val) {\n\t\t\thungarianKey = src._hungarianMap[ key ];\n\t\n\t\t\tif ( hungarianKey !== undefined && (force || user[hungarianKey] === undefined) )\n\t\t\t{\n\t\t\t\t// For objects, we need to buzz down into the object to copy parameters\n\t\t\t\tif ( hungarianKey.charAt(0) === 'o' )\n\t\t\t\t{\n\t\t\t\t\t// Copy the camelCase options over to the hungarian\n\t\t\t\t\tif ( ! user[ hungarianKey ] ) {\n\t\t\t\t\t\tuser[ hungarianKey ] = {};\n\t\t\t\t\t}\n\t\t\t\t\t$.extend( true, user[hungarianKey], user[key] );\n\t\n\t\t\t\t\t_fnCamelToHungarian( src[hungarianKey], user[hungarianKey], force );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tuser[hungarianKey] = user[ key ];\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t}\n\t\n\t\n\t/**\n\t * Language compatibility - when certain options are given, and others aren't, we\n\t * need to duplicate the values over, in order to provide backwards compatibility\n\t * with older language files.\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnLanguageCompat( lang )\n\t{\n\t\t// Note the use of the Hungarian notation for the parameters in this method as\n\t\t// this is called after the mapping of camelCase to Hungarian\n\t\tvar defaults = DataTable.defaults.oLanguage;\n\t\n\t\t// Default mapping\n\t\tvar defaultDecimal = defaults.sDecimal;\n\t\tif ( defaultDecimal ) {\n\t\t\t_addNumericSort( defaultDecimal );\n\t\t}\n\t\n\t\tif ( lang ) {\n\t\t\tvar zeroRecords = lang.sZeroRecords;\n\t\n\t\t\t// Backwards compatibility - if there is no sEmptyTable given, then use the same as\n\t\t\t// sZeroRecords - assuming that is given.\n\t\t\tif ( ! lang.sEmptyTable && zeroRecords &&\n\t\t\t\tdefaults.sEmptyTable === \"No data available in table\" )\n\t\t\t{\n\t\t\t\t_fnMap( lang, lang, 'sZeroRecords', 'sEmptyTable' );\n\t\t\t}\n\t\n\t\t\t// Likewise with loading records\n\t\t\tif ( ! lang.sLoadingRecords && zeroRecords &&\n\t\t\t\tdefaults.sLoadingRecords === \"Loading...\" )\n\t\t\t{\n\t\t\t\t_fnMap( lang, lang, 'sZeroRecords', 'sLoadingRecords' );\n\t\t\t}\n\t\n\t\t\t// Old parameter name of the thousands separator mapped onto the new\n\t\t\tif ( lang.sInfoThousands ) {\n\t\t\t\tlang.sThousands = lang.sInfoThousands;\n\t\t\t}\n\t\n\t\t\tvar decimal = lang.sDecimal;\n\t\t\tif ( decimal && defaultDecimal !== decimal ) {\n\t\t\t\t_addNumericSort( decimal );\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Map one parameter onto another\n\t *  @param {object} o Object to map\n\t *  @param {*} knew The new parameter name\n\t *  @param {*} old The old parameter name\n\t */\n\tvar _fnCompatMap = function ( o, knew, old ) {\n\t\tif ( o[ knew ] !== undefined ) {\n\t\t\to[ old ] = o[ knew ];\n\t\t}\n\t};\n\t\n\t\n\t/**\n\t * Provide backwards compatibility for the main DT options. Note that the new\n\t * options are mapped onto the old parameters, so this is an external interface\n\t * change only.\n\t *  @param {object} init Object to map\n\t */\n\tfunction _fnCompatOpts ( init )\n\t{\n\t\t_fnCompatMap( init, 'ordering',      'bSort' );\n\t\t_fnCompatMap( init, 'orderMulti',    'bSortMulti' );\n\t\t_fnCompatMap( init, 'orderClasses',  'bSortClasses' );\n\t\t_fnCompatMap( init, 'orderCellsTop', 'bSortCellsTop' );\n\t\t_fnCompatMap( init, 'order',         'aaSorting' );\n\t\t_fnCompatMap( init, 'orderFixed',    'aaSortingFixed' );\n\t\t_fnCompatMap( init, 'paging',        'bPaginate' );\n\t\t_fnCompatMap( init, 'pagingType',    'sPaginationType' );\n\t\t_fnCompatMap( init, 'pageLength',    'iDisplayLength' );\n\t\t_fnCompatMap( init, 'searching',     'bFilter' );\n\t\n\t\t// Boolean initialisation of x-scrolling\n\t\tif ( typeof init.sScrollX === 'boolean' ) {\n\t\t\tinit.sScrollX = init.sScrollX ? '100%' : '';\n\t\t}\n\t\tif ( typeof init.scrollX === 'boolean' ) {\n\t\t\tinit.scrollX = init.scrollX ? '100%' : '';\n\t\t}\n\t\n\t\t// Column search objects are in an array, so it needs to be converted\n\t\t// element by element\n\t\tvar searchCols = init.aoSearchCols;\n\t\n\t\tif ( searchCols ) {\n\t\t\tfor ( var i=0, ien=searchCols.length ; i<ien ; i++ ) {\n\t\t\t\tif ( searchCols[i] ) {\n\t\t\t\t\t_fnCamelToHungarian( DataTable.models.oSearch, searchCols[i] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Provide backwards compatibility for column options. Note that the new options\n\t * are mapped onto the old parameters, so this is an external interface change\n\t * only.\n\t *  @param {object} init Object to map\n\t */\n\tfunction _fnCompatCols ( init )\n\t{\n\t\t_fnCompatMap( init, 'orderable',     'bSortable' );\n\t\t_fnCompatMap( init, 'orderData',     'aDataSort' );\n\t\t_fnCompatMap( init, 'orderSequence', 'asSorting' );\n\t\t_fnCompatMap( init, 'orderDataType', 'sortDataType' );\n\t\n\t\t// orderData can be given as an integer\n\t\tvar dataSort = init.aDataSort;\n\t\tif ( typeof dataSort === 'number' && ! $.isArray( dataSort ) ) {\n\t\t\tinit.aDataSort = [ dataSort ];\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Browser feature detection for capabilities, quirks\n\t *  @param {object} settings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnBrowserDetect( settings )\n\t{\n\t\t// We don't need to do this every time DataTables is constructed, the values\n\t\t// calculated are specific to the browser and OS configuration which we\n\t\t// don't expect to change between initialisations\n\t\tif ( ! DataTable.__browser ) {\n\t\t\tvar browser = {};\n\t\t\tDataTable.__browser = browser;\n\t\n\t\t\t// Scrolling feature / quirks detection\n\t\t\tvar n = $('<div/>')\n\t\t\t\t.css( {\n\t\t\t\t\tposition: 'fixed',\n\t\t\t\t\ttop: 0,\n\t\t\t\t\tleft: $(window).scrollLeft()*-1, // allow for scrolling\n\t\t\t\t\theight: 1,\n\t\t\t\t\twidth: 1,\n\t\t\t\t\toverflow: 'hidden'\n\t\t\t\t} )\n\t\t\t\t.append(\n\t\t\t\t\t$('<div/>')\n\t\t\t\t\t\t.css( {\n\t\t\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\t\t\ttop: 1,\n\t\t\t\t\t\t\tleft: 1,\n\t\t\t\t\t\t\twidth: 100,\n\t\t\t\t\t\t\toverflow: 'scroll'\n\t\t\t\t\t\t} )\n\t\t\t\t\t\t.append(\n\t\t\t\t\t\t\t$('<div/>')\n\t\t\t\t\t\t\t\t.css( {\n\t\t\t\t\t\t\t\t\twidth: '100%',\n\t\t\t\t\t\t\t\t\theight: 10\n\t\t\t\t\t\t\t\t} )\n\t\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t\t.appendTo( 'body' );\n\t\n\t\t\tvar outer = n.children();\n\t\t\tvar inner = outer.children();\n\t\n\t\t\t// Numbers below, in order, are:\n\t\t\t// inner.offsetWidth, inner.clientWidth, outer.offsetWidth, outer.clientWidth\n\t\t\t//\n\t\t\t// IE6 XP:                           100 100 100  83\n\t\t\t// IE7 Vista:                        100 100 100  83\n\t\t\t// IE 8+ Windows:                     83  83 100  83\n\t\t\t// Evergreen Windows:                 83  83 100  83\n\t\t\t// Evergreen Mac with scrollbars:     85  85 100  85\n\t\t\t// Evergreen Mac without scrollbars: 100 100 100 100\n\t\n\t\t\t// Get scrollbar width\n\t\t\tbrowser.barWidth = outer[0].offsetWidth - outer[0].clientWidth;\n\t\n\t\t\t// IE6/7 will oversize a width 100% element inside a scrolling element, to\n\t\t\t// include the width of the scrollbar, while other browsers ensure the inner\n\t\t\t// element is contained without forcing scrolling\n\t\t\tbrowser.bScrollOversize = inner[0].offsetWidth === 100 && outer[0].clientWidth !== 100;\n\t\n\t\t\t// In rtl text layout, some browsers (most, but not all) will place the\n\t\t\t// scrollbar on the left, rather than the right.\n\t\t\tbrowser.bScrollbarLeft = Math.round( inner.offset().left ) !== 1;\n\t\n\t\t\t// IE8- don't provide height and width for getBoundingClientRect\n\t\t\tbrowser.bBounding = n[0].getBoundingClientRect().width ? true : false;\n\t\n\t\t\tn.remove();\n\t\t}\n\t\n\t\t$.extend( settings.oBrowser, DataTable.__browser );\n\t\tsettings.oScroll.iBarWidth = DataTable.__browser.barWidth;\n\t}\n\t\n\t\n\t/**\n\t * Array.prototype reduce[Right] method, used for browsers which don't support\n\t * JS 1.6. Done this way to reduce code size, since we iterate either way\n\t *  @param {object} settings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnReduce ( that, fn, init, start, end, inc )\n\t{\n\t\tvar\n\t\t\ti = start,\n\t\t\tvalue,\n\t\t\tisSet = false;\n\t\n\t\tif ( init !== undefined ) {\n\t\t\tvalue = init;\n\t\t\tisSet = true;\n\t\t}\n\t\n\t\twhile ( i !== end ) {\n\t\t\tif ( ! that.hasOwnProperty(i) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\n\t\t\tvalue = isSet ?\n\t\t\t\tfn( value, that[i], i, that ) :\n\t\t\t\tthat[i];\n\t\n\t\t\tisSet = true;\n\t\t\ti += inc;\n\t\t}\n\t\n\t\treturn value;\n\t}\n\t\n\t/**\n\t * Add a column to the list used for the table with default values\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {node} nTh The th element for this column\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnAddColumn( oSettings, nTh )\n\t{\n\t\t// Add column to aoColumns array\n\t\tvar oDefaults = DataTable.defaults.column;\n\t\tvar iCol = oSettings.aoColumns.length;\n\t\tvar oCol = $.extend( {}, DataTable.models.oColumn, oDefaults, {\n\t\t\t\"nTh\": nTh ? nTh : document.createElement('th'),\n\t\t\t\"sTitle\":    oDefaults.sTitle    ? oDefaults.sTitle    : nTh ? nTh.innerHTML : '',\n\t\t\t\"aDataSort\": oDefaults.aDataSort ? oDefaults.aDataSort : [iCol],\n\t\t\t\"mData\": oDefaults.mData ? oDefaults.mData : iCol,\n\t\t\tidx: iCol\n\t\t} );\n\t\toSettings.aoColumns.push( oCol );\n\t\n\t\t// Add search object for column specific search. Note that the `searchCols[ iCol ]`\n\t\t// passed into extend can be undefined. This allows the user to give a default\n\t\t// with only some of the parameters defined, and also not give a default\n\t\tvar searchCols = oSettings.aoPreSearchCols;\n\t\tsearchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch, searchCols[ iCol ] );\n\t\n\t\t// Use the default column options function to initialise classes etc\n\t\t_fnColumnOptions( oSettings, iCol, $(nTh).data() );\n\t}\n\t\n\t\n\t/**\n\t * Apply options for a column\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {int} iCol column index to consider\n\t *  @param {object} oOptions object with sType, bVisible and bSearchable etc\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnColumnOptions( oSettings, iCol, oOptions )\n\t{\n\t\tvar oCol = oSettings.aoColumns[ iCol ];\n\t\tvar oClasses = oSettings.oClasses;\n\t\tvar th = $(oCol.nTh);\n\t\n\t\t// Try to get width information from the DOM. We can't get it from CSS\n\t\t// as we'd need to parse the CSS stylesheet. `width` option can override\n\t\tif ( ! oCol.sWidthOrig ) {\n\t\t\t// Width attribute\n\t\t\toCol.sWidthOrig = th.attr('width') || null;\n\t\n\t\t\t// Style attribute\n\t\t\tvar t = (th.attr('style') || '').match(/width:\\s*(\\d+[pxem%]+)/);\n\t\t\tif ( t ) {\n\t\t\t\toCol.sWidthOrig = t[1];\n\t\t\t}\n\t\t}\n\t\n\t\t/* User specified column options */\n\t\tif ( oOptions !== undefined && oOptions !== null )\n\t\t{\n\t\t\t// Backwards compatibility\n\t\t\t_fnCompatCols( oOptions );\n\t\n\t\t\t// Map camel case parameters to their Hungarian counterparts\n\t\t\t_fnCamelToHungarian( DataTable.defaults.column, oOptions, true );\n\t\n\t\t\t/* Backwards compatibility for mDataProp */\n\t\t\tif ( oOptions.mDataProp !== undefined && !oOptions.mData )\n\t\t\t{\n\t\t\t\toOptions.mData = oOptions.mDataProp;\n\t\t\t}\n\t\n\t\t\tif ( oOptions.sType )\n\t\t\t{\n\t\t\t\toCol._sManualType = oOptions.sType;\n\t\t\t}\n\t\n\t\t\t// `class` is a reserved word in Javascript, so we need to provide\n\t\t\t// the ability to use a valid name for the camel case input\n\t\t\tif ( oOptions.className && ! oOptions.sClass )\n\t\t\t{\n\t\t\t\toOptions.sClass = oOptions.className;\n\t\t\t}\n\t\t\tif ( oOptions.sClass ) {\n\t\t\t\tth.addClass( oOptions.sClass );\n\t\t\t}\n\t\n\t\t\t$.extend( oCol, oOptions );\n\t\t\t_fnMap( oCol, oOptions, \"sWidth\", \"sWidthOrig\" );\n\t\n\t\t\t/* iDataSort to be applied (backwards compatibility), but aDataSort will take\n\t\t\t * priority if defined\n\t\t\t */\n\t\t\tif ( oOptions.iDataSort !== undefined )\n\t\t\t{\n\t\t\t\toCol.aDataSort = [ oOptions.iDataSort ];\n\t\t\t}\n\t\t\t_fnMap( oCol, oOptions, \"aDataSort\" );\n\t\t}\n\t\n\t\t/* Cache the data get and set functions for speed */\n\t\tvar mDataSrc = oCol.mData;\n\t\tvar mData = _fnGetObjectDataFn( mDataSrc );\n\t\tvar mRender = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null;\n\t\n\t\tvar attrTest = function( src ) {\n\t\t\treturn typeof src === 'string' && src.indexOf('@') !== -1;\n\t\t};\n\t\toCol._bAttrSrc = $.isPlainObject( mDataSrc ) && (\n\t\t\tattrTest(mDataSrc.sort) || attrTest(mDataSrc.type) || attrTest(mDataSrc.filter)\n\t\t);\n\t\toCol._setter = null;\n\t\n\t\toCol.fnGetData = function (rowData, type, meta) {\n\t\t\tvar innerData = mData( rowData, type, undefined, meta );\n\t\n\t\t\treturn mRender && type ?\n\t\t\t\tmRender( innerData, type, rowData, meta ) :\n\t\t\t\tinnerData;\n\t\t};\n\t\toCol.fnSetData = function ( rowData, val, meta ) {\n\t\t\treturn _fnSetObjectDataFn( mDataSrc )( rowData, val, meta );\n\t\t};\n\t\n\t\t// Indicate if DataTables should read DOM data as an object or array\n\t\t// Used in _fnGetRowElements\n\t\tif ( typeof mDataSrc !== 'number' ) {\n\t\t\toSettings._rowReadObject = true;\n\t\t}\n\t\n\t\t/* Feature sorting overrides column specific when off */\n\t\tif ( !oSettings.oFeatures.bSort )\n\t\t{\n\t\t\toCol.bSortable = false;\n\t\t\tth.addClass( oClasses.sSortableNone ); // Have to add class here as order event isn't called\n\t\t}\n\t\n\t\t/* Check that the class assignment is correct for sorting */\n\t\tvar bAsc = $.inArray('asc', oCol.asSorting) !== -1;\n\t\tvar bDesc = $.inArray('desc', oCol.asSorting) !== -1;\n\t\tif ( !oCol.bSortable || (!bAsc && !bDesc) )\n\t\t{\n\t\t\toCol.sSortingClass = oClasses.sSortableNone;\n\t\t\toCol.sSortingClassJUI = \"\";\n\t\t}\n\t\telse if ( bAsc && !bDesc )\n\t\t{\n\t\t\toCol.sSortingClass = oClasses.sSortableAsc;\n\t\t\toCol.sSortingClassJUI = oClasses.sSortJUIAscAllowed;\n\t\t}\n\t\telse if ( !bAsc && bDesc )\n\t\t{\n\t\t\toCol.sSortingClass = oClasses.sSortableDesc;\n\t\t\toCol.sSortingClassJUI = oClasses.sSortJUIDescAllowed;\n\t\t}\n\t\telse\n\t\t{\n\t\t\toCol.sSortingClass = oClasses.sSortable;\n\t\t\toCol.sSortingClassJUI = oClasses.sSortJUI;\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Adjust the table column widths for new data. Note: you would probably want to\n\t * do a redraw after calling this function!\n\t *  @param {object} settings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnAdjustColumnSizing ( settings )\n\t{\n\t\t/* Not interested in doing column width calculation if auto-width is disabled */\n\t\tif ( settings.oFeatures.bAutoWidth !== false )\n\t\t{\n\t\t\tvar columns = settings.aoColumns;\n\t\n\t\t\t_fnCalculateColumnWidths( settings );\n\t\t\tfor ( var i=0 , iLen=columns.length ; i<iLen ; i++ )\n\t\t\t{\n\t\t\t\tcolumns[i].nTh.style.width = columns[i].sWidth;\n\t\t\t}\n\t\t}\n\t\n\t\tvar scroll = settings.oScroll;\n\t\tif ( scroll.sY !== '' || scroll.sX !== '')\n\t\t{\n\t\t\t_fnScrollDraw( settings );\n\t\t}\n\t\n\t\t_fnCallbackFire( settings, null, 'column-sizing', [settings] );\n\t}\n\t\n\t\n\t/**\n\t * Covert the index of a visible column to the index in the data array (take account\n\t * of hidden columns)\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {int} iMatch Visible column index to lookup\n\t *  @returns {int} i the data index\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnVisibleToColumnIndex( oSettings, iMatch )\n\t{\n\t\tvar aiVis = _fnGetColumns( oSettings, 'bVisible' );\n\t\n\t\treturn typeof aiVis[iMatch] === 'number' ?\n\t\t\taiVis[iMatch] :\n\t\t\tnull;\n\t}\n\t\n\t\n\t/**\n\t * Covert the index of an index in the data array and convert it to the visible\n\t *   column index (take account of hidden columns)\n\t *  @param {int} iMatch Column index to lookup\n\t *  @param {object} oSettings dataTables settings object\n\t *  @returns {int} i the data index\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnColumnIndexToVisible( oSettings, iMatch )\n\t{\n\t\tvar aiVis = _fnGetColumns( oSettings, 'bVisible' );\n\t\tvar iPos = $.inArray( iMatch, aiVis );\n\t\n\t\treturn iPos !== -1 ? iPos : null;\n\t}\n\t\n\t\n\t/**\n\t * Get the number of visible columns\n\t *  @param {object} oSettings dataTables settings object\n\t *  @returns {int} i the number of visible columns\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnVisbleColumns( oSettings )\n\t{\n\t\tvar vis = 0;\n\t\n\t\t// No reduce in IE8, use a loop for now\n\t\t$.each( oSettings.aoColumns, function ( i, col ) {\n\t\t\tif ( col.bVisible && $(col.nTh).css('display') !== 'none' ) {\n\t\t\t\tvis++;\n\t\t\t}\n\t\t} );\n\t\n\t\treturn vis;\n\t}\n\t\n\t\n\t/**\n\t * Get an array of column indexes that match a given property\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {string} sParam Parameter in aoColumns to look for - typically\n\t *    bVisible or bSearchable\n\t *  @returns {array} Array of indexes with matched properties\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnGetColumns( oSettings, sParam )\n\t{\n\t\tvar a = [];\n\t\n\t\t$.map( oSettings.aoColumns, function(val, i) {\n\t\t\tif ( val[sParam] ) {\n\t\t\t\ta.push( i );\n\t\t\t}\n\t\t} );\n\t\n\t\treturn a;\n\t}\n\t\n\t\n\t/**\n\t * Calculate the 'type' of a column\n\t *  @param {object} settings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnColumnTypes ( settings )\n\t{\n\t\tvar columns = settings.aoColumns;\n\t\tvar data = settings.aoData;\n\t\tvar types = DataTable.ext.type.detect;\n\t\tvar i, ien, j, jen, k, ken;\n\t\tvar col, cell, detectedType, cache;\n\t\n\t\t// For each column, spin over the \n\t\tfor ( i=0, ien=columns.length ; i<ien ; i++ ) {\n\t\t\tcol = columns[i];\n\t\t\tcache = [];\n\t\n\t\t\tif ( ! col.sType && col._sManualType ) {\n\t\t\t\tcol.sType = col._sManualType;\n\t\t\t}\n\t\t\telse if ( ! col.sType ) {\n\t\t\t\tfor ( j=0, jen=types.length ; j<jen ; j++ ) {\n\t\t\t\t\tfor ( k=0, ken=data.length ; k<ken ; k++ ) {\n\t\t\t\t\t\t// Use a cache array so we only need to get the type data\n\t\t\t\t\t\t// from the formatter once (when using multiple detectors)\n\t\t\t\t\t\tif ( cache[k] === undefined ) {\n\t\t\t\t\t\t\tcache[k] = _fnGetCellData( settings, k, i, 'type' );\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\tdetectedType = types[j]( cache[k], settings );\n\t\n\t\t\t\t\t\t// If null, then this type can't apply to this column, so\n\t\t\t\t\t\t// rather than testing all cells, break out. There is an\n\t\t\t\t\t\t// exception for the last type which is `html`. We need to\n\t\t\t\t\t\t// scan all rows since it is possible to mix string and HTML\n\t\t\t\t\t\t// types\n\t\t\t\t\t\tif ( ! detectedType && j !== types.length-1 ) {\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t// Only a single match is needed for html type since it is\n\t\t\t\t\t\t// bottom of the pile and very similar to string\n\t\t\t\t\t\tif ( detectedType === 'html' ) {\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\n\t\t\t\t\t// Type is valid for all data points in the column - use this\n\t\t\t\t\t// type\n\t\t\t\t\tif ( detectedType ) {\n\t\t\t\t\t\tcol.sType = detectedType;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\t// Fall back - if no type was detected, always use string\n\t\t\t\tif ( ! col.sType ) {\n\t\t\t\t\tcol.sType = 'string';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Take the column definitions and static columns arrays and calculate how\n\t * they relate to column indexes. The callback function will then apply the\n\t * definition found for a column to a suitable configuration object.\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {array} aoColDefs The aoColumnDefs array that is to be applied\n\t *  @param {array} aoCols The aoColumns array that defines columns individually\n\t *  @param {function} fn Callback function - takes two parameters, the calculated\n\t *    column index and the definition for that column.\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnApplyColumnDefs( oSettings, aoColDefs, aoCols, fn )\n\t{\n\t\tvar i, iLen, j, jLen, k, kLen, def;\n\t\tvar columns = oSettings.aoColumns;\n\t\n\t\t// Column definitions with aTargets\n\t\tif ( aoColDefs )\n\t\t{\n\t\t\t/* Loop over the definitions array - loop in reverse so first instance has priority */\n\t\t\tfor ( i=aoColDefs.length-1 ; i>=0 ; i-- )\n\t\t\t{\n\t\t\t\tdef = aoColDefs[i];\n\t\n\t\t\t\t/* Each definition can target multiple columns, as it is an array */\n\t\t\t\tvar aTargets = def.targets !== undefined ?\n\t\t\t\t\tdef.targets :\n\t\t\t\t\tdef.aTargets;\n\t\n\t\t\t\tif ( ! $.isArray( aTargets ) )\n\t\t\t\t{\n\t\t\t\t\taTargets = [ aTargets ];\n\t\t\t\t}\n\t\n\t\t\t\tfor ( j=0, jLen=aTargets.length ; j<jLen ; j++ )\n\t\t\t\t{\n\t\t\t\t\tif ( typeof aTargets[j] === 'number' && aTargets[j] >= 0 )\n\t\t\t\t\t{\n\t\t\t\t\t\t/* Add columns that we don't yet know about */\n\t\t\t\t\t\twhile( columns.length <= aTargets[j] )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t_fnAddColumn( oSettings );\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t/* Integer, basic index */\n\t\t\t\t\t\tfn( aTargets[j], def );\n\t\t\t\t\t}\n\t\t\t\t\telse if ( typeof aTargets[j] === 'number' && aTargets[j] < 0 )\n\t\t\t\t\t{\n\t\t\t\t\t\t/* Negative integer, right to left column counting */\n\t\t\t\t\t\tfn( columns.length+aTargets[j], def );\n\t\t\t\t\t}\n\t\t\t\t\telse if ( typeof aTargets[j] === 'string' )\n\t\t\t\t\t{\n\t\t\t\t\t\t/* Class name matching on TH element */\n\t\t\t\t\t\tfor ( k=0, kLen=columns.length ; k<kLen ; k++ )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif ( aTargets[j] == \"_all\" ||\n\t\t\t\t\t\t\t     $(columns[k].nTh).hasClass( aTargets[j] ) )\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tfn( k, def );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\t// Statically defined columns array\n\t\tif ( aoCols )\n\t\t{\n\t\t\tfor ( i=0, iLen=aoCols.length ; i<iLen ; i++ )\n\t\t\t{\n\t\t\t\tfn( i, aoCols[i] );\n\t\t\t}\n\t\t}\n\t}\n\t\n\t/**\n\t * Add a data array to the table, creating DOM node etc. This is the parallel to\n\t * _fnGatherData, but for adding rows from a Javascript source, rather than a\n\t * DOM source.\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {array} aData data array to be added\n\t *  @param {node} [nTr] TR element to add to the table - optional. If not given,\n\t *    DataTables will create a row automatically\n\t *  @param {array} [anTds] Array of TD|TH elements for the row - must be given\n\t *    if nTr is.\n\t *  @returns {int} >=0 if successful (index of new aoData entry), -1 if failed\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnAddData ( oSettings, aDataIn, nTr, anTds )\n\t{\n\t\t/* Create the object for storing information about this new row */\n\t\tvar iRow = oSettings.aoData.length;\n\t\tvar oData = $.extend( true, {}, DataTable.models.oRow, {\n\t\t\tsrc: nTr ? 'dom' : 'data',\n\t\t\tidx: iRow\n\t\t} );\n\t\n\t\toData._aData = aDataIn;\n\t\toSettings.aoData.push( oData );\n\t\n\t\t/* Create the cells */\n\t\tvar nTd, sThisType;\n\t\tvar columns = oSettings.aoColumns;\n\t\n\t\t// Invalidate the column types as the new data needs to be revalidated\n\t\tfor ( var i=0, iLen=columns.length ; i<iLen ; i++ )\n\t\t{\n\t\t\tcolumns[i].sType = null;\n\t\t}\n\t\n\t\t/* Add to the display array */\n\t\toSettings.aiDisplayMaster.push( iRow );\n\t\n\t\tvar id = oSettings.rowIdFn( aDataIn );\n\t\tif ( id !== undefined ) {\n\t\t\toSettings.aIds[ id ] = oData;\n\t\t}\n\t\n\t\t/* Create the DOM information, or register it if already present */\n\t\tif ( nTr || ! oSettings.oFeatures.bDeferRender )\n\t\t{\n\t\t\t_fnCreateTr( oSettings, iRow, nTr, anTds );\n\t\t}\n\t\n\t\treturn iRow;\n\t}\n\t\n\t\n\t/**\n\t * Add one or more TR elements to the table. Generally we'd expect to\n\t * use this for reading data from a DOM sourced table, but it could be\n\t * used for an TR element. Note that if a TR is given, it is used (i.e.\n\t * it is not cloned).\n\t *  @param {object} settings dataTables settings object\n\t *  @param {array|node|jQuery} trs The TR element(s) to add to the table\n\t *  @returns {array} Array of indexes for the added rows\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnAddTr( settings, trs )\n\t{\n\t\tvar row;\n\t\n\t\t// Allow an individual node to be passed in\n\t\tif ( ! (trs instanceof $) ) {\n\t\t\ttrs = $(trs);\n\t\t}\n\t\n\t\treturn trs.map( function (i, el) {\n\t\t\trow = _fnGetRowElements( settings, el );\n\t\t\treturn _fnAddData( settings, row.data, el, row.cells );\n\t\t} );\n\t}\n\t\n\t\n\t/**\n\t * Take a TR element and convert it to an index in aoData\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {node} n the TR element to find\n\t *  @returns {int} index if the node is found, null if not\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnNodeToDataIndex( oSettings, n )\n\t{\n\t\treturn (n._DT_RowIndex!==undefined) ? n._DT_RowIndex : null;\n\t}\n\t\n\t\n\t/**\n\t * Take a TD element and convert it into a column data index (not the visible index)\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {int} iRow The row number the TD/TH can be found in\n\t *  @param {node} n The TD/TH element to find\n\t *  @returns {int} index if the node is found, -1 if not\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnNodeToColumnIndex( oSettings, iRow, n )\n\t{\n\t\treturn $.inArray( n, oSettings.aoData[ iRow ].anCells );\n\t}\n\t\n\t\n\t/**\n\t * Get the data for a given cell from the internal cache, taking into account data mapping\n\t *  @param {object} settings dataTables settings object\n\t *  @param {int} rowIdx aoData row id\n\t *  @param {int} colIdx Column index\n\t *  @param {string} type data get type ('display', 'type' 'filter' 'sort')\n\t *  @returns {*} Cell data\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnGetCellData( settings, rowIdx, colIdx, type )\n\t{\n\t\tvar draw           = settings.iDraw;\n\t\tvar col            = settings.aoColumns[colIdx];\n\t\tvar rowData        = settings.aoData[rowIdx]._aData;\n\t\tvar defaultContent = col.sDefaultContent;\n\t\tvar cellData       = col.fnGetData( rowData, type, {\n\t\t\tsettings: settings,\n\t\t\trow:      rowIdx,\n\t\t\tcol:      colIdx\n\t\t} );\n\t\n\t\tif ( cellData === undefined ) {\n\t\t\tif ( settings.iDrawError != draw && defaultContent === null ) {\n\t\t\t\t_fnLog( settings, 0, \"Requested unknown parameter \"+\n\t\t\t\t\t(typeof col.mData=='function' ? '{function}' : \"'\"+col.mData+\"'\")+\n\t\t\t\t\t\" for row \"+rowIdx+\", column \"+colIdx, 4 );\n\t\t\t\tsettings.iDrawError = draw;\n\t\t\t}\n\t\t\treturn defaultContent;\n\t\t}\n\t\n\t\t// When the data source is null and a specific data type is requested (i.e.\n\t\t// not the original data), we can use default column data\n\t\tif ( (cellData === rowData || cellData === null) && defaultContent !== null && type !== undefined ) {\n\t\t\tcellData = defaultContent;\n\t\t}\n\t\telse if ( typeof cellData === 'function' ) {\n\t\t\t// If the data source is a function, then we run it and use the return,\n\t\t\t// executing in the scope of the data object (for instances)\n\t\t\treturn cellData.call( rowData );\n\t\t}\n\t\n\t\tif ( cellData === null && type == 'display' ) {\n\t\t\treturn '';\n\t\t}\n\t\treturn cellData;\n\t}\n\t\n\t\n\t/**\n\t * Set the value for a specific cell, into the internal data cache\n\t *  @param {object} settings dataTables settings object\n\t *  @param {int} rowIdx aoData row id\n\t *  @param {int} colIdx Column index\n\t *  @param {*} val Value to set\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnSetCellData( settings, rowIdx, colIdx, val )\n\t{\n\t\tvar col     = settings.aoColumns[colIdx];\n\t\tvar rowData = settings.aoData[rowIdx]._aData;\n\t\n\t\tcol.fnSetData( rowData, val, {\n\t\t\tsettings: settings,\n\t\t\trow:      rowIdx,\n\t\t\tcol:      colIdx\n\t\t}  );\n\t}\n\t\n\t\n\t// Private variable that is used to match action syntax in the data property object\n\tvar __reArray = /\\[.*?\\]$/;\n\tvar __reFn = /\\(\\)$/;\n\t\n\t/**\n\t * Split string on periods, taking into account escaped periods\n\t * @param  {string} str String to split\n\t * @return {array} Split string\n\t */\n\tfunction _fnSplitObjNotation( str )\n\t{\n\t\treturn $.map( str.match(/(\\\\.|[^\\.])+/g) || [''], function ( s ) {\n\t\t\treturn s.replace(/\\\\\\./g, '.');\n\t\t} );\n\t}\n\t\n\t\n\t/**\n\t * Return a function that can be used to get data from a source object, taking\n\t * into account the ability to use nested objects as a source\n\t *  @param {string|int|function} mSource The data source for the object\n\t *  @returns {function} Data get function\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnGetObjectDataFn( mSource )\n\t{\n\t\tif ( $.isPlainObject( mSource ) )\n\t\t{\n\t\t\t/* Build an object of get functions, and wrap them in a single call */\n\t\t\tvar o = {};\n\t\t\t$.each( mSource, function (key, val) {\n\t\t\t\tif ( val ) {\n\t\t\t\t\to[key] = _fnGetObjectDataFn( val );\n\t\t\t\t}\n\t\t\t} );\n\t\n\t\t\treturn function (data, type, row, meta) {\n\t\t\t\tvar t = o[type] || o._;\n\t\t\t\treturn t !== undefined ?\n\t\t\t\t\tt(data, type, row, meta) :\n\t\t\t\t\tdata;\n\t\t\t};\n\t\t}\n\t\telse if ( mSource === null )\n\t\t{\n\t\t\t/* Give an empty string for rendering / sorting etc */\n\t\t\treturn function (data) { // type, row and meta also passed, but not used\n\t\t\t\treturn data;\n\t\t\t};\n\t\t}\n\t\telse if ( typeof mSource === 'function' )\n\t\t{\n\t\t\treturn function (data, type, row, meta) {\n\t\t\t\treturn mSource( data, type, row, meta );\n\t\t\t};\n\t\t}\n\t\telse if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 ||\n\t\t\t      mSource.indexOf('[') !== -1 || mSource.indexOf('(') !== -1) )\n\t\t{\n\t\t\t/* If there is a . in the source string then the data source is in a\n\t\t\t * nested object so we loop over the data for each level to get the next\n\t\t\t * level down. On each loop we test for undefined, and if found immediately\n\t\t\t * return. This allows entire objects to be missing and sDefaultContent to\n\t\t\t * be used if defined, rather than throwing an error\n\t\t\t */\n\t\t\tvar fetchData = function (data, type, src) {\n\t\t\t\tvar arrayNotation, funcNotation, out, innerSrc;\n\t\n\t\t\t\tif ( src !== \"\" )\n\t\t\t\t{\n\t\t\t\t\tvar a = _fnSplitObjNotation( src );\n\t\n\t\t\t\t\tfor ( var i=0, iLen=a.length ; i<iLen ; i++ )\n\t\t\t\t\t{\n\t\t\t\t\t\t// Check if we are dealing with special notation\n\t\t\t\t\t\tarrayNotation = a[i].match(__reArray);\n\t\t\t\t\t\tfuncNotation = a[i].match(__reFn);\n\t\n\t\t\t\t\t\tif ( arrayNotation )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t// Array notation\n\t\t\t\t\t\t\ta[i] = a[i].replace(__reArray, '');\n\t\n\t\t\t\t\t\t\t// Condition allows simply [] to be passed in\n\t\t\t\t\t\t\tif ( a[i] !== \"\" ) {\n\t\t\t\t\t\t\t\tdata = data[ a[i] ];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tout = [];\n\t\n\t\t\t\t\t\t\t// Get the remainder of the nested object to get\n\t\t\t\t\t\t\ta.splice( 0, i+1 );\n\t\t\t\t\t\t\tinnerSrc = a.join('.');\n\t\n\t\t\t\t\t\t\t// Traverse each entry in the array getting the properties requested\n\t\t\t\t\t\t\tif ( $.isArray( data ) ) {\n\t\t\t\t\t\t\t\tfor ( var j=0, jLen=data.length ; j<jLen ; j++ ) {\n\t\t\t\t\t\t\t\t\tout.push( fetchData( data[j], type, innerSrc ) );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t\t// If a string is given in between the array notation indicators, that\n\t\t\t\t\t\t\t// is used to join the strings together, otherwise an array is returned\n\t\t\t\t\t\t\tvar join = arrayNotation[0].substring(1, arrayNotation[0].length-1);\n\t\t\t\t\t\t\tdata = (join===\"\") ? out : out.join(join);\n\t\n\t\t\t\t\t\t\t// The inner call to fetchData has already traversed through the remainder\n\t\t\t\t\t\t\t// of the source requested, so we exit from the loop\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse if ( funcNotation )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t// Function call\n\t\t\t\t\t\t\ta[i] = a[i].replace(__reFn, '');\n\t\t\t\t\t\t\tdata = data[ a[i] ]();\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\tif ( data === null || data[ a[i] ] === undefined )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\treturn undefined;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdata = data[ a[i] ];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\treturn data;\n\t\t\t};\n\t\n\t\t\treturn function (data, type) { // row and meta also passed, but not used\n\t\t\t\treturn fetchData( data, type, mSource );\n\t\t\t};\n\t\t}\n\t\telse\n\t\t{\n\t\t\t/* Array or flat object mapping */\n\t\t\treturn function (data, type) { // row and meta also passed, but not used\n\t\t\t\treturn data[mSource];\n\t\t\t};\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Return a function that can be used to set data from a source object, taking\n\t * into account the ability to use nested objects as a source\n\t *  @param {string|int|function} mSource The data source for the object\n\t *  @returns {function} Data set function\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnSetObjectDataFn( mSource )\n\t{\n\t\tif ( $.isPlainObject( mSource ) )\n\t\t{\n\t\t\t/* Unlike get, only the underscore (global) option is used for for\n\t\t\t * setting data since we don't know the type here. This is why an object\n\t\t\t * option is not documented for `mData` (which is read/write), but it is\n\t\t\t * for `mRender` which is read only.\n\t\t\t */\n\t\t\treturn _fnSetObjectDataFn( mSource._ );\n\t\t}\n\t\telse if ( mSource === null )\n\t\t{\n\t\t\t/* Nothing to do when the data source is null */\n\t\t\treturn function () {};\n\t\t}\n\t\telse if ( typeof mSource === 'function' )\n\t\t{\n\t\t\treturn function (data, val, meta) {\n\t\t\t\tmSource( data, 'set', val, meta );\n\t\t\t};\n\t\t}\n\t\telse if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 ||\n\t\t\t      mSource.indexOf('[') !== -1 || mSource.indexOf('(') !== -1) )\n\t\t{\n\t\t\t/* Like the get, we need to get data from a nested object */\n\t\t\tvar setData = function (data, val, src) {\n\t\t\t\tvar a = _fnSplitObjNotation( src ), b;\n\t\t\t\tvar aLast = a[a.length-1];\n\t\t\t\tvar arrayNotation, funcNotation, o, innerSrc;\n\t\n\t\t\t\tfor ( var i=0, iLen=a.length-1 ; i<iLen ; i++ )\n\t\t\t\t{\n\t\t\t\t\t// Check if we are dealing with an array notation request\n\t\t\t\t\tarrayNotation = a[i].match(__reArray);\n\t\t\t\t\tfuncNotation = a[i].match(__reFn);\n\t\n\t\t\t\t\tif ( arrayNotation )\n\t\t\t\t\t{\n\t\t\t\t\t\ta[i] = a[i].replace(__reArray, '');\n\t\t\t\t\t\tdata[ a[i] ] = [];\n\t\n\t\t\t\t\t\t// Get the remainder of the nested object to set so we can recurse\n\t\t\t\t\t\tb = a.slice();\n\t\t\t\t\t\tb.splice( 0, i+1 );\n\t\t\t\t\t\tinnerSrc = b.join('.');\n\t\n\t\t\t\t\t\t// Traverse each entry in the array setting the properties requested\n\t\t\t\t\t\tif ( $.isArray( val ) )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfor ( var j=0, jLen=val.length ; j<jLen ; j++ )\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\to = {};\n\t\t\t\t\t\t\t\tsetData( o, val[j], innerSrc );\n\t\t\t\t\t\t\t\tdata[ a[i] ].push( o );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t// We've been asked to save data to an array, but it\n\t\t\t\t\t\t\t// isn't array data to be saved. Best that can be done\n\t\t\t\t\t\t\t// is to just save the value.\n\t\t\t\t\t\t\tdata[ a[i] ] = val;\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t// The inner call to setData has already traversed through the remainder\n\t\t\t\t\t\t// of the source and has set the data, thus we can exit here\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\telse if ( funcNotation )\n\t\t\t\t\t{\n\t\t\t\t\t\t// Function call\n\t\t\t\t\t\ta[i] = a[i].replace(__reFn, '');\n\t\t\t\t\t\tdata = data[ a[i] ]( val );\n\t\t\t\t\t}\n\t\n\t\t\t\t\t// If the nested object doesn't currently exist - since we are\n\t\t\t\t\t// trying to set the value - create it\n\t\t\t\t\tif ( data[ a[i] ] === null || data[ a[i] ] === undefined )\n\t\t\t\t\t{\n\t\t\t\t\t\tdata[ a[i] ] = {};\n\t\t\t\t\t}\n\t\t\t\t\tdata = data[ a[i] ];\n\t\t\t\t}\n\t\n\t\t\t\t// Last item in the input - i.e, the actual set\n\t\t\t\tif ( aLast.match(__reFn ) )\n\t\t\t\t{\n\t\t\t\t\t// Function call\n\t\t\t\t\tdata = data[ aLast.replace(__reFn, '') ]( val );\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t// If array notation is used, we just want to strip it and use the property name\n\t\t\t\t\t// and assign the value. If it isn't used, then we get the result we want anyway\n\t\t\t\t\tdata[ aLast.replace(__reArray, '') ] = val;\n\t\t\t\t}\n\t\t\t};\n\t\n\t\t\treturn function (data, val) { // meta is also passed in, but not used\n\t\t\t\treturn setData( data, val, mSource );\n\t\t\t};\n\t\t}\n\t\telse\n\t\t{\n\t\t\t/* Array or flat object mapping */\n\t\t\treturn function (data, val) { // meta is also passed in, but not used\n\t\t\t\tdata[mSource] = val;\n\t\t\t};\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Return an array with the full table data\n\t *  @param {object} oSettings dataTables settings object\n\t *  @returns array {array} aData Master data array\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnGetDataMaster ( settings )\n\t{\n\t\treturn _pluck( settings.aoData, '_aData' );\n\t}\n\t\n\t\n\t/**\n\t * Nuke the table\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnClearTable( settings )\n\t{\n\t\tsettings.aoData.length = 0;\n\t\tsettings.aiDisplayMaster.length = 0;\n\t\tsettings.aiDisplay.length = 0;\n\t\tsettings.aIds = {};\n\t}\n\t\n\t\n\t /**\n\t * Take an array of integers (index array) and remove a target integer (value - not\n\t * the key!)\n\t *  @param {array} a Index array to target\n\t *  @param {int} iTarget value to find\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnDeleteIndex( a, iTarget, splice )\n\t{\n\t\tvar iTargetIndex = -1;\n\t\n\t\tfor ( var i=0, iLen=a.length ; i<iLen ; i++ )\n\t\t{\n\t\t\tif ( a[i] == iTarget )\n\t\t\t{\n\t\t\t\tiTargetIndex = i;\n\t\t\t}\n\t\t\telse if ( a[i] > iTarget )\n\t\t\t{\n\t\t\t\ta[i]--;\n\t\t\t}\n\t\t}\n\t\n\t\tif ( iTargetIndex != -1 && splice === undefined )\n\t\t{\n\t\t\ta.splice( iTargetIndex, 1 );\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Mark cached data as invalid such that a re-read of the data will occur when\n\t * the cached data is next requested. Also update from the data source object.\n\t *\n\t * @param {object} settings DataTables settings object\n\t * @param {int}    rowIdx   Row index to invalidate\n\t * @param {string} [src]    Source to invalidate from: undefined, 'auto', 'dom'\n\t *     or 'data'\n\t * @param {int}    [colIdx] Column index to invalidate. If undefined the whole\n\t *     row will be invalidated\n\t * @memberof DataTable#oApi\n\t *\n\t * @todo For the modularisation of v1.11 this will need to become a callback, so\n\t *   the sort and filter methods can subscribe to it. That will required\n\t *   initialisation options for sorting, which is why it is not already baked in\n\t */\n\tfunction _fnInvalidate( settings, rowIdx, src, colIdx )\n\t{\n\t\tvar row = settings.aoData[ rowIdx ];\n\t\tvar i, ien;\n\t\tvar cellWrite = function ( cell, col ) {\n\t\t\t// This is very frustrating, but in IE if you just write directly\n\t\t\t// to innerHTML, and elements that are overwritten are GC'ed,\n\t\t\t// even if there is a reference to them elsewhere\n\t\t\twhile ( cell.childNodes.length ) {\n\t\t\t\tcell.removeChild( cell.firstChild );\n\t\t\t}\n\t\n\t\t\tcell.innerHTML = _fnGetCellData( settings, rowIdx, col, 'display' );\n\t\t};\n\t\n\t\t// Are we reading last data from DOM or the data object?\n\t\tif ( src === 'dom' || ((! src || src === 'auto') && row.src === 'dom') ) {\n\t\t\t// Read the data from the DOM\n\t\t\trow._aData = _fnGetRowElements(\n\t\t\t\t\tsettings, row, colIdx, colIdx === undefined ? undefined : row._aData\n\t\t\t\t)\n\t\t\t\t.data;\n\t\t}\n\t\telse {\n\t\t\t// Reading from data object, update the DOM\n\t\t\tvar cells = row.anCells;\n\t\n\t\t\tif ( cells ) {\n\t\t\t\tif ( colIdx !== undefined ) {\n\t\t\t\t\tcellWrite( cells[colIdx], colIdx );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tfor ( i=0, ien=cells.length ; i<ien ; i++ ) {\n\t\t\t\t\t\tcellWrite( cells[i], i );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\t// For both row and cell invalidation, the cached data for sorting and\n\t\t// filtering is nulled out\n\t\trow._aSortData = null;\n\t\trow._aFilterData = null;\n\t\n\t\t// Invalidate the type for a specific column (if given) or all columns since\n\t\t// the data might have changed\n\t\tvar cols = settings.aoColumns;\n\t\tif ( colIdx !== undefined ) {\n\t\t\tcols[ colIdx ].sType = null;\n\t\t}\n\t\telse {\n\t\t\tfor ( i=0, ien=cols.length ; i<ien ; i++ ) {\n\t\t\t\tcols[i].sType = null;\n\t\t\t}\n\t\n\t\t\t// Update DataTables special `DT_*` attributes for the row\n\t\t\t_fnRowAttributes( settings, row );\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Build a data source object from an HTML row, reading the contents of the\n\t * cells that are in the row.\n\t *\n\t * @param {object} settings DataTables settings object\n\t * @param {node|object} TR element from which to read data or existing row\n\t *   object from which to re-read the data from the cells\n\t * @param {int} [colIdx] Optional column index\n\t * @param {array|object} [d] Data source object. If `colIdx` is given then this\n\t *   parameter should also be given and will be used to write the data into.\n\t *   Only the column in question will be written\n\t * @returns {object} Object with two parameters: `data` the data read, in\n\t *   document order, and `cells` and array of nodes (they can be useful to the\n\t *   caller, so rather than needing a second traversal to get them, just return\n\t *   them from here).\n\t * @memberof DataTable#oApi\n\t */\n\tfunction _fnGetRowElements( settings, row, colIdx, d )\n\t{\n\t\tvar\n\t\t\ttds = [],\n\t\t\ttd = row.firstChild,\n\t\t\tname, col, o, i=0, contents,\n\t\t\tcolumns = settings.aoColumns,\n\t\t\tobjectRead = settings._rowReadObject;\n\t\n\t\t// Allow the data object to be passed in, or construct\n\t\td = d !== undefined ?\n\t\t\td :\n\t\t\tobjectRead ?\n\t\t\t\t{} :\n\t\t\t\t[];\n\t\n\t\tvar attr = function ( str, td  ) {\n\t\t\tif ( typeof str === 'string' ) {\n\t\t\t\tvar idx = str.indexOf('@');\n\t\n\t\t\t\tif ( idx !== -1 ) {\n\t\t\t\t\tvar attr = str.substring( idx+1 );\n\t\t\t\t\tvar setter = _fnSetObjectDataFn( str );\n\t\t\t\t\tsetter( d, td.getAttribute( attr ) );\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\n\t\t// Read data from a cell and store into the data object\n\t\tvar cellProcess = function ( cell ) {\n\t\t\tif ( colIdx === undefined || colIdx === i ) {\n\t\t\t\tcol = columns[i];\n\t\t\t\tcontents = $.trim(cell.innerHTML);\n\t\n\t\t\t\tif ( col && col._bAttrSrc ) {\n\t\t\t\t\tvar setter = _fnSetObjectDataFn( col.mData._ );\n\t\t\t\t\tsetter( d, contents );\n\t\n\t\t\t\t\tattr( col.mData.sort, cell );\n\t\t\t\t\tattr( col.mData.type, cell );\n\t\t\t\t\tattr( col.mData.filter, cell );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t// Depending on the `data` option for the columns the data can\n\t\t\t\t\t// be read to either an object or an array.\n\t\t\t\t\tif ( objectRead ) {\n\t\t\t\t\t\tif ( ! col._setter ) {\n\t\t\t\t\t\t\t// Cache the setter function\n\t\t\t\t\t\t\tcol._setter = _fnSetObjectDataFn( col.mData );\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcol._setter( d, contents );\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\td[i] = contents;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\ti++;\n\t\t};\n\t\n\t\tif ( td ) {\n\t\t\t// `tr` element was passed in\n\t\t\twhile ( td ) {\n\t\t\t\tname = td.nodeName.toUpperCase();\n\t\n\t\t\t\tif ( name == \"TD\" || name == \"TH\" ) {\n\t\t\t\t\tcellProcess( td );\n\t\t\t\t\ttds.push( td );\n\t\t\t\t}\n\t\n\t\t\t\ttd = td.nextSibling;\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\t// Existing row object passed in\n\t\t\ttds = row.anCells;\n\t\n\t\t\tfor ( var j=0, jen=tds.length ; j<jen ; j++ ) {\n\t\t\t\tcellProcess( tds[j] );\n\t\t\t}\n\t\t}\n\t\n\t\t// Read the ID from the DOM if present\n\t\tvar rowNode = row.firstChild ? row : row.nTr;\n\t\n\t\tif ( rowNode ) {\n\t\t\tvar id = rowNode.getAttribute( 'id' );\n\t\n\t\t\tif ( id ) {\n\t\t\t\t_fnSetObjectDataFn( settings.rowId )( d, id );\n\t\t\t}\n\t\t}\n\t\n\t\treturn {\n\t\t\tdata: d,\n\t\t\tcells: tds\n\t\t};\n\t}\n\t/**\n\t * Create a new TR element (and it's TD children) for a row\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {int} iRow Row to consider\n\t *  @param {node} [nTrIn] TR element to add to the table - optional. If not given,\n\t *    DataTables will create a row automatically\n\t *  @param {array} [anTds] Array of TD|TH elements for the row - must be given\n\t *    if nTr is.\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnCreateTr ( oSettings, iRow, nTrIn, anTds )\n\t{\n\t\tvar\n\t\t\trow = oSettings.aoData[iRow],\n\t\t\trowData = row._aData,\n\t\t\tcells = [],\n\t\t\tnTr, nTd, oCol,\n\t\t\ti, iLen, create;\n\t\n\t\tif ( row.nTr === null )\n\t\t{\n\t\t\tnTr = nTrIn || document.createElement('tr');\n\t\n\t\t\trow.nTr = nTr;\n\t\t\trow.anCells = cells;\n\t\n\t\t\t/* Use a private property on the node to allow reserve mapping from the node\n\t\t\t * to the aoData array for fast look up\n\t\t\t */\n\t\t\tnTr._DT_RowIndex = iRow;\n\t\n\t\t\t/* Special parameters can be given by the data source to be used on the row */\n\t\t\t_fnRowAttributes( oSettings, row );\n\t\n\t\t\t/* Process each column */\n\t\t\tfor ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )\n\t\t\t{\n\t\t\t\toCol = oSettings.aoColumns[i];\n\t\t\t\tcreate = nTrIn ? false : true;\n\t\n\t\t\t\tnTd = create ? document.createElement( oCol.sCellType ) : anTds[i];\n\t\t\t\tnTd._DT_CellIndex = {\n\t\t\t\t\trow: iRow,\n\t\t\t\t\tcolumn: i\n\t\t\t\t};\n\t\t\t\t\n\t\t\t\tcells.push( nTd );\n\t\n\t\t\t\t// Need to create the HTML if new, or if a rendering function is defined\n\t\t\t\tif ( create || ((!nTrIn || oCol.mRender || oCol.mData !== i) &&\n\t\t\t\t\t (!$.isPlainObject(oCol.mData) || oCol.mData._ !== i+'.display')\n\t\t\t\t)) {\n\t\t\t\t\tnTd.innerHTML = _fnGetCellData( oSettings, iRow, i, 'display' );\n\t\t\t\t}\n\t\n\t\t\t\t/* Add user defined class */\n\t\t\t\tif ( oCol.sClass )\n\t\t\t\t{\n\t\t\t\t\tnTd.className += ' '+oCol.sClass;\n\t\t\t\t}\n\t\n\t\t\t\t// Visibility - add or remove as required\n\t\t\t\tif ( oCol.bVisible && ! nTrIn )\n\t\t\t\t{\n\t\t\t\t\tnTr.appendChild( nTd );\n\t\t\t\t}\n\t\t\t\telse if ( ! oCol.bVisible && nTrIn )\n\t\t\t\t{\n\t\t\t\t\tnTd.parentNode.removeChild( nTd );\n\t\t\t\t}\n\t\n\t\t\t\tif ( oCol.fnCreatedCell )\n\t\t\t\t{\n\t\t\t\t\toCol.fnCreatedCell.call( oSettings.oInstance,\n\t\t\t\t\t\tnTd, _fnGetCellData( oSettings, iRow, i ), rowData, iRow, i\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t_fnCallbackFire( oSettings, 'aoRowCreatedCallback', null, [nTr, rowData, iRow, cells] );\n\t\t}\n\t\n\t\t// Remove once webkit bug 131819 and Chromium bug 365619 have been resolved\n\t\t// and deployed\n\t\trow.nTr.setAttribute( 'role', 'row' );\n\t}\n\t\n\t\n\t/**\n\t * Add attributes to a row based on the special `DT_*` parameters in a data\n\t * source object.\n\t *  @param {object} settings DataTables settings object\n\t *  @param {object} DataTables row object for the row to be modified\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnRowAttributes( settings, row )\n\t{\n\t\tvar tr = row.nTr;\n\t\tvar data = row._aData;\n\t\n\t\tif ( tr ) {\n\t\t\tvar id = settings.rowIdFn( data );\n\t\n\t\t\tif ( id ) {\n\t\t\t\ttr.id = id;\n\t\t\t}\n\t\n\t\t\tif ( data.DT_RowClass ) {\n\t\t\t\t// Remove any classes added by DT_RowClass before\n\t\t\t\tvar a = data.DT_RowClass.split(' ');\n\t\t\t\trow.__rowc = row.__rowc ?\n\t\t\t\t\t_unique( row.__rowc.concat( a ) ) :\n\t\t\t\t\ta;\n\t\n\t\t\t\t$(tr)\n\t\t\t\t\t.removeClass( row.__rowc.join(' ') )\n\t\t\t\t\t.addClass( data.DT_RowClass );\n\t\t\t}\n\t\n\t\t\tif ( data.DT_RowAttr ) {\n\t\t\t\t$(tr).attr( data.DT_RowAttr );\n\t\t\t}\n\t\n\t\t\tif ( data.DT_RowData ) {\n\t\t\t\t$(tr).data( data.DT_RowData );\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Create the HTML header for the table\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnBuildHead( oSettings )\n\t{\n\t\tvar i, ien, cell, row, column;\n\t\tvar thead = oSettings.nTHead;\n\t\tvar tfoot = oSettings.nTFoot;\n\t\tvar createHeader = $('th, td', thead).length === 0;\n\t\tvar classes = oSettings.oClasses;\n\t\tvar columns = oSettings.aoColumns;\n\t\n\t\tif ( createHeader ) {\n\t\t\trow = $('<tr/>').appendTo( thead );\n\t\t}\n\t\n\t\tfor ( i=0, ien=columns.length ; i<ien ; i++ ) {\n\t\t\tcolumn = columns[i];\n\t\t\tcell = $( column.nTh ).addClass( column.sClass );\n\t\n\t\t\tif ( createHeader ) {\n\t\t\t\tcell.appendTo( row );\n\t\t\t}\n\t\n\t\t\t// 1.11 move into sorting\n\t\t\tif ( oSettings.oFeatures.bSort ) {\n\t\t\t\tcell.addClass( column.sSortingClass );\n\t\n\t\t\t\tif ( column.bSortable !== false ) {\n\t\t\t\t\tcell\n\t\t\t\t\t\t.attr( 'tabindex', oSettings.iTabIndex )\n\t\t\t\t\t\t.attr( 'aria-controls', oSettings.sTableId );\n\t\n\t\t\t\t\t_fnSortAttachListener( oSettings, column.nTh, i );\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\tif ( column.sTitle != cell[0].innerHTML ) {\n\t\t\t\tcell.html( column.sTitle );\n\t\t\t}\n\t\n\t\t\t_fnRenderer( oSettings, 'header' )(\n\t\t\t\toSettings, cell, column, classes\n\t\t\t);\n\t\t}\n\t\n\t\tif ( createHeader ) {\n\t\t\t_fnDetectHeader( oSettings.aoHeader, thead );\n\t\t}\n\t\t\n\t\t/* ARIA role for the rows */\n\t \t$(thead).find('>tr').attr('role', 'row');\n\t\n\t\t/* Deal with the footer - add classes if required */\n\t\t$(thead).find('>tr>th, >tr>td').addClass( classes.sHeaderTH );\n\t\t$(tfoot).find('>tr>th, >tr>td').addClass( classes.sFooterTH );\n\t\n\t\t// Cache the footer cells. Note that we only take the cells from the first\n\t\t// row in the footer. If there is more than one row the user wants to\n\t\t// interact with, they need to use the table().foot() method. Note also this\n\t\t// allows cells to be used for multiple columns using colspan\n\t\tif ( tfoot !== null ) {\n\t\t\tvar cells = oSettings.aoFooter[0];\n\t\n\t\t\tfor ( i=0, ien=cells.length ; i<ien ; i++ ) {\n\t\t\t\tcolumn = columns[i];\n\t\t\t\tcolumn.nTf = cells[i].cell;\n\t\n\t\t\t\tif ( column.sClass ) {\n\t\t\t\t\t$(column.nTf).addClass( column.sClass );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Draw the header (or footer) element based on the column visibility states. The\n\t * methodology here is to use the layout array from _fnDetectHeader, modified for\n\t * the instantaneous column visibility, to construct the new layout. The grid is\n\t * traversed over cell at a time in a rows x columns grid fashion, although each\n\t * cell insert can cover multiple elements in the grid - which is tracks using the\n\t * aApplied array. Cell inserts in the grid will only occur where there isn't\n\t * already a cell in that position.\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param array {objects} aoSource Layout array from _fnDetectHeader\n\t *  @param {boolean} [bIncludeHidden=false] If true then include the hidden columns in the calc,\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnDrawHead( oSettings, aoSource, bIncludeHidden )\n\t{\n\t\tvar i, iLen, j, jLen, k, kLen, n, nLocalTr;\n\t\tvar aoLocal = [];\n\t\tvar aApplied = [];\n\t\tvar iColumns = oSettings.aoColumns.length;\n\t\tvar iRowspan, iColspan;\n\t\n\t\tif ( ! aoSource )\n\t\t{\n\t\t\treturn;\n\t\t}\n\t\n\t\tif (  bIncludeHidden === undefined )\n\t\t{\n\t\t\tbIncludeHidden = false;\n\t\t}\n\t\n\t\t/* Make a copy of the master layout array, but without the visible columns in it */\n\t\tfor ( i=0, iLen=aoSource.length ; i<iLen ; i++ )\n\t\t{\n\t\t\taoLocal[i] = aoSource[i].slice();\n\t\t\taoLocal[i].nTr = aoSource[i].nTr;\n\t\n\t\t\t/* Remove any columns which are currently hidden */\n\t\t\tfor ( j=iColumns-1 ; j>=0 ; j-- )\n\t\t\t{\n\t\t\t\tif ( !oSettings.aoColumns[j].bVisible && !bIncludeHidden )\n\t\t\t\t{\n\t\t\t\t\taoLocal[i].splice( j, 1 );\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t/* Prep the applied array - it needs an element for each row */\n\t\t\taApplied.push( [] );\n\t\t}\n\t\n\t\tfor ( i=0, iLen=aoLocal.length ; i<iLen ; i++ )\n\t\t{\n\t\t\tnLocalTr = aoLocal[i].nTr;\n\t\n\t\t\t/* All cells are going to be replaced, so empty out the row */\n\t\t\tif ( nLocalTr )\n\t\t\t{\n\t\t\t\twhile( (n = nLocalTr.firstChild) )\n\t\t\t\t{\n\t\t\t\t\tnLocalTr.removeChild( n );\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\tfor ( j=0, jLen=aoLocal[i].length ; j<jLen ; j++ )\n\t\t\t{\n\t\t\t\tiRowspan = 1;\n\t\t\t\tiColspan = 1;\n\t\n\t\t\t\t/* Check to see if there is already a cell (row/colspan) covering our target\n\t\t\t\t * insert point. If there is, then there is nothing to do.\n\t\t\t\t */\n\t\t\t\tif ( aApplied[i][j] === undefined )\n\t\t\t\t{\n\t\t\t\t\tnLocalTr.appendChild( aoLocal[i][j].cell );\n\t\t\t\t\taApplied[i][j] = 1;\n\t\n\t\t\t\t\t/* Expand the cell to cover as many rows as needed */\n\t\t\t\t\twhile ( aoLocal[i+iRowspan] !== undefined &&\n\t\t\t\t\t        aoLocal[i][j].cell == aoLocal[i+iRowspan][j].cell )\n\t\t\t\t\t{\n\t\t\t\t\t\taApplied[i+iRowspan][j] = 1;\n\t\t\t\t\t\tiRowspan++;\n\t\t\t\t\t}\n\t\n\t\t\t\t\t/* Expand the cell to cover as many columns as needed */\n\t\t\t\t\twhile ( aoLocal[i][j+iColspan] !== undefined &&\n\t\t\t\t\t        aoLocal[i][j].cell == aoLocal[i][j+iColspan].cell )\n\t\t\t\t\t{\n\t\t\t\t\t\t/* Must update the applied array over the rows for the columns */\n\t\t\t\t\t\tfor ( k=0 ; k<iRowspan ; k++ )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\taApplied[i+k][j+iColspan] = 1;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tiColspan++;\n\t\t\t\t\t}\n\t\n\t\t\t\t\t/* Do the actual expansion in the DOM */\n\t\t\t\t\t$(aoLocal[i][j].cell)\n\t\t\t\t\t\t.attr('rowspan', iRowspan)\n\t\t\t\t\t\t.attr('colspan', iColspan);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Insert the required TR nodes into the table for display\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnDraw( oSettings )\n\t{\n\t\t/* Provide a pre-callback function which can be used to cancel the draw is false is returned */\n\t\tvar aPreDraw = _fnCallbackFire( oSettings, 'aoPreDrawCallback', 'preDraw', [oSettings] );\n\t\tif ( $.inArray( false, aPreDraw ) !== -1 )\n\t\t{\n\t\t\t_fnProcessingDisplay( oSettings, false );\n\t\t\treturn;\n\t\t}\n\t\n\t\tvar i, iLen, n;\n\t\tvar anRows = [];\n\t\tvar iRowCount = 0;\n\t\tvar asStripeClasses = oSettings.asStripeClasses;\n\t\tvar iStripes = asStripeClasses.length;\n\t\tvar iOpenRows = oSettings.aoOpenRows.length;\n\t\tvar oLang = oSettings.oLanguage;\n\t\tvar iInitDisplayStart = oSettings.iInitDisplayStart;\n\t\tvar bServerSide = _fnDataSource( oSettings ) == 'ssp';\n\t\tvar aiDisplay = oSettings.aiDisplay;\n\t\n\t\toSettings.bDrawing = true;\n\t\n\t\t/* Check and see if we have an initial draw position from state saving */\n\t\tif ( iInitDisplayStart !== undefined && iInitDisplayStart !== -1 )\n\t\t{\n\t\t\toSettings._iDisplayStart = bServerSide ?\n\t\t\t\tiInitDisplayStart :\n\t\t\t\tiInitDisplayStart >= oSettings.fnRecordsDisplay() ?\n\t\t\t\t\t0 :\n\t\t\t\t\tiInitDisplayStart;\n\t\n\t\t\toSettings.iInitDisplayStart = -1;\n\t\t}\n\t\n\t\tvar iDisplayStart = oSettings._iDisplayStart;\n\t\tvar iDisplayEnd = oSettings.fnDisplayEnd();\n\t\n\t\t/* Server-side processing draw intercept */\n\t\tif ( oSettings.bDeferLoading )\n\t\t{\n\t\t\toSettings.bDeferLoading = false;\n\t\t\toSettings.iDraw++;\n\t\t\t_fnProcessingDisplay( oSettings, false );\n\t\t}\n\t\telse if ( !bServerSide )\n\t\t{\n\t\t\toSettings.iDraw++;\n\t\t}\n\t\telse if ( !oSettings.bDestroying && !_fnAjaxUpdate( oSettings ) )\n\t\t{\n\t\t\treturn;\n\t\t}\n\t\n\t\tif ( aiDisplay.length !== 0 )\n\t\t{\n\t\t\tvar iStart = bServerSide ? 0 : iDisplayStart;\n\t\t\tvar iEnd = bServerSide ? oSettings.aoData.length : iDisplayEnd;\n\t\n\t\t\tfor ( var j=iStart ; j<iEnd ; j++ )\n\t\t\t{\n\t\t\t\tvar iDataIndex = aiDisplay[j];\n\t\t\t\tvar aoData = oSettings.aoData[ iDataIndex ];\n\t\t\t\tif ( aoData.nTr === null )\n\t\t\t\t{\n\t\t\t\t\t_fnCreateTr( oSettings, iDataIndex );\n\t\t\t\t}\n\t\n\t\t\t\tvar nRow = aoData.nTr;\n\t\n\t\t\t\t/* Remove the old striping classes and then add the new one */\n\t\t\t\tif ( iStripes !== 0 )\n\t\t\t\t{\n\t\t\t\t\tvar sStripe = asStripeClasses[ iRowCount % iStripes ];\n\t\t\t\t\tif ( aoData._sRowStripe != sStripe )\n\t\t\t\t\t{\n\t\t\t\t\t\t$(nRow).removeClass( aoData._sRowStripe ).addClass( sStripe );\n\t\t\t\t\t\taoData._sRowStripe = sStripe;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\t// Row callback functions - might want to manipulate the row\n\t\t\t\t// iRowCount and j are not currently documented. Are they at all\n\t\t\t\t// useful?\n\t\t\t\t_fnCallbackFire( oSettings, 'aoRowCallback', null,\n\t\t\t\t\t[nRow, aoData._aData, iRowCount, j, iDataIndex] );\n\t\n\t\t\t\tanRows.push( nRow );\n\t\t\t\tiRowCount++;\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\t/* Table is empty - create a row with an empty message in it */\n\t\t\tvar sZero = oLang.sZeroRecords;\n\t\t\tif ( oSettings.iDraw == 1 &&  _fnDataSource( oSettings ) == 'ajax' )\n\t\t\t{\n\t\t\t\tsZero = oLang.sLoadingRecords;\n\t\t\t}\n\t\t\telse if ( oLang.sEmptyTable && oSettings.fnRecordsTotal() === 0 )\n\t\t\t{\n\t\t\t\tsZero = oLang.sEmptyTable;\n\t\t\t}\n\t\n\t\t\tanRows[ 0 ] = $( '<tr/>', { 'class': iStripes ? asStripeClasses[0] : '' } )\n\t\t\t\t.append( $('<td />', {\n\t\t\t\t\t'valign':  'top',\n\t\t\t\t\t'colSpan': _fnVisbleColumns( oSettings ),\n\t\t\t\t\t'class':   oSettings.oClasses.sRowEmpty\n\t\t\t\t} ).html( sZero ) )[0];\n\t\t}\n\t\n\t\t/* Header and footer callbacks */\n\t\t_fnCallbackFire( oSettings, 'aoHeaderCallback', 'header', [ $(oSettings.nTHead).children('tr')[0],\n\t\t\t_fnGetDataMaster( oSettings ), iDisplayStart, iDisplayEnd, aiDisplay ] );\n\t\n\t\t_fnCallbackFire( oSettings, 'aoFooterCallback', 'footer', [ $(oSettings.nTFoot).children('tr')[0],\n\t\t\t_fnGetDataMaster( oSettings ), iDisplayStart, iDisplayEnd, aiDisplay ] );\n\t\n\t\tvar body = $(oSettings.nTBody);\n\t\n\t\tbody.children().detach();\n\t\tbody.append( $(anRows) );\n\t\n\t\t/* Call all required callback functions for the end of a draw */\n\t\t_fnCallbackFire( oSettings, 'aoDrawCallback', 'draw', [oSettings] );\n\t\n\t\t/* Draw is complete, sorting and filtering must be as well */\n\t\toSettings.bSorted = false;\n\t\toSettings.bFiltered = false;\n\t\toSettings.bDrawing = false;\n\t}\n\t\n\t\n\t/**\n\t * Redraw the table - taking account of the various features which are enabled\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {boolean} [holdPosition] Keep the current paging position. By default\n\t *    the paging is reset to the first page\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnReDraw( settings, holdPosition )\n\t{\n\t\tvar\n\t\t\tfeatures = settings.oFeatures,\n\t\t\tsort     = features.bSort,\n\t\t\tfilter   = features.bFilter;\n\t\n\t\tif ( sort ) {\n\t\t\t_fnSort( settings );\n\t\t}\n\t\n\t\tif ( filter ) {\n\t\t\t_fnFilterComplete( settings, settings.oPreviousSearch );\n\t\t}\n\t\telse {\n\t\t\t// No filtering, so we want to just use the display master\n\t\t\tsettings.aiDisplay = settings.aiDisplayMaster.slice();\n\t\t}\n\t\n\t\tif ( holdPosition !== true ) {\n\t\t\tsettings._iDisplayStart = 0;\n\t\t}\n\t\n\t\t// Let any modules know about the draw hold position state (used by\n\t\t// scrolling internally)\n\t\tsettings._drawHold = holdPosition;\n\t\n\t\t_fnDraw( settings );\n\t\n\t\tsettings._drawHold = false;\n\t}\n\t\n\t\n\t/**\n\t * Add the options to the page HTML for the table\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnAddOptionsHtml ( oSettings )\n\t{\n\t\tvar classes = oSettings.oClasses;\n\t\tvar table = $(oSettings.nTable);\n\t\tvar holding = $('<div/>').insertBefore( table ); // Holding element for speed\n\t\tvar features = oSettings.oFeatures;\n\t\n\t\t// All DataTables are wrapped in a div\n\t\tvar insert = $('<div/>', {\n\t\t\tid:      oSettings.sTableId+'_wrapper',\n\t\t\t'class': classes.sWrapper + (oSettings.nTFoot ? '' : ' '+classes.sNoFooter)\n\t\t} );\n\t\n\t\toSettings.nHolding = holding[0];\n\t\toSettings.nTableWrapper = insert[0];\n\t\toSettings.nTableReinsertBefore = oSettings.nTable.nextSibling;\n\t\n\t\t/* Loop over the user set positioning and place the elements as needed */\n\t\tvar aDom = oSettings.sDom.split('');\n\t\tvar featureNode, cOption, nNewNode, cNext, sAttr, j;\n\t\tfor ( var i=0 ; i<aDom.length ; i++ )\n\t\t{\n\t\t\tfeatureNode = null;\n\t\t\tcOption = aDom[i];\n\t\n\t\t\tif ( cOption == '<' )\n\t\t\t{\n\t\t\t\t/* New container div */\n\t\t\t\tnNewNode = $('<div/>')[0];\n\t\n\t\t\t\t/* Check to see if we should append an id and/or a class name to the container */\n\t\t\t\tcNext = aDom[i+1];\n\t\t\t\tif ( cNext == \"'\" || cNext == '\"' )\n\t\t\t\t{\n\t\t\t\t\tsAttr = \"\";\n\t\t\t\t\tj = 2;\n\t\t\t\t\twhile ( aDom[i+j] != cNext )\n\t\t\t\t\t{\n\t\t\t\t\t\tsAttr += aDom[i+j];\n\t\t\t\t\t\tj++;\n\t\t\t\t\t}\n\t\n\t\t\t\t\t/* Replace jQuery UI constants @todo depreciated */\n\t\t\t\t\tif ( sAttr == \"H\" )\n\t\t\t\t\t{\n\t\t\t\t\t\tsAttr = classes.sJUIHeader;\n\t\t\t\t\t}\n\t\t\t\t\telse if ( sAttr == \"F\" )\n\t\t\t\t\t{\n\t\t\t\t\t\tsAttr = classes.sJUIFooter;\n\t\t\t\t\t}\n\t\n\t\t\t\t\t/* The attribute can be in the format of \"#id.class\", \"#id\" or \"class\" This logic\n\t\t\t\t\t * breaks the string into parts and applies them as needed\n\t\t\t\t\t */\n\t\t\t\t\tif ( sAttr.indexOf('.') != -1 )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar aSplit = sAttr.split('.');\n\t\t\t\t\t\tnNewNode.id = aSplit[0].substr(1, aSplit[0].length-1);\n\t\t\t\t\t\tnNewNode.className = aSplit[1];\n\t\t\t\t\t}\n\t\t\t\t\telse if ( sAttr.charAt(0) == \"#\" )\n\t\t\t\t\t{\n\t\t\t\t\t\tnNewNode.id = sAttr.substr(1, sAttr.length-1);\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tnNewNode.className = sAttr;\n\t\t\t\t\t}\n\t\n\t\t\t\t\ti += j; /* Move along the position array */\n\t\t\t\t}\n\t\n\t\t\t\tinsert.append( nNewNode );\n\t\t\t\tinsert = $(nNewNode);\n\t\t\t}\n\t\t\telse if ( cOption == '>' )\n\t\t\t{\n\t\t\t\t/* End container div */\n\t\t\t\tinsert = insert.parent();\n\t\t\t}\n\t\t\t// @todo Move options into their own plugins?\n\t\t\telse if ( cOption == 'l' && features.bPaginate && features.bLengthChange )\n\t\t\t{\n\t\t\t\t/* Length */\n\t\t\t\tfeatureNode = _fnFeatureHtmlLength( oSettings );\n\t\t\t}\n\t\t\telse if ( cOption == 'f' && features.bFilter )\n\t\t\t{\n\t\t\t\t/* Filter */\n\t\t\t\tfeatureNode = _fnFeatureHtmlFilter( oSettings );\n\t\t\t}\n\t\t\telse if ( cOption == 'r' && features.bProcessing )\n\t\t\t{\n\t\t\t\t/* pRocessing */\n\t\t\t\tfeatureNode = _fnFeatureHtmlProcessing( oSettings );\n\t\t\t}\n\t\t\telse if ( cOption == 't' )\n\t\t\t{\n\t\t\t\t/* Table */\n\t\t\t\tfeatureNode = _fnFeatureHtmlTable( oSettings );\n\t\t\t}\n\t\t\telse if ( cOption ==  'i' && features.bInfo )\n\t\t\t{\n\t\t\t\t/* Info */\n\t\t\t\tfeatureNode = _fnFeatureHtmlInfo( oSettings );\n\t\t\t}\n\t\t\telse if ( cOption == 'p' && features.bPaginate )\n\t\t\t{\n\t\t\t\t/* Pagination */\n\t\t\t\tfeatureNode = _fnFeatureHtmlPaginate( oSettings );\n\t\t\t}\n\t\t\telse if ( DataTable.ext.feature.length !== 0 )\n\t\t\t{\n\t\t\t\t/* Plug-in features */\n\t\t\t\tvar aoFeatures = DataTable.ext.feature;\n\t\t\t\tfor ( var k=0, kLen=aoFeatures.length ; k<kLen ; k++ )\n\t\t\t\t{\n\t\t\t\t\tif ( cOption == aoFeatures[k].cFeature )\n\t\t\t\t\t{\n\t\t\t\t\t\tfeatureNode = aoFeatures[k].fnInit( oSettings );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t/* Add to the 2D features array */\n\t\t\tif ( featureNode )\n\t\t\t{\n\t\t\t\tvar aanFeatures = oSettings.aanFeatures;\n\t\n\t\t\t\tif ( ! aanFeatures[cOption] )\n\t\t\t\t{\n\t\t\t\t\taanFeatures[cOption] = [];\n\t\t\t\t}\n\t\n\t\t\t\taanFeatures[cOption].push( featureNode );\n\t\t\t\tinsert.append( featureNode );\n\t\t\t}\n\t\t}\n\t\n\t\t/* Built our DOM structure - replace the holding div with what we want */\n\t\tholding.replaceWith( insert );\n\t\toSettings.nHolding = null;\n\t}\n\t\n\t\n\t/**\n\t * Use the DOM source to create up an array of header cells. The idea here is to\n\t * create a layout grid (array) of rows x columns, which contains a reference\n\t * to the cell that that point in the grid (regardless of col/rowspan), such that\n\t * any column / row could be removed and the new grid constructed\n\t *  @param array {object} aLayout Array to store the calculated layout in\n\t *  @param {node} nThead The header/footer element for the table\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnDetectHeader ( aLayout, nThead )\n\t{\n\t\tvar nTrs = $(nThead).children('tr');\n\t\tvar nTr, nCell;\n\t\tvar i, k, l, iLen, jLen, iColShifted, iColumn, iColspan, iRowspan;\n\t\tvar bUnique;\n\t\tvar fnShiftCol = function ( a, i, j ) {\n\t\t\tvar k = a[i];\n\t                while ( k[j] ) {\n\t\t\t\tj++;\n\t\t\t}\n\t\t\treturn j;\n\t\t};\n\t\n\t\taLayout.splice( 0, aLayout.length );\n\t\n\t\t/* We know how many rows there are in the layout - so prep it */\n\t\tfor ( i=0, iLen=nTrs.length ; i<iLen ; i++ )\n\t\t{\n\t\t\taLayout.push( [] );\n\t\t}\n\t\n\t\t/* Calculate a layout array */\n\t\tfor ( i=0, iLen=nTrs.length ; i<iLen ; i++ )\n\t\t{\n\t\t\tnTr = nTrs[i];\n\t\t\tiColumn = 0;\n\t\n\t\t\t/* For every cell in the row... */\n\t\t\tnCell = nTr.firstChild;\n\t\t\twhile ( nCell ) {\n\t\t\t\tif ( nCell.nodeName.toUpperCase() == \"TD\" ||\n\t\t\t\t     nCell.nodeName.toUpperCase() == \"TH\" )\n\t\t\t\t{\n\t\t\t\t\t/* Get the col and rowspan attributes from the DOM and sanitise them */\n\t\t\t\t\tiColspan = nCell.getAttribute('colspan') * 1;\n\t\t\t\t\tiRowspan = nCell.getAttribute('rowspan') * 1;\n\t\t\t\t\tiColspan = (!iColspan || iColspan===0 || iColspan===1) ? 1 : iColspan;\n\t\t\t\t\tiRowspan = (!iRowspan || iRowspan===0 || iRowspan===1) ? 1 : iRowspan;\n\t\n\t\t\t\t\t/* There might be colspan cells already in this row, so shift our target\n\t\t\t\t\t * accordingly\n\t\t\t\t\t */\n\t\t\t\t\tiColShifted = fnShiftCol( aLayout, i, iColumn );\n\t\n\t\t\t\t\t/* Cache calculation for unique columns */\n\t\t\t\t\tbUnique = iColspan === 1 ? true : false;\n\t\n\t\t\t\t\t/* If there is col / rowspan, copy the information into the layout grid */\n\t\t\t\t\tfor ( l=0 ; l<iColspan ; l++ )\n\t\t\t\t\t{\n\t\t\t\t\t\tfor ( k=0 ; k<iRowspan ; k++ )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\taLayout[i+k][iColShifted+l] = {\n\t\t\t\t\t\t\t\t\"cell\": nCell,\n\t\t\t\t\t\t\t\t\"unique\": bUnique\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\taLayout[i+k].nTr = nTr;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tnCell = nCell.nextSibling;\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Get an array of unique th elements, one for each column\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {node} nHeader automatically detect the layout from this node - optional\n\t *  @param {array} aLayout thead/tfoot layout from _fnDetectHeader - optional\n\t *  @returns array {node} aReturn list of unique th's\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnGetUniqueThs ( oSettings, nHeader, aLayout )\n\t{\n\t\tvar aReturn = [];\n\t\tif ( !aLayout )\n\t\t{\n\t\t\taLayout = oSettings.aoHeader;\n\t\t\tif ( nHeader )\n\t\t\t{\n\t\t\t\taLayout = [];\n\t\t\t\t_fnDetectHeader( aLayout, nHeader );\n\t\t\t}\n\t\t}\n\t\n\t\tfor ( var i=0, iLen=aLayout.length ; i<iLen ; i++ )\n\t\t{\n\t\t\tfor ( var j=0, jLen=aLayout[i].length ; j<jLen ; j++ )\n\t\t\t{\n\t\t\t\tif ( aLayout[i][j].unique &&\n\t\t\t\t\t (!aReturn[j] || !oSettings.bSortCellsTop) )\n\t\t\t\t{\n\t\t\t\t\taReturn[j] = aLayout[i][j].cell;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\treturn aReturn;\n\t}\n\t\n\t/**\n\t * Create an Ajax call based on the table's settings, taking into account that\n\t * parameters can have multiple forms, and backwards compatibility.\n\t *\n\t * @param {object} oSettings dataTables settings object\n\t * @param {array} data Data to send to the server, required by\n\t *     DataTables - may be augmented by developer callbacks\n\t * @param {function} fn Callback function to run when data is obtained\n\t */\n\tfunction _fnBuildAjax( oSettings, data, fn )\n\t{\n\t\t// Compatibility with 1.9-, allow fnServerData and event to manipulate\n\t\t_fnCallbackFire( oSettings, 'aoServerParams', 'serverParams', [data] );\n\t\n\t\t// Convert to object based for 1.10+ if using the old array scheme which can\n\t\t// come from server-side processing or serverParams\n\t\tif ( data && $.isArray(data) ) {\n\t\t\tvar tmp = {};\n\t\t\tvar rbracket = /(.*?)\\[\\]$/;\n\t\n\t\t\t$.each( data, function (key, val) {\n\t\t\t\tvar match = val.name.match(rbracket);\n\t\n\t\t\t\tif ( match ) {\n\t\t\t\t\t// Support for arrays\n\t\t\t\t\tvar name = match[0];\n\t\n\t\t\t\t\tif ( ! tmp[ name ] ) {\n\t\t\t\t\t\ttmp[ name ] = [];\n\t\t\t\t\t}\n\t\t\t\t\ttmp[ name ].push( val.value );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\ttmp[val.name] = val.value;\n\t\t\t\t}\n\t\t\t} );\n\t\t\tdata = tmp;\n\t\t}\n\t\n\t\tvar ajaxData;\n\t\tvar ajax = oSettings.ajax;\n\t\tvar instance = oSettings.oInstance;\n\t\tvar callback = function ( json ) {\n\t\t\t_fnCallbackFire( oSettings, null, 'xhr', [oSettings, json, oSettings.jqXHR] );\n\t\t\tfn( json );\n\t\t};\n\t\n\t\tif ( $.isPlainObject( ajax ) && ajax.data )\n\t\t{\n\t\t\tajaxData = ajax.data;\n\t\n\t\t\tvar newData = typeof ajaxData === 'function' ?\n\t\t\t\tajaxData( data, oSettings ) :  // fn can manipulate data or return\n\t\t\t\tajaxData;                      // an object object or array to merge\n\t\n\t\t\t// If the function returned something, use that alone\n\t\t\tdata = typeof ajaxData === 'function' && newData ?\n\t\t\t\tnewData :\n\t\t\t\t$.extend( true, data, newData );\n\t\n\t\t\t// Remove the data property as we've resolved it already and don't want\n\t\t\t// jQuery to do it again (it is restored at the end of the function)\n\t\t\tdelete ajax.data;\n\t\t}\n\t\n\t\tvar baseAjax = {\n\t\t\t\"data\": data,\n\t\t\t\"success\": function (json) {\n\t\t\t\tvar error = json.error || json.sError;\n\t\t\t\tif ( error ) {\n\t\t\t\t\t_fnLog( oSettings, 0, error );\n\t\t\t\t}\n\t\n\t\t\t\toSettings.json = json;\n\t\t\t\tcallback( json );\n\t\t\t},\n\t\t\t\"dataType\": \"json\",\n\t\t\t\"cache\": false,\n\t\t\t\"type\": oSettings.sServerMethod,\n\t\t\t\"error\": function (xhr, error, thrown) {\n\t\t\t\tvar ret = _fnCallbackFire( oSettings, null, 'xhr', [oSettings, null, oSettings.jqXHR] );\n\t\n\t\t\t\tif ( $.inArray( true, ret ) === -1 ) {\n\t\t\t\t\tif ( error == \"parsererror\" ) {\n\t\t\t\t\t\t_fnLog( oSettings, 0, 'Invalid JSON response', 1 );\n\t\t\t\t\t}\n\t\t\t\t\telse if ( xhr.readyState === 4 ) {\n\t\t\t\t\t\t_fnLog( oSettings, 0, 'Ajax error', 7 );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\t_fnProcessingDisplay( oSettings, false );\n\t\t\t}\n\t\t};\n\t\n\t\t// Store the data submitted for the API\n\t\toSettings.oAjaxData = data;\n\t\n\t\t// Allow plug-ins and external processes to modify the data\n\t\t_fnCallbackFire( oSettings, null, 'preXhr', [oSettings, data] );\n\t\n\t\tif ( oSettings.fnServerData )\n\t\t{\n\t\t\t// DataTables 1.9- compatibility\n\t\t\toSettings.fnServerData.call( instance,\n\t\t\t\toSettings.sAjaxSource,\n\t\t\t\t$.map( data, function (val, key) { // Need to convert back to 1.9 trad format\n\t\t\t\t\treturn { name: key, value: val };\n\t\t\t\t} ),\n\t\t\t\tcallback,\n\t\t\t\toSettings\n\t\t\t);\n\t\t}\n\t\telse if ( oSettings.sAjaxSource || typeof ajax === 'string' )\n\t\t{\n\t\t\t// DataTables 1.9- compatibility\n\t\t\toSettings.jqXHR = $.ajax( $.extend( baseAjax, {\n\t\t\t\turl: ajax || oSettings.sAjaxSource\n\t\t\t} ) );\n\t\t}\n\t\telse if ( typeof ajax === 'function' )\n\t\t{\n\t\t\t// Is a function - let the caller define what needs to be done\n\t\t\toSettings.jqXHR = ajax.call( instance, data, callback, oSettings );\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// Object to extend the base settings\n\t\t\toSettings.jqXHR = $.ajax( $.extend( baseAjax, ajax ) );\n\t\n\t\t\t// Restore for next time around\n\t\t\tajax.data = ajaxData;\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Update the table using an Ajax call\n\t *  @param {object} settings dataTables settings object\n\t *  @returns {boolean} Block the table drawing or not\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnAjaxUpdate( settings )\n\t{\n\t\tif ( settings.bAjaxDataGet ) {\n\t\t\tsettings.iDraw++;\n\t\t\t_fnProcessingDisplay( settings, true );\n\t\n\t\t\t_fnBuildAjax(\n\t\t\t\tsettings,\n\t\t\t\t_fnAjaxParameters( settings ),\n\t\t\t\tfunction(json) {\n\t\t\t\t\t_fnAjaxUpdateDraw( settings, json );\n\t\t\t\t}\n\t\t\t);\n\t\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\t\n\t\n\t/**\n\t * Build up the parameters in an object needed for a server-side processing\n\t * request. Note that this is basically done twice, is different ways - a modern\n\t * method which is used by default in DataTables 1.10 which uses objects and\n\t * arrays, or the 1.9- method with is name / value pairs. 1.9 method is used if\n\t * the sAjaxSource option is used in the initialisation, or the legacyAjax\n\t * option is set.\n\t *  @param {object} oSettings dataTables settings object\n\t *  @returns {bool} block the table drawing or not\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnAjaxParameters( settings )\n\t{\n\t\tvar\n\t\t\tcolumns = settings.aoColumns,\n\t\t\tcolumnCount = columns.length,\n\t\t\tfeatures = settings.oFeatures,\n\t\t\tpreSearch = settings.oPreviousSearch,\n\t\t\tpreColSearch = settings.aoPreSearchCols,\n\t\t\ti, data = [], dataProp, column, columnSearch,\n\t\t\tsort = _fnSortFlatten( settings ),\n\t\t\tdisplayStart = settings._iDisplayStart,\n\t\t\tdisplayLength = features.bPaginate !== false ?\n\t\t\t\tsettings._iDisplayLength :\n\t\t\t\t-1;\n\t\n\t\tvar param = function ( name, value ) {\n\t\t\tdata.push( { 'name': name, 'value': value } );\n\t\t};\n\t\n\t\t// DataTables 1.9- compatible method\n\t\tparam( 'sEcho',          settings.iDraw );\n\t\tparam( 'iColumns',       columnCount );\n\t\tparam( 'sColumns',       _pluck( columns, 'sName' ).join(',') );\n\t\tparam( 'iDisplayStart',  displayStart );\n\t\tparam( 'iDisplayLength', displayLength );\n\t\n\t\t// DataTables 1.10+ method\n\t\tvar d = {\n\t\t\tdraw:    settings.iDraw,\n\t\t\tcolumns: [],\n\t\t\torder:   [],\n\t\t\tstart:   displayStart,\n\t\t\tlength:  displayLength,\n\t\t\tsearch:  {\n\t\t\t\tvalue: preSearch.sSearch,\n\t\t\t\tregex: preSearch.bRegex\n\t\t\t}\n\t\t};\n\t\n\t\tfor ( i=0 ; i<columnCount ; i++ ) {\n\t\t\tcolumn = columns[i];\n\t\t\tcolumnSearch = preColSearch[i];\n\t\t\tdataProp = typeof column.mData==\"function\" ? 'function' : column.mData ;\n\t\n\t\t\td.columns.push( {\n\t\t\t\tdata:       dataProp,\n\t\t\t\tname:       column.sName,\n\t\t\t\tsearchable: column.bSearchable,\n\t\t\t\torderable:  column.bSortable,\n\t\t\t\tsearch:     {\n\t\t\t\t\tvalue: columnSearch.sSearch,\n\t\t\t\t\tregex: columnSearch.bRegex\n\t\t\t\t}\n\t\t\t} );\n\t\n\t\t\tparam( \"mDataProp_\"+i, dataProp );\n\t\n\t\t\tif ( features.bFilter ) {\n\t\t\t\tparam( 'sSearch_'+i,     columnSearch.sSearch );\n\t\t\t\tparam( 'bRegex_'+i,      columnSearch.bRegex );\n\t\t\t\tparam( 'bSearchable_'+i, column.bSearchable );\n\t\t\t}\n\t\n\t\t\tif ( features.bSort ) {\n\t\t\t\tparam( 'bSortable_'+i, column.bSortable );\n\t\t\t}\n\t\t}\n\t\n\t\tif ( features.bFilter ) {\n\t\t\tparam( 'sSearch', preSearch.sSearch );\n\t\t\tparam( 'bRegex', preSearch.bRegex );\n\t\t}\n\t\n\t\tif ( features.bSort ) {\n\t\t\t$.each( sort, function ( i, val ) {\n\t\t\t\td.order.push( { column: val.col, dir: val.dir } );\n\t\n\t\t\t\tparam( 'iSortCol_'+i, val.col );\n\t\t\t\tparam( 'sSortDir_'+i, val.dir );\n\t\t\t} );\n\t\n\t\t\tparam( 'iSortingCols', sort.length );\n\t\t}\n\t\n\t\t// If the legacy.ajax parameter is null, then we automatically decide which\n\t\t// form to use, based on sAjaxSource\n\t\tvar legacy = DataTable.ext.legacy.ajax;\n\t\tif ( legacy === null ) {\n\t\t\treturn settings.sAjaxSource ? data : d;\n\t\t}\n\t\n\t\t// Otherwise, if legacy has been specified then we use that to decide on the\n\t\t// form\n\t\treturn legacy ? data : d;\n\t}\n\t\n\t\n\t/**\n\t * Data the data from the server (nuking the old) and redraw the table\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {object} json json data return from the server.\n\t *  @param {string} json.sEcho Tracking flag for DataTables to match requests\n\t *  @param {int} json.iTotalRecords Number of records in the data set, not accounting for filtering\n\t *  @param {int} json.iTotalDisplayRecords Number of records in the data set, accounting for filtering\n\t *  @param {array} json.aaData The data to display on this page\n\t *  @param {string} [json.sColumns] Column ordering (sName, comma separated)\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnAjaxUpdateDraw ( settings, json )\n\t{\n\t\t// v1.10 uses camelCase variables, while 1.9 uses Hungarian notation.\n\t\t// Support both\n\t\tvar compat = function ( old, modern ) {\n\t\t\treturn json[old] !== undefined ? json[old] : json[modern];\n\t\t};\n\t\n\t\tvar data = _fnAjaxDataSrc( settings, json );\n\t\tvar draw            = compat( 'sEcho',                'draw' );\n\t\tvar recordsTotal    = compat( 'iTotalRecords',        'recordsTotal' );\n\t\tvar recordsFiltered = compat( 'iTotalDisplayRecords', 'recordsFiltered' );\n\t\n\t\tif ( draw !== undefined ) {\n\t\t\t// Protect against out of sequence returns\n\t\t\tif ( draw*1 < settings.iDraw ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsettings.iDraw = draw * 1;\n\t\t}\n\t\n\t\t_fnClearTable( settings );\n\t\tsettings._iRecordsTotal   = parseInt(recordsTotal, 10);\n\t\tsettings._iRecordsDisplay = parseInt(recordsFiltered, 10);\n\t\n\t\tfor ( var i=0, ien=data.length ; i<ien ; i++ ) {\n\t\t\t_fnAddData( settings, data[i] );\n\t\t}\n\t\tsettings.aiDisplay = settings.aiDisplayMaster.slice();\n\t\n\t\tsettings.bAjaxDataGet = false;\n\t\t_fnDraw( settings );\n\t\n\t\tif ( ! settings._bInitComplete ) {\n\t\t\t_fnInitComplete( settings, json );\n\t\t}\n\t\n\t\tsettings.bAjaxDataGet = true;\n\t\t_fnProcessingDisplay( settings, false );\n\t}\n\t\n\t\n\t/**\n\t * Get the data from the JSON data source to use for drawing a table. Using\n\t * `_fnGetObjectDataFn` allows the data to be sourced from a property of the\n\t * source object, or from a processing function.\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param  {object} json Data source object / array from the server\n\t *  @return {array} Array of data to use\n\t */\n\tfunction _fnAjaxDataSrc ( oSettings, json )\n\t{\n\t\tvar dataSrc = $.isPlainObject( oSettings.ajax ) && oSettings.ajax.dataSrc !== undefined ?\n\t\t\toSettings.ajax.dataSrc :\n\t\t\toSettings.sAjaxDataProp; // Compatibility with 1.9-.\n\t\n\t\t// Compatibility with 1.9-. In order to read from aaData, check if the\n\t\t// default has been changed, if not, check for aaData\n\t\tif ( dataSrc === 'data' ) {\n\t\t\treturn json.aaData || json[dataSrc];\n\t\t}\n\t\n\t\treturn dataSrc !== \"\" ?\n\t\t\t_fnGetObjectDataFn( dataSrc )( json ) :\n\t\t\tjson;\n\t}\n\t\n\t/**\n\t * Generate the node required for filtering text\n\t *  @returns {node} Filter control element\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnFeatureHtmlFilter ( settings )\n\t{\n\t\tvar classes = settings.oClasses;\n\t\tvar tableId = settings.sTableId;\n\t\tvar language = settings.oLanguage;\n\t\tvar previousSearch = settings.oPreviousSearch;\n\t\tvar features = settings.aanFeatures;\n\t\tvar input = '<input type=\"search\" class=\"'+classes.sFilterInput+'\"/>';\n\t\n\t\tvar str = language.sSearch;\n\t\tstr = str.match(/_INPUT_/) ?\n\t\t\tstr.replace('_INPUT_', input) :\n\t\t\tstr+input;\n\t\n\t\tvar filter = $('<div/>', {\n\t\t\t\t'id': ! features.f ? tableId+'_filter' : null,\n\t\t\t\t'class': classes.sFilter\n\t\t\t} )\n\t\t\t.append( $('<label/>' ).append( str ) );\n\t\n\t\tvar searchFn = function() {\n\t\t\t/* Update all other filter input elements for the new display */\n\t\t\tvar n = features.f;\n\t\t\tvar val = !this.value ? \"\" : this.value; // mental IE8 fix :-(\n\t\n\t\t\t/* Now do the filter */\n\t\t\tif ( val != previousSearch.sSearch ) {\n\t\t\t\t_fnFilterComplete( settings, {\n\t\t\t\t\t\"sSearch\": val,\n\t\t\t\t\t\"bRegex\": previousSearch.bRegex,\n\t\t\t\t\t\"bSmart\": previousSearch.bSmart ,\n\t\t\t\t\t\"bCaseInsensitive\": previousSearch.bCaseInsensitive\n\t\t\t\t} );\n\t\n\t\t\t\t// Need to redraw, without resorting\n\t\t\t\tsettings._iDisplayStart = 0;\n\t\t\t\t_fnDraw( settings );\n\t\t\t}\n\t\t};\n\t\n\t\tvar searchDelay = settings.searchDelay !== null ?\n\t\t\tsettings.searchDelay :\n\t\t\t_fnDataSource( settings ) === 'ssp' ?\n\t\t\t\t400 :\n\t\t\t\t0;\n\t\n\t\tvar jqFilter = $('input', filter)\n\t\t\t.val( previousSearch.sSearch )\n\t\t\t.attr( 'placeholder', language.sSearchPlaceholder )\n\t\t\t.on(\n\t\t\t\t'keyup.DT search.DT input.DT paste.DT cut.DT',\n\t\t\t\tsearchDelay ?\n\t\t\t\t\t_fnThrottle( searchFn, searchDelay ) :\n\t\t\t\t\tsearchFn\n\t\t\t)\n\t\t\t.on( 'mouseup', function(e) {\n\t\t\t\t// Edge fix! Edge 17 does not trigger anything other than mouse events when clicking\n\t\t\t\t// on the clear icon (Edge bug 17584515). This is safe in other browsers as `searchFn`\n\t\t\t\t// checks the value to see if it has changed. In other browsers it won't have.\n\t\t\t\tsetTimeout( function () {\n\t\t\t\t\tsearchFn.call(jqFilter[0]);\n\t\t\t\t}, 10);\n\t\t\t} )\n\t\t\t.on( 'keypress.DT', function(e) {\n\t\t\t\t/* Prevent form submission */\n\t\t\t\tif ( e.keyCode == 13 ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} )\n\t\t\t.attr('aria-controls', tableId);\n\t\n\t\t// Update the input elements whenever the table is filtered\n\t\t$(settings.nTable).on( 'search.dt.DT', function ( ev, s ) {\n\t\t\tif ( settings === s ) {\n\t\t\t\t// IE9 throws an 'unknown error' if document.activeElement is used\n\t\t\t\t// inside an iframe or frame...\n\t\t\t\ttry {\n\t\t\t\t\tif ( jqFilter[0] !== document.activeElement ) {\n\t\t\t\t\t\tjqFilter.val( previousSearch.sSearch );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcatch ( e ) {}\n\t\t\t}\n\t\t} );\n\t\n\t\treturn filter[0];\n\t}\n\t\n\t\n\t/**\n\t * Filter the table using both the global filter and column based filtering\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {object} oSearch search information\n\t *  @param {int} [iForce] force a research of the master array (1) or not (undefined or 0)\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnFilterComplete ( oSettings, oInput, iForce )\n\t{\n\t\tvar oPrevSearch = oSettings.oPreviousSearch;\n\t\tvar aoPrevSearch = oSettings.aoPreSearchCols;\n\t\tvar fnSaveFilter = function ( oFilter ) {\n\t\t\t/* Save the filtering values */\n\t\t\toPrevSearch.sSearch = oFilter.sSearch;\n\t\t\toPrevSearch.bRegex = oFilter.bRegex;\n\t\t\toPrevSearch.bSmart = oFilter.bSmart;\n\t\t\toPrevSearch.bCaseInsensitive = oFilter.bCaseInsensitive;\n\t\t};\n\t\tvar fnRegex = function ( o ) {\n\t\t\t// Backwards compatibility with the bEscapeRegex option\n\t\t\treturn o.bEscapeRegex !== undefined ? !o.bEscapeRegex : o.bRegex;\n\t\t};\n\t\n\t\t// Resolve any column types that are unknown due to addition or invalidation\n\t\t// @todo As per sort - can this be moved into an event handler?\n\t\t_fnColumnTypes( oSettings );\n\t\n\t\t/* In server-side processing all filtering is done by the server, so no point hanging around here */\n\t\tif ( _fnDataSource( oSettings ) != 'ssp' )\n\t\t{\n\t\t\t/* Global filter */\n\t\t\t_fnFilter( oSettings, oInput.sSearch, iForce, fnRegex(oInput), oInput.bSmart, oInput.bCaseInsensitive );\n\t\t\tfnSaveFilter( oInput );\n\t\n\t\t\t/* Now do the individual column filter */\n\t\t\tfor ( var i=0 ; i<aoPrevSearch.length ; i++ )\n\t\t\t{\n\t\t\t\t_fnFilterColumn( oSettings, aoPrevSearch[i].sSearch, i, fnRegex(aoPrevSearch[i]),\n\t\t\t\t\taoPrevSearch[i].bSmart, aoPrevSearch[i].bCaseInsensitive );\n\t\t\t}\n\t\n\t\t\t/* Custom filtering */\n\t\t\t_fnFilterCustom( oSettings );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tfnSaveFilter( oInput );\n\t\t}\n\t\n\t\t/* Tell the draw function we have been filtering */\n\t\toSettings.bFiltered = true;\n\t\t_fnCallbackFire( oSettings, null, 'search', [oSettings] );\n\t}\n\t\n\t\n\t/**\n\t * Apply custom filtering functions\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnFilterCustom( settings )\n\t{\n\t\tvar filters = DataTable.ext.search;\n\t\tvar displayRows = settings.aiDisplay;\n\t\tvar row, rowIdx;\n\t\n\t\tfor ( var i=0, ien=filters.length ; i<ien ; i++ ) {\n\t\t\tvar rows = [];\n\t\n\t\t\t// Loop over each row and see if it should be included\n\t\t\tfor ( var j=0, jen=displayRows.length ; j<jen ; j++ ) {\n\t\t\t\trowIdx = displayRows[ j ];\n\t\t\t\trow = settings.aoData[ rowIdx ];\n\t\n\t\t\t\tif ( filters[i]( settings, row._aFilterData, rowIdx, row._aData, j ) ) {\n\t\t\t\t\trows.push( rowIdx );\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t// So the array reference doesn't break set the results into the\n\t\t\t// existing array\n\t\t\tdisplayRows.length = 0;\n\t\t\t$.merge( displayRows, rows );\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Filter the table on a per-column basis\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {string} sInput string to filter on\n\t *  @param {int} iColumn column to filter\n\t *  @param {bool} bRegex treat search string as a regular expression or not\n\t *  @param {bool} bSmart use smart filtering or not\n\t *  @param {bool} bCaseInsensitive Do case insenstive matching or not\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnFilterColumn ( settings, searchStr, colIdx, regex, smart, caseInsensitive )\n\t{\n\t\tif ( searchStr === '' ) {\n\t\t\treturn;\n\t\t}\n\t\n\t\tvar data;\n\t\tvar out = [];\n\t\tvar display = settings.aiDisplay;\n\t\tvar rpSearch = _fnFilterCreateSearch( searchStr, regex, smart, caseInsensitive );\n\t\n\t\tfor ( var i=0 ; i<display.length ; i++ ) {\n\t\t\tdata = settings.aoData[ display[i] ]._aFilterData[ colIdx ];\n\t\n\t\t\tif ( rpSearch.test( data ) ) {\n\t\t\t\tout.push( display[i] );\n\t\t\t}\n\t\t}\n\t\n\t\tsettings.aiDisplay = out;\n\t}\n\t\n\t\n\t/**\n\t * Filter the data table based on user input and draw the table\n\t *  @param {object} settings dataTables settings object\n\t *  @param {string} input string to filter on\n\t *  @param {int} force optional - force a research of the master array (1) or not (undefined or 0)\n\t *  @param {bool} regex treat as a regular expression or not\n\t *  @param {bool} smart perform smart filtering or not\n\t *  @param {bool} caseInsensitive Do case insenstive matching or not\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnFilter( settings, input, force, regex, smart, caseInsensitive )\n\t{\n\t\tvar rpSearch = _fnFilterCreateSearch( input, regex, smart, caseInsensitive );\n\t\tvar prevSearch = settings.oPreviousSearch.sSearch;\n\t\tvar displayMaster = settings.aiDisplayMaster;\n\t\tvar display, invalidated, i;\n\t\tvar filtered = [];\n\t\n\t\t// Need to take account of custom filtering functions - always filter\n\t\tif ( DataTable.ext.search.length !== 0 ) {\n\t\t\tforce = true;\n\t\t}\n\t\n\t\t// Check if any of the rows were invalidated\n\t\tinvalidated = _fnFilterData( settings );\n\t\n\t\t// If the input is blank - we just want the full data set\n\t\tif ( input.length <= 0 ) {\n\t\t\tsettings.aiDisplay = displayMaster.slice();\n\t\t}\n\t\telse {\n\t\t\t// New search - start from the master array\n\t\t\tif ( invalidated ||\n\t\t\t\t force ||\n\t\t\t\t regex ||\n\t\t\t\t prevSearch.length > input.length ||\n\t\t\t\t input.indexOf(prevSearch) !== 0 ||\n\t\t\t\t settings.bSorted // On resort, the display master needs to be\n\t\t\t\t                  // re-filtered since indexes will have changed\n\t\t\t) {\n\t\t\t\tsettings.aiDisplay = displayMaster.slice();\n\t\t\t}\n\t\n\t\t\t// Search the display array\n\t\t\tdisplay = settings.aiDisplay;\n\t\n\t\t\tfor ( i=0 ; i<display.length ; i++ ) {\n\t\t\t\tif ( rpSearch.test( settings.aoData[ display[i] ]._sFilterRow ) ) {\n\t\t\t\t\tfiltered.push( display[i] );\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\tsettings.aiDisplay = filtered;\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Build a regular expression object suitable for searching a table\n\t *  @param {string} sSearch string to search for\n\t *  @param {bool} bRegex treat as a regular expression or not\n\t *  @param {bool} bSmart perform smart filtering or not\n\t *  @param {bool} bCaseInsensitive Do case insensitive matching or not\n\t *  @returns {RegExp} constructed object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnFilterCreateSearch( search, regex, smart, caseInsensitive )\n\t{\n\t\tsearch = regex ?\n\t\t\tsearch :\n\t\t\t_fnEscapeRegex( search );\n\t\t\n\t\tif ( smart ) {\n\t\t\t/* For smart filtering we want to allow the search to work regardless of\n\t\t\t * word order. We also want double quoted text to be preserved, so word\n\t\t\t * order is important - a la google. So this is what we want to\n\t\t\t * generate:\n\t\t\t * \n\t\t\t * ^(?=.*?\\bone\\b)(?=.*?\\btwo three\\b)(?=.*?\\bfour\\b).*$\n\t\t\t */\n\t\t\tvar a = $.map( search.match( /\"[^\"]+\"|[^ ]+/g ) || [''], function ( word ) {\n\t\t\t\tif ( word.charAt(0) === '\"' ) {\n\t\t\t\t\tvar m = word.match( /^\"(.*)\"$/ );\n\t\t\t\t\tword = m ? m[1] : word;\n\t\t\t\t}\n\t\n\t\t\t\treturn word.replace('\"', '');\n\t\t\t} );\n\t\n\t\t\tsearch = '^(?=.*?'+a.join( ')(?=.*?' )+').*$';\n\t\t}\n\t\n\t\treturn new RegExp( search, caseInsensitive ? 'i' : '' );\n\t}\n\t\n\t\n\t/**\n\t * Escape a string such that it can be used in a regular expression\n\t *  @param {string} sVal string to escape\n\t *  @returns {string} escaped string\n\t *  @memberof DataTable#oApi\n\t */\n\tvar _fnEscapeRegex = DataTable.util.escapeRegex;\n\t\n\tvar __filter_div = $('<div>')[0];\n\tvar __filter_div_textContent = __filter_div.textContent !== undefined;\n\t\n\t// Update the filtering data for each row if needed (by invalidation or first run)\n\tfunction _fnFilterData ( settings )\n\t{\n\t\tvar columns = settings.aoColumns;\n\t\tvar column;\n\t\tvar i, j, ien, jen, filterData, cellData, row;\n\t\tvar fomatters = DataTable.ext.type.search;\n\t\tvar wasInvalidated = false;\n\t\n\t\tfor ( i=0, ien=settings.aoData.length ; i<ien ; i++ ) {\n\t\t\trow = settings.aoData[i];\n\t\n\t\t\tif ( ! row._aFilterData ) {\n\t\t\t\tfilterData = [];\n\t\n\t\t\t\tfor ( j=0, jen=columns.length ; j<jen ; j++ ) {\n\t\t\t\t\tcolumn = columns[j];\n\t\n\t\t\t\t\tif ( column.bSearchable ) {\n\t\t\t\t\t\tcellData = _fnGetCellData( settings, i, j, 'filter' );\n\t\n\t\t\t\t\t\tif ( fomatters[ column.sType ] ) {\n\t\t\t\t\t\t\tcellData = fomatters[ column.sType ]( cellData );\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t// Search in DataTables 1.10 is string based. In 1.11 this\n\t\t\t\t\t\t// should be altered to also allow strict type checking.\n\t\t\t\t\t\tif ( cellData === null ) {\n\t\t\t\t\t\t\tcellData = '';\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\tif ( typeof cellData !== 'string' && cellData.toString ) {\n\t\t\t\t\t\t\tcellData = cellData.toString();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tcellData = '';\n\t\t\t\t\t}\n\t\n\t\t\t\t\t// If it looks like there is an HTML entity in the string,\n\t\t\t\t\t// attempt to decode it so sorting works as expected. Note that\n\t\t\t\t\t// we could use a single line of jQuery to do this, but the DOM\n\t\t\t\t\t// method used here is much faster http://jsperf.com/html-decode\n\t\t\t\t\tif ( cellData.indexOf && cellData.indexOf('&') !== -1 ) {\n\t\t\t\t\t\t__filter_div.innerHTML = cellData;\n\t\t\t\t\t\tcellData = __filter_div_textContent ?\n\t\t\t\t\t\t\t__filter_div.textContent :\n\t\t\t\t\t\t\t__filter_div.innerText;\n\t\t\t\t\t}\n\t\n\t\t\t\t\tif ( cellData.replace ) {\n\t\t\t\t\t\tcellData = cellData.replace(/[\\r\\n\\u2028]/g, '');\n\t\t\t\t\t}\n\t\n\t\t\t\t\tfilterData.push( cellData );\n\t\t\t\t}\n\t\n\t\t\t\trow._aFilterData = filterData;\n\t\t\t\trow._sFilterRow = filterData.join('  ');\n\t\t\t\twasInvalidated = true;\n\t\t\t}\n\t\t}\n\t\n\t\treturn wasInvalidated;\n\t}\n\t\n\t\n\t/**\n\t * Convert from the internal Hungarian notation to camelCase for external\n\t * interaction\n\t *  @param {object} obj Object to convert\n\t *  @returns {object} Inverted object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnSearchToCamel ( obj )\n\t{\n\t\treturn {\n\t\t\tsearch:          obj.sSearch,\n\t\t\tsmart:           obj.bSmart,\n\t\t\tregex:           obj.bRegex,\n\t\t\tcaseInsensitive: obj.bCaseInsensitive\n\t\t};\n\t}\n\t\n\t\n\t\n\t/**\n\t * Convert from camelCase notation to the internal Hungarian. We could use the\n\t * Hungarian convert function here, but this is cleaner\n\t *  @param {object} obj Object to convert\n\t *  @returns {object} Inverted object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnSearchToHung ( obj )\n\t{\n\t\treturn {\n\t\t\tsSearch:          obj.search,\n\t\t\tbSmart:           obj.smart,\n\t\t\tbRegex:           obj.regex,\n\t\t\tbCaseInsensitive: obj.caseInsensitive\n\t\t};\n\t}\n\t\n\t/**\n\t * Generate the node required for the info display\n\t *  @param {object} oSettings dataTables settings object\n\t *  @returns {node} Information element\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnFeatureHtmlInfo ( settings )\n\t{\n\t\tvar\n\t\t\ttid = settings.sTableId,\n\t\t\tnodes = settings.aanFeatures.i,\n\t\t\tn = $('<div/>', {\n\t\t\t\t'class': settings.oClasses.sInfo,\n\t\t\t\t'id': ! nodes ? tid+'_info' : null\n\t\t\t} );\n\t\n\t\tif ( ! nodes ) {\n\t\t\t// Update display on each draw\n\t\t\tsettings.aoDrawCallback.push( {\n\t\t\t\t\"fn\": _fnUpdateInfo,\n\t\t\t\t\"sName\": \"information\"\n\t\t\t} );\n\t\n\t\t\tn\n\t\t\t\t.attr( 'role', 'status' )\n\t\t\t\t.attr( 'aria-live', 'polite' );\n\t\n\t\t\t// Table is described by our info div\n\t\t\t$(settings.nTable).attr( 'aria-describedby', tid+'_info' );\n\t\t}\n\t\n\t\treturn n[0];\n\t}\n\t\n\t\n\t/**\n\t * Update the information elements in the display\n\t *  @param {object} settings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnUpdateInfo ( settings )\n\t{\n\t\t/* Show information about the table */\n\t\tvar nodes = settings.aanFeatures.i;\n\t\tif ( nodes.length === 0 ) {\n\t\t\treturn;\n\t\t}\n\t\n\t\tvar\n\t\t\tlang  = settings.oLanguage,\n\t\t\tstart = settings._iDisplayStart+1,\n\t\t\tend   = settings.fnDisplayEnd(),\n\t\t\tmax   = settings.fnRecordsTotal(),\n\t\t\ttotal = settings.fnRecordsDisplay(),\n\t\t\tout   = total ?\n\t\t\t\tlang.sInfo :\n\t\t\t\tlang.sInfoEmpty;\n\t\n\t\tif ( total !== max ) {\n\t\t\t/* Record set after filtering */\n\t\t\tout += ' ' + lang.sInfoFiltered;\n\t\t}\n\t\n\t\t// Convert the macros\n\t\tout += lang.sInfoPostFix;\n\t\tout = _fnInfoMacros( settings, out );\n\t\n\t\tvar callback = lang.fnInfoCallback;\n\t\tif ( callback !== null ) {\n\t\t\tout = callback.call( settings.oInstance,\n\t\t\t\tsettings, start, end, max, total, out\n\t\t\t);\n\t\t}\n\t\n\t\t$(nodes).html( out );\n\t}\n\t\n\t\n\tfunction _fnInfoMacros ( settings, str )\n\t{\n\t\t// When infinite scrolling, we are always starting at 1. _iDisplayStart is used only\n\t\t// internally\n\t\tvar\n\t\t\tformatter  = settings.fnFormatNumber,\n\t\t\tstart      = settings._iDisplayStart+1,\n\t\t\tlen        = settings._iDisplayLength,\n\t\t\tvis        = settings.fnRecordsDisplay(),\n\t\t\tall        = len === -1;\n\t\n\t\treturn str.\n\t\t\treplace(/_START_/g, formatter.call( settings, start ) ).\n\t\t\treplace(/_END_/g,   formatter.call( settings, settings.fnDisplayEnd() ) ).\n\t\t\treplace(/_MAX_/g,   formatter.call( settings, settings.fnRecordsTotal() ) ).\n\t\t\treplace(/_TOTAL_/g, formatter.call( settings, vis ) ).\n\t\t\treplace(/_PAGE_/g,  formatter.call( settings, all ? 1 : Math.ceil( start / len ) ) ).\n\t\t\treplace(/_PAGES_/g, formatter.call( settings, all ? 1 : Math.ceil( vis / len ) ) );\n\t}\n\t\n\t\n\t\n\t/**\n\t * Draw the table for the first time, adding all required features\n\t *  @param {object} settings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnInitialise ( settings )\n\t{\n\t\tvar i, iLen, iAjaxStart=settings.iInitDisplayStart;\n\t\tvar columns = settings.aoColumns, column;\n\t\tvar features = settings.oFeatures;\n\t\tvar deferLoading = settings.bDeferLoading; // value modified by the draw\n\t\n\t\t/* Ensure that the table data is fully initialised */\n\t\tif ( ! settings.bInitialised ) {\n\t\t\tsetTimeout( function(){ _fnInitialise( settings ); }, 200 );\n\t\t\treturn;\n\t\t}\n\t\n\t\t/* Show the display HTML options */\n\t\t_fnAddOptionsHtml( settings );\n\t\n\t\t/* Build and draw the header / footer for the table */\n\t\t_fnBuildHead( settings );\n\t\t_fnDrawHead( settings, settings.aoHeader );\n\t\t_fnDrawHead( settings, settings.aoFooter );\n\t\n\t\t/* Okay to show that something is going on now */\n\t\t_fnProcessingDisplay( settings, true );\n\t\n\t\t/* Calculate sizes for columns */\n\t\tif ( features.bAutoWidth ) {\n\t\t\t_fnCalculateColumnWidths( settings );\n\t\t}\n\t\n\t\tfor ( i=0, iLen=columns.length ; i<iLen ; i++ ) {\n\t\t\tcolumn = columns[i];\n\t\n\t\t\tif ( column.sWidth ) {\n\t\t\t\tcolumn.nTh.style.width = _fnStringToCss( column.sWidth );\n\t\t\t}\n\t\t}\n\t\n\t\t_fnCallbackFire( settings, null, 'preInit', [settings] );\n\t\n\t\t// If there is default sorting required - let's do it. The sort function\n\t\t// will do the drawing for us. Otherwise we draw the table regardless of the\n\t\t// Ajax source - this allows the table to look initialised for Ajax sourcing\n\t\t// data (show 'loading' message possibly)\n\t\t_fnReDraw( settings );\n\t\n\t\t// Server-side processing init complete is done by _fnAjaxUpdateDraw\n\t\tvar dataSrc = _fnDataSource( settings );\n\t\tif ( dataSrc != 'ssp' || deferLoading ) {\n\t\t\t// if there is an ajax source load the data\n\t\t\tif ( dataSrc == 'ajax' ) {\n\t\t\t\t_fnBuildAjax( settings, [], function(json) {\n\t\t\t\t\tvar aData = _fnAjaxDataSrc( settings, json );\n\t\n\t\t\t\t\t// Got the data - add it to the table\n\t\t\t\t\tfor ( i=0 ; i<aData.length ; i++ ) {\n\t\t\t\t\t\t_fnAddData( settings, aData[i] );\n\t\t\t\t\t}\n\t\n\t\t\t\t\t// Reset the init display for cookie saving. We've already done\n\t\t\t\t\t// a filter, and therefore cleared it before. So we need to make\n\t\t\t\t\t// it appear 'fresh'\n\t\t\t\t\tsettings.iInitDisplayStart = iAjaxStart;\n\t\n\t\t\t\t\t_fnReDraw( settings );\n\t\n\t\t\t\t\t_fnProcessingDisplay( settings, false );\n\t\t\t\t\t_fnInitComplete( settings, json );\n\t\t\t\t}, settings );\n\t\t\t}\n\t\t\telse {\n\t\t\t\t_fnProcessingDisplay( settings, false );\n\t\t\t\t_fnInitComplete( settings );\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Draw the table for the first time, adding all required features\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {object} [json] JSON from the server that completed the table, if using Ajax source\n\t *    with client-side processing (optional)\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnInitComplete ( settings, json )\n\t{\n\t\tsettings._bInitComplete = true;\n\t\n\t\t// When data was added after the initialisation (data or Ajax) we need to\n\t\t// calculate the column sizing\n\t\tif ( json || settings.oInit.aaData ) {\n\t\t\t_fnAdjustColumnSizing( settings );\n\t\t}\n\t\n\t\t_fnCallbackFire( settings, null, 'plugin-init', [settings, json] );\n\t\t_fnCallbackFire( settings, 'aoInitComplete', 'init', [settings, json] );\n\t}\n\t\n\t\n\tfunction _fnLengthChange ( settings, val )\n\t{\n\t\tvar len = parseInt( val, 10 );\n\t\tsettings._iDisplayLength = len;\n\t\n\t\t_fnLengthOverflow( settings );\n\t\n\t\t// Fire length change event\n\t\t_fnCallbackFire( settings, null, 'length', [settings, len] );\n\t}\n\t\n\t\n\t/**\n\t * Generate the node required for user display length changing\n\t *  @param {object} settings dataTables settings object\n\t *  @returns {node} Display length feature node\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnFeatureHtmlLength ( settings )\n\t{\n\t\tvar\n\t\t\tclasses  = settings.oClasses,\n\t\t\ttableId  = settings.sTableId,\n\t\t\tmenu     = settings.aLengthMenu,\n\t\t\td2       = $.isArray( menu[0] ),\n\t\t\tlengths  = d2 ? menu[0] : menu,\n\t\t\tlanguage = d2 ? menu[1] : menu;\n\t\n\t\tvar select = $('<select/>', {\n\t\t\t'name':          tableId+'_length',\n\t\t\t'aria-controls': tableId,\n\t\t\t'class':         classes.sLengthSelect\n\t\t} );\n\t\n\t\tfor ( var i=0, ien=lengths.length ; i<ien ; i++ ) {\n\t\t\tselect[0][ i ] = new Option(\n\t\t\t\ttypeof language[i] === 'number' ?\n\t\t\t\t\tsettings.fnFormatNumber( language[i] ) :\n\t\t\t\t\tlanguage[i],\n\t\t\t\tlengths[i]\n\t\t\t);\n\t\t}\n\t\n\t\tvar div = $('<div><label/></div>').addClass( classes.sLength );\n\t\tif ( ! settings.aanFeatures.l ) {\n\t\t\tdiv[0].id = tableId+'_length';\n\t\t}\n\t\n\t\tdiv.children().append(\n\t\t\tsettings.oLanguage.sLengthMenu.replace( '_MENU_', select[0].outerHTML )\n\t\t);\n\t\n\t\t// Can't use `select` variable as user might provide their own and the\n\t\t// reference is broken by the use of outerHTML\n\t\t$('select', div)\n\t\t\t.val( settings._iDisplayLength )\n\t\t\t.on( 'change.DT', function(e) {\n\t\t\t\t_fnLengthChange( settings, $(this).val() );\n\t\t\t\t_fnDraw( settings );\n\t\t\t} );\n\t\n\t\t// Update node value whenever anything changes the table's length\n\t\t$(settings.nTable).on( 'length.dt.DT', function (e, s, len) {\n\t\t\tif ( settings === s ) {\n\t\t\t\t$('select', div).val( len );\n\t\t\t}\n\t\t} );\n\t\n\t\treturn div[0];\n\t}\n\t\n\t\n\t\n\t/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n\t * Note that most of the paging logic is done in\n\t * DataTable.ext.pager\n\t */\n\t\n\t/**\n\t * Generate the node required for default pagination\n\t *  @param {object} oSettings dataTables settings object\n\t *  @returns {node} Pagination feature node\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnFeatureHtmlPaginate ( settings )\n\t{\n\t\tvar\n\t\t\ttype   = settings.sPaginationType,\n\t\t\tplugin = DataTable.ext.pager[ type ],\n\t\t\tmodern = typeof plugin === 'function',\n\t\t\tredraw = function( settings ) {\n\t\t\t\t_fnDraw( settings );\n\t\t\t},\n\t\t\tnode = $('<div/>').addClass( settings.oClasses.sPaging + type )[0],\n\t\t\tfeatures = settings.aanFeatures;\n\t\n\t\tif ( ! modern ) {\n\t\t\tplugin.fnInit( settings, node, redraw );\n\t\t}\n\t\n\t\t/* Add a draw callback for the pagination on first instance, to update the paging display */\n\t\tif ( ! features.p )\n\t\t{\n\t\t\tnode.id = settings.sTableId+'_paginate';\n\t\n\t\t\tsettings.aoDrawCallback.push( {\n\t\t\t\t\"fn\": function( settings ) {\n\t\t\t\t\tif ( modern ) {\n\t\t\t\t\t\tvar\n\t\t\t\t\t\t\tstart      = settings._iDisplayStart,\n\t\t\t\t\t\t\tlen        = settings._iDisplayLength,\n\t\t\t\t\t\t\tvisRecords = settings.fnRecordsDisplay(),\n\t\t\t\t\t\t\tall        = len === -1,\n\t\t\t\t\t\t\tpage = all ? 0 : Math.ceil( start / len ),\n\t\t\t\t\t\t\tpages = all ? 1 : Math.ceil( visRecords / len ),\n\t\t\t\t\t\t\tbuttons = plugin(page, pages),\n\t\t\t\t\t\t\ti, ien;\n\t\n\t\t\t\t\t\tfor ( i=0, ien=features.p.length ; i<ien ; i++ ) {\n\t\t\t\t\t\t\t_fnRenderer( settings, 'pageButton' )(\n\t\t\t\t\t\t\t\tsettings, features.p[i], i, buttons, page, pages\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tplugin.fnUpdate( settings, redraw );\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"sName\": \"pagination\"\n\t\t\t} );\n\t\t}\n\t\n\t\treturn node;\n\t}\n\t\n\t\n\t/**\n\t * Alter the display settings to change the page\n\t *  @param {object} settings DataTables settings object\n\t *  @param {string|int} action Paging action to take: \"first\", \"previous\",\n\t *    \"next\" or \"last\" or page number to jump to (integer)\n\t *  @param [bool] redraw Automatically draw the update or not\n\t *  @returns {bool} true page has changed, false - no change\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnPageChange ( settings, action, redraw )\n\t{\n\t\tvar\n\t\t\tstart     = settings._iDisplayStart,\n\t\t\tlen       = settings._iDisplayLength,\n\t\t\trecords   = settings.fnRecordsDisplay();\n\t\n\t\tif ( records === 0 || len === -1 )\n\t\t{\n\t\t\tstart = 0;\n\t\t}\n\t\telse if ( typeof action === \"number\" )\n\t\t{\n\t\t\tstart = action * len;\n\t\n\t\t\tif ( start > records )\n\t\t\t{\n\t\t\t\tstart = 0;\n\t\t\t}\n\t\t}\n\t\telse if ( action == \"first\" )\n\t\t{\n\t\t\tstart = 0;\n\t\t}\n\t\telse if ( action == \"previous\" )\n\t\t{\n\t\t\tstart = len >= 0 ?\n\t\t\t\tstart - len :\n\t\t\t\t0;\n\t\n\t\t\tif ( start < 0 )\n\t\t\t{\n\t\t\t  start = 0;\n\t\t\t}\n\t\t}\n\t\telse if ( action == \"next\" )\n\t\t{\n\t\t\tif ( start + len < records )\n\t\t\t{\n\t\t\t\tstart += len;\n\t\t\t}\n\t\t}\n\t\telse if ( action == \"last\" )\n\t\t{\n\t\t\tstart = Math.floor( (records-1) / len) * len;\n\t\t}\n\t\telse\n\t\t{\n\t\t\t_fnLog( settings, 0, \"Unknown paging action: \"+action, 5 );\n\t\t}\n\t\n\t\tvar changed = settings._iDisplayStart !== start;\n\t\tsettings._iDisplayStart = start;\n\t\n\t\tif ( changed ) {\n\t\t\t_fnCallbackFire( settings, null, 'page', [settings] );\n\t\n\t\t\tif ( redraw ) {\n\t\t\t\t_fnDraw( settings );\n\t\t\t}\n\t\t}\n\t\n\t\treturn changed;\n\t}\n\t\n\t\n\t\n\t/**\n\t * Generate the node required for the processing node\n\t *  @param {object} settings dataTables settings object\n\t *  @returns {node} Processing element\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnFeatureHtmlProcessing ( settings )\n\t{\n\t\treturn $('<div/>', {\n\t\t\t\t'id': ! settings.aanFeatures.r ? settings.sTableId+'_processing' : null,\n\t\t\t\t'class': settings.oClasses.sProcessing\n\t\t\t} )\n\t\t\t.html( settings.oLanguage.sProcessing )\n\t\t\t.insertBefore( settings.nTable )[0];\n\t}\n\t\n\t\n\t/**\n\t * Display or hide the processing indicator\n\t *  @param {object} settings dataTables settings object\n\t *  @param {bool} show Show the processing indicator (true) or not (false)\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnProcessingDisplay ( settings, show )\n\t{\n\t\tif ( settings.oFeatures.bProcessing ) {\n\t\t\t$(settings.aanFeatures.r).css( 'display', show ? 'block' : 'none' );\n\t\t}\n\t\n\t\t_fnCallbackFire( settings, null, 'processing', [settings, show] );\n\t}\n\t\n\t/**\n\t * Add any control elements for the table - specifically scrolling\n\t *  @param {object} settings dataTables settings object\n\t *  @returns {node} Node to add to the DOM\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnFeatureHtmlTable ( settings )\n\t{\n\t\tvar table = $(settings.nTable);\n\t\n\t\t// Add the ARIA grid role to the table\n\t\ttable.attr( 'role', 'grid' );\n\t\n\t\t// Scrolling from here on in\n\t\tvar scroll = settings.oScroll;\n\t\n\t\tif ( scroll.sX === '' && scroll.sY === '' ) {\n\t\t\treturn settings.nTable;\n\t\t}\n\t\n\t\tvar scrollX = scroll.sX;\n\t\tvar scrollY = scroll.sY;\n\t\tvar classes = settings.oClasses;\n\t\tvar caption = table.children('caption');\n\t\tvar captionSide = caption.length ? caption[0]._captionSide : null;\n\t\tvar headerClone = $( table[0].cloneNode(false) );\n\t\tvar footerClone = $( table[0].cloneNode(false) );\n\t\tvar footer = table.children('tfoot');\n\t\tvar _div = '<div/>';\n\t\tvar size = function ( s ) {\n\t\t\treturn !s ? null : _fnStringToCss( s );\n\t\t};\n\t\n\t\tif ( ! footer.length ) {\n\t\t\tfooter = null;\n\t\t}\n\t\n\t\t/*\n\t\t * The HTML structure that we want to generate in this function is:\n\t\t *  div - scroller\n\t\t *    div - scroll head\n\t\t *      div - scroll head inner\n\t\t *        table - scroll head table\n\t\t *          thead - thead\n\t\t *    div - scroll body\n\t\t *      table - table (master table)\n\t\t *        thead - thead clone for sizing\n\t\t *        tbody - tbody\n\t\t *    div - scroll foot\n\t\t *      div - scroll foot inner\n\t\t *        table - scroll foot table\n\t\t *          tfoot - tfoot\n\t\t */\n\t\tvar scroller = $( _div, { 'class': classes.sScrollWrapper } )\n\t\t\t.append(\n\t\t\t\t$(_div, { 'class': classes.sScrollHead } )\n\t\t\t\t\t.css( {\n\t\t\t\t\t\toverflow: 'hidden',\n\t\t\t\t\t\tposition: 'relative',\n\t\t\t\t\t\tborder: 0,\n\t\t\t\t\t\twidth: scrollX ? size(scrollX) : '100%'\n\t\t\t\t\t} )\n\t\t\t\t\t.append(\n\t\t\t\t\t\t$(_div, { 'class': classes.sScrollHeadInner } )\n\t\t\t\t\t\t\t.css( {\n\t\t\t\t\t\t\t\t'box-sizing': 'content-box',\n\t\t\t\t\t\t\t\twidth: scroll.sXInner || '100%'\n\t\t\t\t\t\t\t} )\n\t\t\t\t\t\t\t.append(\n\t\t\t\t\t\t\t\theaderClone\n\t\t\t\t\t\t\t\t\t.removeAttr('id')\n\t\t\t\t\t\t\t\t\t.css( 'margin-left', 0 )\n\t\t\t\t\t\t\t\t\t.append( captionSide === 'top' ? caption : null )\n\t\t\t\t\t\t\t\t\t.append(\n\t\t\t\t\t\t\t\t\t\ttable.children('thead')\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t)\n\t\t\t.append(\n\t\t\t\t$(_div, { 'class': classes.sScrollBody } )\n\t\t\t\t\t.css( {\n\t\t\t\t\t\tposition: 'relative',\n\t\t\t\t\t\toverflow: 'auto',\n\t\t\t\t\t\twidth: size( scrollX )\n\t\t\t\t\t} )\n\t\t\t\t\t.append( table )\n\t\t\t);\n\t\n\t\tif ( footer ) {\n\t\t\tscroller.append(\n\t\t\t\t$(_div, { 'class': classes.sScrollFoot } )\n\t\t\t\t\t.css( {\n\t\t\t\t\t\toverflow: 'hidden',\n\t\t\t\t\t\tborder: 0,\n\t\t\t\t\t\twidth: scrollX ? size(scrollX) : '100%'\n\t\t\t\t\t} )\n\t\t\t\t\t.append(\n\t\t\t\t\t\t$(_div, { 'class': classes.sScrollFootInner } )\n\t\t\t\t\t\t\t.append(\n\t\t\t\t\t\t\t\tfooterClone\n\t\t\t\t\t\t\t\t\t.removeAttr('id')\n\t\t\t\t\t\t\t\t\t.css( 'margin-left', 0 )\n\t\t\t\t\t\t\t\t\t.append( captionSide === 'bottom' ? caption : null )\n\t\t\t\t\t\t\t\t\t.append(\n\t\t\t\t\t\t\t\t\t\ttable.children('tfoot')\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t);\n\t\t}\n\t\n\t\tvar children = scroller.children();\n\t\tvar scrollHead = children[0];\n\t\tvar scrollBody = children[1];\n\t\tvar scrollFoot = footer ? children[2] : null;\n\t\n\t\t// When the body is scrolled, then we also want to scroll the headers\n\t\tif ( scrollX ) {\n\t\t\t$(scrollBody).on( 'scroll.DT', function (e) {\n\t\t\t\tvar scrollLeft = this.scrollLeft;\n\t\n\t\t\t\tscrollHead.scrollLeft = scrollLeft;\n\t\n\t\t\t\tif ( footer ) {\n\t\t\t\t\tscrollFoot.scrollLeft = scrollLeft;\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\t\n\t\t$(scrollBody).css('max-height', scrollY);\n\t\tif (! scroll.bCollapse) {\n\t\t\t$(scrollBody).css('height', scrollY);\n\t\t}\n\t\n\t\tsettings.nScrollHead = scrollHead;\n\t\tsettings.nScrollBody = scrollBody;\n\t\tsettings.nScrollFoot = scrollFoot;\n\t\n\t\t// On redraw - align columns\n\t\tsettings.aoDrawCallback.push( {\n\t\t\t\"fn\": _fnScrollDraw,\n\t\t\t\"sName\": \"scrolling\"\n\t\t} );\n\t\n\t\treturn scroller[0];\n\t}\n\t\n\t\n\t\n\t/**\n\t * Update the header, footer and body tables for resizing - i.e. column\n\t * alignment.\n\t *\n\t * Welcome to the most horrible function DataTables. The process that this\n\t * function follows is basically:\n\t *   1. Re-create the table inside the scrolling div\n\t *   2. Take live measurements from the DOM\n\t *   3. Apply the measurements to align the columns\n\t *   4. Clean up\n\t *\n\t *  @param {object} settings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnScrollDraw ( settings )\n\t{\n\t\t// Given that this is such a monster function, a lot of variables are use\n\t\t// to try and keep the minimised size as small as possible\n\t\tvar\n\t\t\tscroll         = settings.oScroll,\n\t\t\tscrollX        = scroll.sX,\n\t\t\tscrollXInner   = scroll.sXInner,\n\t\t\tscrollY        = scroll.sY,\n\t\t\tbarWidth       = scroll.iBarWidth,\n\t\t\tdivHeader      = $(settings.nScrollHead),\n\t\t\tdivHeaderStyle = divHeader[0].style,\n\t\t\tdivHeaderInner = divHeader.children('div'),\n\t\t\tdivHeaderInnerStyle = divHeaderInner[0].style,\n\t\t\tdivHeaderTable = divHeaderInner.children('table'),\n\t\t\tdivBodyEl      = settings.nScrollBody,\n\t\t\tdivBody        = $(divBodyEl),\n\t\t\tdivBodyStyle   = divBodyEl.style,\n\t\t\tdivFooter      = $(settings.nScrollFoot),\n\t\t\tdivFooterInner = divFooter.children('div'),\n\t\t\tdivFooterTable = divFooterInner.children('table'),\n\t\t\theader         = $(settings.nTHead),\n\t\t\ttable          = $(settings.nTable),\n\t\t\ttableEl        = table[0],\n\t\t\ttableStyle     = tableEl.style,\n\t\t\tfooter         = settings.nTFoot ? $(settings.nTFoot) : null,\n\t\t\tbrowser        = settings.oBrowser,\n\t\t\tie67           = browser.bScrollOversize,\n\t\t\tdtHeaderCells  = _pluck( settings.aoColumns, 'nTh' ),\n\t\t\theaderTrgEls, footerTrgEls,\n\t\t\theaderSrcEls, footerSrcEls,\n\t\t\theaderCopy, footerCopy,\n\t\t\theaderWidths=[], footerWidths=[],\n\t\t\theaderContent=[], footerContent=[],\n\t\t\tidx, correction, sanityWidth,\n\t\t\tzeroOut = function(nSizer) {\n\t\t\t\tvar style = nSizer.style;\n\t\t\t\tstyle.paddingTop = \"0\";\n\t\t\t\tstyle.paddingBottom = \"0\";\n\t\t\t\tstyle.borderTopWidth = \"0\";\n\t\t\t\tstyle.borderBottomWidth = \"0\";\n\t\t\t\tstyle.height = 0;\n\t\t\t};\n\t\n\t\t// If the scrollbar visibility has changed from the last draw, we need to\n\t\t// adjust the column sizes as the table width will have changed to account\n\t\t// for the scrollbar\n\t\tvar scrollBarVis = divBodyEl.scrollHeight > divBodyEl.clientHeight;\n\t\t\n\t\tif ( settings.scrollBarVis !== scrollBarVis && settings.scrollBarVis !== undefined ) {\n\t\t\tsettings.scrollBarVis = scrollBarVis;\n\t\t\t_fnAdjustColumnSizing( settings );\n\t\t\treturn; // adjust column sizing will call this function again\n\t\t}\n\t\telse {\n\t\t\tsettings.scrollBarVis = scrollBarVis;\n\t\t}\n\t\n\t\t/*\n\t\t * 1. Re-create the table inside the scrolling div\n\t\t */\n\t\n\t\t// Remove the old minimised thead and tfoot elements in the inner table\n\t\ttable.children('thead, tfoot').remove();\n\t\n\t\tif ( footer ) {\n\t\t\tfooterCopy = footer.clone().prependTo( table );\n\t\t\tfooterTrgEls = footer.find('tr'); // the original tfoot is in its own table and must be sized\n\t\t\tfooterSrcEls = footerCopy.find('tr');\n\t\t}\n\t\n\t\t// Clone the current header and footer elements and then place it into the inner table\n\t\theaderCopy = header.clone().prependTo( table );\n\t\theaderTrgEls = header.find('tr'); // original header is in its own table\n\t\theaderSrcEls = headerCopy.find('tr');\n\t\theaderCopy.find('th, td').removeAttr('tabindex');\n\t\n\t\n\t\t/*\n\t\t * 2. Take live measurements from the DOM - do not alter the DOM itself!\n\t\t */\n\t\n\t\t// Remove old sizing and apply the calculated column widths\n\t\t// Get the unique column headers in the newly created (cloned) header. We want to apply the\n\t\t// calculated sizes to this header\n\t\tif ( ! scrollX )\n\t\t{\n\t\t\tdivBodyStyle.width = '100%';\n\t\t\tdivHeader[0].style.width = '100%';\n\t\t}\n\t\n\t\t$.each( _fnGetUniqueThs( settings, headerCopy ), function ( i, el ) {\n\t\t\tidx = _fnVisibleToColumnIndex( settings, i );\n\t\t\tel.style.width = settings.aoColumns[idx].sWidth;\n\t\t} );\n\t\n\t\tif ( footer ) {\n\t\t\t_fnApplyToChildren( function(n) {\n\t\t\t\tn.style.width = \"\";\n\t\t\t}, footerSrcEls );\n\t\t}\n\t\n\t\t// Size the table as a whole\n\t\tsanityWidth = table.outerWidth();\n\t\tif ( scrollX === \"\" ) {\n\t\t\t// No x scrolling\n\t\t\ttableStyle.width = \"100%\";\n\t\n\t\t\t// IE7 will make the width of the table when 100% include the scrollbar\n\t\t\t// - which is shouldn't. When there is a scrollbar we need to take this\n\t\t\t// into account.\n\t\t\tif ( ie67 && (table.find('tbody').height() > divBodyEl.offsetHeight ||\n\t\t\t\tdivBody.css('overflow-y') == \"scroll\")\n\t\t\t) {\n\t\t\t\ttableStyle.width = _fnStringToCss( table.outerWidth() - barWidth);\n\t\t\t}\n\t\n\t\t\t// Recalculate the sanity width\n\t\t\tsanityWidth = table.outerWidth();\n\t\t}\n\t\telse if ( scrollXInner !== \"\" ) {\n\t\t\t// legacy x scroll inner has been given - use it\n\t\t\ttableStyle.width = _fnStringToCss(scrollXInner);\n\t\n\t\t\t// Recalculate the sanity width\n\t\t\tsanityWidth = table.outerWidth();\n\t\t}\n\t\n\t\t// Hidden header should have zero height, so remove padding and borders. Then\n\t\t// set the width based on the real headers\n\t\n\t\t// Apply all styles in one pass\n\t\t_fnApplyToChildren( zeroOut, headerSrcEls );\n\t\n\t\t// Read all widths in next pass\n\t\t_fnApplyToChildren( function(nSizer) {\n\t\t\theaderContent.push( nSizer.innerHTML );\n\t\t\theaderWidths.push( _fnStringToCss( $(nSizer).css('width') ) );\n\t\t}, headerSrcEls );\n\t\n\t\t// Apply all widths in final pass\n\t\t_fnApplyToChildren( function(nToSize, i) {\n\t\t\t// Only apply widths to the DataTables detected header cells - this\n\t\t\t// prevents complex headers from having contradictory sizes applied\n\t\t\tif ( $.inArray( nToSize, dtHeaderCells ) !== -1 ) {\n\t\t\t\tnToSize.style.width = headerWidths[i];\n\t\t\t}\n\t\t}, headerTrgEls );\n\t\n\t\t$(headerSrcEls).height(0);\n\t\n\t\t/* Same again with the footer if we have one */\n\t\tif ( footer )\n\t\t{\n\t\t\t_fnApplyToChildren( zeroOut, footerSrcEls );\n\t\n\t\t\t_fnApplyToChildren( function(nSizer) {\n\t\t\t\tfooterContent.push( nSizer.innerHTML );\n\t\t\t\tfooterWidths.push( _fnStringToCss( $(nSizer).css('width') ) );\n\t\t\t}, footerSrcEls );\n\t\n\t\t\t_fnApplyToChildren( function(nToSize, i) {\n\t\t\t\tnToSize.style.width = footerWidths[i];\n\t\t\t}, footerTrgEls );\n\t\n\t\t\t$(footerSrcEls).height(0);\n\t\t}\n\t\n\t\n\t\t/*\n\t\t * 3. Apply the measurements\n\t\t */\n\t\n\t\t// \"Hide\" the header and footer that we used for the sizing. We need to keep\n\t\t// the content of the cell so that the width applied to the header and body\n\t\t// both match, but we want to hide it completely. We want to also fix their\n\t\t// width to what they currently are\n\t\t_fnApplyToChildren( function(nSizer, i) {\n\t\t\tnSizer.innerHTML = '<div class=\"dataTables_sizing\">'+headerContent[i]+'</div>';\n\t\t\tnSizer.childNodes[0].style.height = \"0\";\n\t\t\tnSizer.childNodes[0].style.overflow = \"hidden\";\n\t\t\tnSizer.style.width = headerWidths[i];\n\t\t}, headerSrcEls );\n\t\n\t\tif ( footer )\n\t\t{\n\t\t\t_fnApplyToChildren( function(nSizer, i) {\n\t\t\t\tnSizer.innerHTML = '<div class=\"dataTables_sizing\">'+footerContent[i]+'</div>';\n\t\t\t\tnSizer.childNodes[0].style.height = \"0\";\n\t\t\t\tnSizer.childNodes[0].style.overflow = \"hidden\";\n\t\t\t\tnSizer.style.width = footerWidths[i];\n\t\t\t}, footerSrcEls );\n\t\t}\n\t\n\t\t// Sanity check that the table is of a sensible width. If not then we are going to get\n\t\t// misalignment - try to prevent this by not allowing the table to shrink below its min width\n\t\tif ( table.outerWidth() < sanityWidth )\n\t\t{\n\t\t\t// The min width depends upon if we have a vertical scrollbar visible or not */\n\t\t\tcorrection = ((divBodyEl.scrollHeight > divBodyEl.offsetHeight ||\n\t\t\t\tdivBody.css('overflow-y') == \"scroll\")) ?\n\t\t\t\t\tsanityWidth+barWidth :\n\t\t\t\t\tsanityWidth;\n\t\n\t\t\t// IE6/7 are a law unto themselves...\n\t\t\tif ( ie67 && (divBodyEl.scrollHeight >\n\t\t\t\tdivBodyEl.offsetHeight || divBody.css('overflow-y') == \"scroll\")\n\t\t\t) {\n\t\t\t\ttableStyle.width = _fnStringToCss( correction-barWidth );\n\t\t\t}\n\t\n\t\t\t// And give the user a warning that we've stopped the table getting too small\n\t\t\tif ( scrollX === \"\" || scrollXInner !== \"\" ) {\n\t\t\t\t_fnLog( settings, 1, 'Possible column misalignment', 6 );\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\tcorrection = '100%';\n\t\t}\n\t\n\t\t// Apply to the container elements\n\t\tdivBodyStyle.width = _fnStringToCss( correction );\n\t\tdivHeaderStyle.width = _fnStringToCss( correction );\n\t\n\t\tif ( footer ) {\n\t\t\tsettings.nScrollFoot.style.width = _fnStringToCss( correction );\n\t\t}\n\t\n\t\n\t\t/*\n\t\t * 4. Clean up\n\t\t */\n\t\tif ( ! scrollY ) {\n\t\t\t/* IE7< puts a vertical scrollbar in place (when it shouldn't be) due to subtracting\n\t\t\t * the scrollbar height from the visible display, rather than adding it on. We need to\n\t\t\t * set the height in order to sort this. Don't want to do it in any other browsers.\n\t\t\t */\n\t\t\tif ( ie67 ) {\n\t\t\t\tdivBodyStyle.height = _fnStringToCss( tableEl.offsetHeight+barWidth );\n\t\t\t}\n\t\t}\n\t\n\t\t/* Finally set the width's of the header and footer tables */\n\t\tvar iOuterWidth = table.outerWidth();\n\t\tdivHeaderTable[0].style.width = _fnStringToCss( iOuterWidth );\n\t\tdivHeaderInnerStyle.width = _fnStringToCss( iOuterWidth );\n\t\n\t\t// Figure out if there are scrollbar present - if so then we need a the header and footer to\n\t\t// provide a bit more space to allow \"overflow\" scrolling (i.e. past the scrollbar)\n\t\tvar bScrolling = table.height() > divBodyEl.clientHeight || divBody.css('overflow-y') == \"scroll\";\n\t\tvar padding = 'padding' + (browser.bScrollbarLeft ? 'Left' : 'Right' );\n\t\tdivHeaderInnerStyle[ padding ] = bScrolling ? barWidth+\"px\" : \"0px\";\n\t\n\t\tif ( footer ) {\n\t\t\tdivFooterTable[0].style.width = _fnStringToCss( iOuterWidth );\n\t\t\tdivFooterInner[0].style.width = _fnStringToCss( iOuterWidth );\n\t\t\tdivFooterInner[0].style[padding] = bScrolling ? barWidth+\"px\" : \"0px\";\n\t\t}\n\t\n\t\t// Correct DOM ordering for colgroup - comes before the thead\n\t\ttable.children('colgroup').insertBefore( table.children('thead') );\n\t\n\t\t/* Adjust the position of the header in case we loose the y-scrollbar */\n\t\tdivBody.trigger('scroll');\n\t\n\t\t// If sorting or filtering has occurred, jump the scrolling back to the top\n\t\t// only if we aren't holding the position\n\t\tif ( (settings.bSorted || settings.bFiltered) && ! settings._drawHold ) {\n\t\t\tdivBodyEl.scrollTop = 0;\n\t\t}\n\t}\n\t\n\t\n\t\n\t/**\n\t * Apply a given function to the display child nodes of an element array (typically\n\t * TD children of TR rows\n\t *  @param {function} fn Method to apply to the objects\n\t *  @param array {nodes} an1 List of elements to look through for display children\n\t *  @param array {nodes} an2 Another list (identical structure to the first) - optional\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnApplyToChildren( fn, an1, an2 )\n\t{\n\t\tvar index=0, i=0, iLen=an1.length;\n\t\tvar nNode1, nNode2;\n\t\n\t\twhile ( i < iLen ) {\n\t\t\tnNode1 = an1[i].firstChild;\n\t\t\tnNode2 = an2 ? an2[i].firstChild : null;\n\t\n\t\t\twhile ( nNode1 ) {\n\t\t\t\tif ( nNode1.nodeType === 1 ) {\n\t\t\t\t\tif ( an2 ) {\n\t\t\t\t\t\tfn( nNode1, nNode2, index );\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tfn( nNode1, index );\n\t\t\t\t\t}\n\t\n\t\t\t\t\tindex++;\n\t\t\t\t}\n\t\n\t\t\t\tnNode1 = nNode1.nextSibling;\n\t\t\t\tnNode2 = an2 ? nNode2.nextSibling : null;\n\t\t\t}\n\t\n\t\t\ti++;\n\t\t}\n\t}\n\t\n\t\n\t\n\tvar __re_html_remove = /<.*?>/g;\n\t\n\t\n\t/**\n\t * Calculate the width of columns for the table\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnCalculateColumnWidths ( oSettings )\n\t{\n\t\tvar\n\t\t\ttable = oSettings.nTable,\n\t\t\tcolumns = oSettings.aoColumns,\n\t\t\tscroll = oSettings.oScroll,\n\t\t\tscrollY = scroll.sY,\n\t\t\tscrollX = scroll.sX,\n\t\t\tscrollXInner = scroll.sXInner,\n\t\t\tcolumnCount = columns.length,\n\t\t\tvisibleColumns = _fnGetColumns( oSettings, 'bVisible' ),\n\t\t\theaderCells = $('th', oSettings.nTHead),\n\t\t\ttableWidthAttr = table.getAttribute('width'), // from DOM element\n\t\t\ttableContainer = table.parentNode,\n\t\t\tuserInputs = false,\n\t\t\ti, column, columnIdx, width, outerWidth,\n\t\t\tbrowser = oSettings.oBrowser,\n\t\t\tie67 = browser.bScrollOversize;\n\t\n\t\tvar styleWidth = table.style.width;\n\t\tif ( styleWidth && styleWidth.indexOf('%') !== -1 ) {\n\t\t\ttableWidthAttr = styleWidth;\n\t\t}\n\t\n\t\t/* Convert any user input sizes into pixel sizes */\n\t\tfor ( i=0 ; i<visibleColumns.length ; i++ ) {\n\t\t\tcolumn = columns[ visibleColumns[i] ];\n\t\n\t\t\tif ( column.sWidth !== null ) {\n\t\t\t\tcolumn.sWidth = _fnConvertToWidth( column.sWidthOrig, tableContainer );\n\t\n\t\t\t\tuserInputs = true;\n\t\t\t}\n\t\t}\n\t\n\t\t/* If the number of columns in the DOM equals the number that we have to\n\t\t * process in DataTables, then we can use the offsets that are created by\n\t\t * the web- browser. No custom sizes can be set in order for this to happen,\n\t\t * nor scrolling used\n\t\t */\n\t\tif ( ie67 || ! userInputs && ! scrollX && ! scrollY &&\n\t\t     columnCount == _fnVisbleColumns( oSettings ) &&\n\t\t     columnCount == headerCells.length\n\t\t) {\n\t\t\tfor ( i=0 ; i<columnCount ; i++ ) {\n\t\t\t\tvar colIdx = _fnVisibleToColumnIndex( oSettings, i );\n\t\n\t\t\t\tif ( colIdx !== null ) {\n\t\t\t\t\tcolumns[ colIdx ].sWidth = _fnStringToCss( headerCells.eq(i).width() );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// Otherwise construct a single row, worst case, table with the widest\n\t\t\t// node in the data, assign any user defined widths, then insert it into\n\t\t\t// the DOM and allow the browser to do all the hard work of calculating\n\t\t\t// table widths\n\t\t\tvar tmpTable = $(table).clone() // don't use cloneNode - IE8 will remove events on the main table\n\t\t\t\t.css( 'visibility', 'hidden' )\n\t\t\t\t.removeAttr( 'id' );\n\t\n\t\t\t// Clean up the table body\n\t\t\ttmpTable.find('tbody tr').remove();\n\t\t\tvar tr = $('<tr/>').appendTo( tmpTable.find('tbody') );\n\t\n\t\t\t// Clone the table header and footer - we can't use the header / footer\n\t\t\t// from the cloned table, since if scrolling is active, the table's\n\t\t\t// real header and footer are contained in different table tags\n\t\t\ttmpTable.find('thead, tfoot').remove();\n\t\t\ttmpTable\n\t\t\t\t.append( $(oSettings.nTHead).clone() )\n\t\t\t\t.append( $(oSettings.nTFoot).clone() );\n\t\n\t\t\t// Remove any assigned widths from the footer (from scrolling)\n\t\t\ttmpTable.find('tfoot th, tfoot td').css('width', '');\n\t\n\t\t\t// Apply custom sizing to the cloned header\n\t\t\theaderCells = _fnGetUniqueThs( oSettings, tmpTable.find('thead')[0] );\n\t\n\t\t\tfor ( i=0 ; i<visibleColumns.length ; i++ ) {\n\t\t\t\tcolumn = columns[ visibleColumns[i] ];\n\t\n\t\t\t\theaderCells[i].style.width = column.sWidthOrig !== null && column.sWidthOrig !== '' ?\n\t\t\t\t\t_fnStringToCss( column.sWidthOrig ) :\n\t\t\t\t\t'';\n\t\n\t\t\t\t// For scrollX we need to force the column width otherwise the\n\t\t\t\t// browser will collapse it. If this width is smaller than the\n\t\t\t\t// width the column requires, then it will have no effect\n\t\t\t\tif ( column.sWidthOrig && scrollX ) {\n\t\t\t\t\t$( headerCells[i] ).append( $('<div/>').css( {\n\t\t\t\t\t\twidth: column.sWidthOrig,\n\t\t\t\t\t\tmargin: 0,\n\t\t\t\t\t\tpadding: 0,\n\t\t\t\t\t\tborder: 0,\n\t\t\t\t\t\theight: 1\n\t\t\t\t\t} ) );\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t// Find the widest cell for each column and put it into the table\n\t\t\tif ( oSettings.aoData.length ) {\n\t\t\t\tfor ( i=0 ; i<visibleColumns.length ; i++ ) {\n\t\t\t\t\tcolumnIdx = visibleColumns[i];\n\t\t\t\t\tcolumn = columns[ columnIdx ];\n\t\n\t\t\t\t\t$( _fnGetWidestNode( oSettings, columnIdx ) )\n\t\t\t\t\t\t.clone( false )\n\t\t\t\t\t\t.append( column.sContentPadding )\n\t\t\t\t\t\t.appendTo( tr );\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t// Tidy the temporary table - remove name attributes so there aren't\n\t\t\t// duplicated in the dom (radio elements for example)\n\t\t\t$('[name]', tmpTable).removeAttr('name');\n\t\n\t\t\t// Table has been built, attach to the document so we can work with it.\n\t\t\t// A holding element is used, positioned at the top of the container\n\t\t\t// with minimal height, so it has no effect on if the container scrolls\n\t\t\t// or not. Otherwise it might trigger scrolling when it actually isn't\n\t\t\t// needed\n\t\t\tvar holder = $('<div/>').css( scrollX || scrollY ?\n\t\t\t\t\t{\n\t\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\t\ttop: 0,\n\t\t\t\t\t\tleft: 0,\n\t\t\t\t\t\theight: 1,\n\t\t\t\t\t\tright: 0,\n\t\t\t\t\t\toverflow: 'hidden'\n\t\t\t\t\t} :\n\t\t\t\t\t{}\n\t\t\t\t)\n\t\t\t\t.append( tmpTable )\n\t\t\t\t.appendTo( tableContainer );\n\t\n\t\t\t// When scrolling (X or Y) we want to set the width of the table as \n\t\t\t// appropriate. However, when not scrolling leave the table width as it\n\t\t\t// is. This results in slightly different, but I think correct behaviour\n\t\t\tif ( scrollX && scrollXInner ) {\n\t\t\t\ttmpTable.width( scrollXInner );\n\t\t\t}\n\t\t\telse if ( scrollX ) {\n\t\t\t\ttmpTable.css( 'width', 'auto' );\n\t\t\t\ttmpTable.removeAttr('width');\n\t\n\t\t\t\t// If there is no width attribute or style, then allow the table to\n\t\t\t\t// collapse\n\t\t\t\tif ( tmpTable.width() < tableContainer.clientWidth && tableWidthAttr ) {\n\t\t\t\t\ttmpTable.width( tableContainer.clientWidth );\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if ( scrollY ) {\n\t\t\t\ttmpTable.width( tableContainer.clientWidth );\n\t\t\t}\n\t\t\telse if ( tableWidthAttr ) {\n\t\t\t\ttmpTable.width( tableWidthAttr );\n\t\t\t}\n\t\n\t\t\t// Get the width of each column in the constructed table - we need to\n\t\t\t// know the inner width (so it can be assigned to the other table's\n\t\t\t// cells) and the outer width so we can calculate the full width of the\n\t\t\t// table. This is safe since DataTables requires a unique cell for each\n\t\t\t// column, but if ever a header can span multiple columns, this will\n\t\t\t// need to be modified.\n\t\t\tvar total = 0;\n\t\t\tfor ( i=0 ; i<visibleColumns.length ; i++ ) {\n\t\t\t\tvar cell = $(headerCells[i]);\n\t\t\t\tvar border = cell.outerWidth() - cell.width();\n\t\n\t\t\t\t// Use getBounding... where possible (not IE8-) because it can give\n\t\t\t\t// sub-pixel accuracy, which we then want to round up!\n\t\t\t\tvar bounding = browser.bBounding ?\n\t\t\t\t\tMath.ceil( headerCells[i].getBoundingClientRect().width ) :\n\t\t\t\t\tcell.outerWidth();\n\t\n\t\t\t\t// Total is tracked to remove any sub-pixel errors as the outerWidth\n\t\t\t\t// of the table might not equal the total given here (IE!).\n\t\t\t\ttotal += bounding;\n\t\n\t\t\t\t// Width for each column to use\n\t\t\t\tcolumns[ visibleColumns[i] ].sWidth = _fnStringToCss( bounding - border );\n\t\t\t}\n\t\n\t\t\ttable.style.width = _fnStringToCss( total );\n\t\n\t\t\t// Finished with the table - ditch it\n\t\t\tholder.remove();\n\t\t}\n\t\n\t\t// If there is a width attr, we want to attach an event listener which\n\t\t// allows the table sizing to automatically adjust when the window is\n\t\t// resized. Use the width attr rather than CSS, since we can't know if the\n\t\t// CSS is a relative value or absolute - DOM read is always px.\n\t\tif ( tableWidthAttr ) {\n\t\t\ttable.style.width = _fnStringToCss( tableWidthAttr );\n\t\t}\n\t\n\t\tif ( (tableWidthAttr || scrollX) && ! oSettings._reszEvt ) {\n\t\t\tvar bindResize = function () {\n\t\t\t\t$(window).on('resize.DT-'+oSettings.sInstance, _fnThrottle( function () {\n\t\t\t\t\t_fnAdjustColumnSizing( oSettings );\n\t\t\t\t} ) );\n\t\t\t};\n\t\n\t\t\t// IE6/7 will crash if we bind a resize event handler on page load.\n\t\t\t// To be removed in 1.11 which drops IE6/7 support\n\t\t\tif ( ie67 ) {\n\t\t\t\tsetTimeout( bindResize, 1000 );\n\t\t\t}\n\t\t\telse {\n\t\t\t\tbindResize();\n\t\t\t}\n\t\n\t\t\toSettings._reszEvt = true;\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Throttle the calls to a function. Arguments and context are maintained for\n\t * the throttled function\n\t *  @param {function} fn Function to be called\n\t *  @param {int} [freq=200] call frequency in mS\n\t *  @returns {function} wrapped function\n\t *  @memberof DataTable#oApi\n\t */\n\tvar _fnThrottle = DataTable.util.throttle;\n\t\n\t\n\t/**\n\t * Convert a CSS unit width to pixels (e.g. 2em)\n\t *  @param {string} width width to be converted\n\t *  @param {node} parent parent to get the with for (required for relative widths) - optional\n\t *  @returns {int} width in pixels\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnConvertToWidth ( width, parent )\n\t{\n\t\tif ( ! width ) {\n\t\t\treturn 0;\n\t\t}\n\t\n\t\tvar n = $('<div/>')\n\t\t\t.css( 'width', _fnStringToCss( width ) )\n\t\t\t.appendTo( parent || document.body );\n\t\n\t\tvar val = n[0].offsetWidth;\n\t\tn.remove();\n\t\n\t\treturn val;\n\t}\n\t\n\t\n\t/**\n\t * Get the widest node\n\t *  @param {object} settings dataTables settings object\n\t *  @param {int} colIdx column of interest\n\t *  @returns {node} widest table node\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnGetWidestNode( settings, colIdx )\n\t{\n\t\tvar idx = _fnGetMaxLenString( settings, colIdx );\n\t\tif ( idx < 0 ) {\n\t\t\treturn null;\n\t\t}\n\t\n\t\tvar data = settings.aoData[ idx ];\n\t\treturn ! data.nTr ? // Might not have been created when deferred rendering\n\t\t\t$('<td/>').html( _fnGetCellData( settings, idx, colIdx, 'display' ) )[0] :\n\t\t\tdata.anCells[ colIdx ];\n\t}\n\t\n\t\n\t/**\n\t * Get the maximum strlen for each data column\n\t *  @param {object} settings dataTables settings object\n\t *  @param {int} colIdx column of interest\n\t *  @returns {string} max string length for each column\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnGetMaxLenString( settings, colIdx )\n\t{\n\t\tvar s, max=-1, maxIdx = -1;\n\t\n\t\tfor ( var i=0, ien=settings.aoData.length ; i<ien ; i++ ) {\n\t\t\ts = _fnGetCellData( settings, i, colIdx, 'display' )+'';\n\t\t\ts = s.replace( __re_html_remove, '' );\n\t\t\ts = s.replace( /&nbsp;/g, ' ' );\n\t\n\t\t\tif ( s.length > max ) {\n\t\t\t\tmax = s.length;\n\t\t\t\tmaxIdx = i;\n\t\t\t}\n\t\t}\n\t\n\t\treturn maxIdx;\n\t}\n\t\n\t\n\t/**\n\t * Append a CSS unit (only if required) to a string\n\t *  @param {string} value to css-ify\n\t *  @returns {string} value with css unit\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnStringToCss( s )\n\t{\n\t\tif ( s === null ) {\n\t\t\treturn '0px';\n\t\t}\n\t\n\t\tif ( typeof s == 'number' ) {\n\t\t\treturn s < 0 ?\n\t\t\t\t'0px' :\n\t\t\t\ts+'px';\n\t\t}\n\t\n\t\t// Check it has a unit character already\n\t\treturn s.match(/\\d$/) ?\n\t\t\ts+'px' :\n\t\t\ts;\n\t}\n\t\n\t\n\t\n\tfunction _fnSortFlatten ( settings )\n\t{\n\t\tvar\n\t\t\ti, iLen, k, kLen,\n\t\t\taSort = [],\n\t\t\taiOrig = [],\n\t\t\taoColumns = settings.aoColumns,\n\t\t\taDataSort, iCol, sType, srcCol,\n\t\t\tfixed = settings.aaSortingFixed,\n\t\t\tfixedObj = $.isPlainObject( fixed ),\n\t\t\tnestedSort = [],\n\t\t\tadd = function ( a ) {\n\t\t\t\tif ( a.length && ! $.isArray( a[0] ) ) {\n\t\t\t\t\t// 1D array\n\t\t\t\t\tnestedSort.push( a );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t// 2D array\n\t\t\t\t\t$.merge( nestedSort, a );\n\t\t\t\t}\n\t\t\t};\n\t\n\t\t// Build the sort array, with pre-fix and post-fix options if they have been\n\t\t// specified\n\t\tif ( $.isArray( fixed ) ) {\n\t\t\tadd( fixed );\n\t\t}\n\t\n\t\tif ( fixedObj && fixed.pre ) {\n\t\t\tadd( fixed.pre );\n\t\t}\n\t\n\t\tadd( settings.aaSorting );\n\t\n\t\tif (fixedObj && fixed.post ) {\n\t\t\tadd( fixed.post );\n\t\t}\n\t\n\t\tfor ( i=0 ; i<nestedSort.length ; i++ )\n\t\t{\n\t\t\tsrcCol = nestedSort[i][0];\n\t\t\taDataSort = aoColumns[ srcCol ].aDataSort;\n\t\n\t\t\tfor ( k=0, kLen=aDataSort.length ; k<kLen ; k++ )\n\t\t\t{\n\t\t\t\tiCol = aDataSort[k];\n\t\t\t\tsType = aoColumns[ iCol ].sType || 'string';\n\t\n\t\t\t\tif ( nestedSort[i]._idx === undefined ) {\n\t\t\t\t\tnestedSort[i]._idx = $.inArray( nestedSort[i][1], aoColumns[iCol].asSorting );\n\t\t\t\t}\n\t\n\t\t\t\taSort.push( {\n\t\t\t\t\tsrc:       srcCol,\n\t\t\t\t\tcol:       iCol,\n\t\t\t\t\tdir:       nestedSort[i][1],\n\t\t\t\t\tindex:     nestedSort[i]._idx,\n\t\t\t\t\ttype:      sType,\n\t\t\t\t\tformatter: DataTable.ext.type.order[ sType+\"-pre\" ]\n\t\t\t\t} );\n\t\t\t}\n\t\t}\n\t\n\t\treturn aSort;\n\t}\n\t\n\t/**\n\t * Change the order of the table\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t *  @todo This really needs split up!\n\t */\n\tfunction _fnSort ( oSettings )\n\t{\n\t\tvar\n\t\t\ti, ien, iLen, j, jLen, k, kLen,\n\t\t\tsDataType, nTh,\n\t\t\taiOrig = [],\n\t\t\toExtSort = DataTable.ext.type.order,\n\t\t\taoData = oSettings.aoData,\n\t\t\taoColumns = oSettings.aoColumns,\n\t\t\taDataSort, data, iCol, sType, oSort,\n\t\t\tformatters = 0,\n\t\t\tsortCol,\n\t\t\tdisplayMaster = oSettings.aiDisplayMaster,\n\t\t\taSort;\n\t\n\t\t// Resolve any column types that are unknown due to addition or invalidation\n\t\t// @todo Can this be moved into a 'data-ready' handler which is called when\n\t\t//   data is going to be used in the table?\n\t\t_fnColumnTypes( oSettings );\n\t\n\t\taSort = _fnSortFlatten( oSettings );\n\t\n\t\tfor ( i=0, ien=aSort.length ; i<ien ; i++ ) {\n\t\t\tsortCol = aSort[i];\n\t\n\t\t\t// Track if we can use the fast sort algorithm\n\t\t\tif ( sortCol.formatter ) {\n\t\t\t\tformatters++;\n\t\t\t}\n\t\n\t\t\t// Load the data needed for the sort, for each cell\n\t\t\t_fnSortData( oSettings, sortCol.col );\n\t\t}\n\t\n\t\t/* No sorting required if server-side or no sorting array */\n\t\tif ( _fnDataSource( oSettings ) != 'ssp' && aSort.length !== 0 )\n\t\t{\n\t\t\t// Create a value - key array of the current row positions such that we can use their\n\t\t\t// current position during the sort, if values match, in order to perform stable sorting\n\t\t\tfor ( i=0, iLen=displayMaster.length ; i<iLen ; i++ ) {\n\t\t\t\taiOrig[ displayMaster[i] ] = i;\n\t\t\t}\n\t\n\t\t\t/* Do the sort - here we want multi-column sorting based on a given data source (column)\n\t\t\t * and sorting function (from oSort) in a certain direction. It's reasonably complex to\n\t\t\t * follow on it's own, but this is what we want (example two column sorting):\n\t\t\t *  fnLocalSorting = function(a,b){\n\t\t\t *    var iTest;\n\t\t\t *    iTest = oSort['string-asc']('data11', 'data12');\n\t\t\t *      if (iTest !== 0)\n\t\t\t *        return iTest;\n\t\t\t *    iTest = oSort['numeric-desc']('data21', 'data22');\n\t\t\t *    if (iTest !== 0)\n\t\t\t *      return iTest;\n\t\t\t *    return oSort['numeric-asc']( aiOrig[a], aiOrig[b] );\n\t\t\t *  }\n\t\t\t * Basically we have a test for each sorting column, if the data in that column is equal,\n\t\t\t * test the next column. If all columns match, then we use a numeric sort on the row\n\t\t\t * positions in the original data array to provide a stable sort.\n\t\t\t *\n\t\t\t * Note - I know it seems excessive to have two sorting methods, but the first is around\n\t\t\t * 15% faster, so the second is only maintained for backwards compatibility with sorting\n\t\t\t * methods which do not have a pre-sort formatting function.\n\t\t\t */\n\t\t\tif ( formatters === aSort.length ) {\n\t\t\t\t// All sort types have formatting functions\n\t\t\t\tdisplayMaster.sort( function ( a, b ) {\n\t\t\t\t\tvar\n\t\t\t\t\t\tx, y, k, test, sort,\n\t\t\t\t\t\tlen=aSort.length,\n\t\t\t\t\t\tdataA = aoData[a]._aSortData,\n\t\t\t\t\t\tdataB = aoData[b]._aSortData;\n\t\n\t\t\t\t\tfor ( k=0 ; k<len ; k++ ) {\n\t\t\t\t\t\tsort = aSort[k];\n\t\n\t\t\t\t\t\tx = dataA[ sort.col ];\n\t\t\t\t\t\ty = dataB[ sort.col ];\n\t\n\t\t\t\t\t\ttest = x<y ? -1 : x>y ? 1 : 0;\n\t\t\t\t\t\tif ( test !== 0 ) {\n\t\t\t\t\t\t\treturn sort.dir === 'asc' ? test : -test;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\n\t\t\t\t\tx = aiOrig[a];\n\t\t\t\t\ty = aiOrig[b];\n\t\t\t\t\treturn x<y ? -1 : x>y ? 1 : 0;\n\t\t\t\t} );\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// Depreciated - remove in 1.11 (providing a plug-in option)\n\t\t\t\t// Not all sort types have formatting methods, so we have to call their sorting\n\t\t\t\t// methods.\n\t\t\t\tdisplayMaster.sort( function ( a, b ) {\n\t\t\t\t\tvar\n\t\t\t\t\t\tx, y, k, l, test, sort, fn,\n\t\t\t\t\t\tlen=aSort.length,\n\t\t\t\t\t\tdataA = aoData[a]._aSortData,\n\t\t\t\t\t\tdataB = aoData[b]._aSortData;\n\t\n\t\t\t\t\tfor ( k=0 ; k<len ; k++ ) {\n\t\t\t\t\t\tsort = aSort[k];\n\t\n\t\t\t\t\t\tx = dataA[ sort.col ];\n\t\t\t\t\t\ty = dataB[ sort.col ];\n\t\n\t\t\t\t\t\tfn = oExtSort[ sort.type+\"-\"+sort.dir ] || oExtSort[ \"string-\"+sort.dir ];\n\t\t\t\t\t\ttest = fn( x, y );\n\t\t\t\t\t\tif ( test !== 0 ) {\n\t\t\t\t\t\t\treturn test;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\n\t\t\t\t\tx = aiOrig[a];\n\t\t\t\t\ty = aiOrig[b];\n\t\t\t\t\treturn x<y ? -1 : x>y ? 1 : 0;\n\t\t\t\t} );\n\t\t\t}\n\t\t}\n\t\n\t\t/* Tell the draw function that we have sorted the data */\n\t\toSettings.bSorted = true;\n\t}\n\t\n\t\n\tfunction _fnSortAria ( settings )\n\t{\n\t\tvar label;\n\t\tvar nextSort;\n\t\tvar columns = settings.aoColumns;\n\t\tvar aSort = _fnSortFlatten( settings );\n\t\tvar oAria = settings.oLanguage.oAria;\n\t\n\t\t// ARIA attributes - need to loop all columns, to update all (removing old\n\t\t// attributes as needed)\n\t\tfor ( var i=0, iLen=columns.length ; i<iLen ; i++ )\n\t\t{\n\t\t\tvar col = columns[i];\n\t\t\tvar asSorting = col.asSorting;\n\t\t\tvar sTitle = col.sTitle.replace( /<.*?>/g, \"\" );\n\t\t\tvar th = col.nTh;\n\t\n\t\t\t// IE7 is throwing an error when setting these properties with jQuery's\n\t\t\t// attr() and removeAttr() methods...\n\t\t\tth.removeAttribute('aria-sort');\n\t\n\t\t\t/* In ARIA only the first sorting column can be marked as sorting - no multi-sort option */\n\t\t\tif ( col.bSortable ) {\n\t\t\t\tif ( aSort.length > 0 && aSort[0].col == i ) {\n\t\t\t\t\tth.setAttribute('aria-sort', aSort[0].dir==\"asc\" ? \"ascending\" : \"descending\" );\n\t\t\t\t\tnextSort = asSorting[ aSort[0].index+1 ] || asSorting[0];\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tnextSort = asSorting[0];\n\t\t\t\t}\n\t\n\t\t\t\tlabel = sTitle + ( nextSort === \"asc\" ?\n\t\t\t\t\toAria.sSortAscending :\n\t\t\t\t\toAria.sSortDescending\n\t\t\t\t);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlabel = sTitle;\n\t\t\t}\n\t\n\t\t\tth.setAttribute('aria-label', label);\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Function to run on user sort request\n\t *  @param {object} settings dataTables settings object\n\t *  @param {node} attachTo node to attach the handler to\n\t *  @param {int} colIdx column sorting index\n\t *  @param {boolean} [append=false] Append the requested sort to the existing\n\t *    sort if true (i.e. multi-column sort)\n\t *  @param {function} [callback] callback function\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnSortListener ( settings, colIdx, append, callback )\n\t{\n\t\tvar col = settings.aoColumns[ colIdx ];\n\t\tvar sorting = settings.aaSorting;\n\t\tvar asSorting = col.asSorting;\n\t\tvar nextSortIdx;\n\t\tvar next = function ( a, overflow ) {\n\t\t\tvar idx = a._idx;\n\t\t\tif ( idx === undefined ) {\n\t\t\t\tidx = $.inArray( a[1], asSorting );\n\t\t\t}\n\t\n\t\t\treturn idx+1 < asSorting.length ?\n\t\t\t\tidx+1 :\n\t\t\t\toverflow ?\n\t\t\t\t\tnull :\n\t\t\t\t\t0;\n\t\t};\n\t\n\t\t// Convert to 2D array if needed\n\t\tif ( typeof sorting[0] === 'number' ) {\n\t\t\tsorting = settings.aaSorting = [ sorting ];\n\t\t}\n\t\n\t\t// If appending the sort then we are multi-column sorting\n\t\tif ( append && settings.oFeatures.bSortMulti ) {\n\t\t\t// Are we already doing some kind of sort on this column?\n\t\t\tvar sortIdx = $.inArray( colIdx, _pluck(sorting, '0') );\n\t\n\t\t\tif ( sortIdx !== -1 ) {\n\t\t\t\t// Yes, modify the sort\n\t\t\t\tnextSortIdx = next( sorting[sortIdx], true );\n\t\n\t\t\t\tif ( nextSortIdx === null && sorting.length === 1 ) {\n\t\t\t\t\tnextSortIdx = 0; // can't remove sorting completely\n\t\t\t\t}\n\t\n\t\t\t\tif ( nextSortIdx === null ) {\n\t\t\t\t\tsorting.splice( sortIdx, 1 );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tsorting[sortIdx][1] = asSorting[ nextSortIdx ];\n\t\t\t\t\tsorting[sortIdx]._idx = nextSortIdx;\n\t\t\t\t}\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// No sort on this column yet\n\t\t\t\tsorting.push( [ colIdx, asSorting[0], 0 ] );\n\t\t\t\tsorting[sorting.length-1]._idx = 0;\n\t\t\t}\n\t\t}\n\t\telse if ( sorting.length && sorting[0][0] == colIdx ) {\n\t\t\t// Single column - already sorting on this column, modify the sort\n\t\t\tnextSortIdx = next( sorting[0] );\n\t\n\t\t\tsorting.length = 1;\n\t\t\tsorting[0][1] = asSorting[ nextSortIdx ];\n\t\t\tsorting[0]._idx = nextSortIdx;\n\t\t}\n\t\telse {\n\t\t\t// Single column - sort only on this column\n\t\t\tsorting.length = 0;\n\t\t\tsorting.push( [ colIdx, asSorting[0] ] );\n\t\t\tsorting[0]._idx = 0;\n\t\t}\n\t\n\t\t// Run the sort by calling a full redraw\n\t\t_fnReDraw( settings );\n\t\n\t\t// callback used for async user interaction\n\t\tif ( typeof callback == 'function' ) {\n\t\t\tcallback( settings );\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Attach a sort handler (click) to a node\n\t *  @param {object} settings dataTables settings object\n\t *  @param {node} attachTo node to attach the handler to\n\t *  @param {int} colIdx column sorting index\n\t *  @param {function} [callback] callback function\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnSortAttachListener ( settings, attachTo, colIdx, callback )\n\t{\n\t\tvar col = settings.aoColumns[ colIdx ];\n\t\n\t\t_fnBindAction( attachTo, {}, function (e) {\n\t\t\t/* If the column is not sortable - don't to anything */\n\t\t\tif ( col.bSortable === false ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\n\t\t\t// If processing is enabled use a timeout to allow the processing\n\t\t\t// display to be shown - otherwise to it synchronously\n\t\t\tif ( settings.oFeatures.bProcessing ) {\n\t\t\t\t_fnProcessingDisplay( settings, true );\n\t\n\t\t\t\tsetTimeout( function() {\n\t\t\t\t\t_fnSortListener( settings, colIdx, e.shiftKey, callback );\n\t\n\t\t\t\t\t// In server-side processing, the draw callback will remove the\n\t\t\t\t\t// processing display\n\t\t\t\t\tif ( _fnDataSource( settings ) !== 'ssp' ) {\n\t\t\t\t\t\t_fnProcessingDisplay( settings, false );\n\t\t\t\t\t}\n\t\t\t\t}, 0 );\n\t\t\t}\n\t\t\telse {\n\t\t\t\t_fnSortListener( settings, colIdx, e.shiftKey, callback );\n\t\t\t}\n\t\t} );\n\t}\n\t\n\t\n\t/**\n\t * Set the sorting classes on table's body, Note: it is safe to call this function\n\t * when bSort and bSortClasses are false\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnSortingClasses( settings )\n\t{\n\t\tvar oldSort = settings.aLastSort;\n\t\tvar sortClass = settings.oClasses.sSortColumn;\n\t\tvar sort = _fnSortFlatten( settings );\n\t\tvar features = settings.oFeatures;\n\t\tvar i, ien, colIdx;\n\t\n\t\tif ( features.bSort && features.bSortClasses ) {\n\t\t\t// Remove old sorting classes\n\t\t\tfor ( i=0, ien=oldSort.length ; i<ien ; i++ ) {\n\t\t\t\tcolIdx = oldSort[i].src;\n\t\n\t\t\t\t// Remove column sorting\n\t\t\t\t$( _pluck( settings.aoData, 'anCells', colIdx ) )\n\t\t\t\t\t.removeClass( sortClass + (i<2 ? i+1 : 3) );\n\t\t\t}\n\t\n\t\t\t// Add new column sorting\n\t\t\tfor ( i=0, ien=sort.length ; i<ien ; i++ ) {\n\t\t\t\tcolIdx = sort[i].src;\n\t\n\t\t\t\t$( _pluck( settings.aoData, 'anCells', colIdx ) )\n\t\t\t\t\t.addClass( sortClass + (i<2 ? i+1 : 3) );\n\t\t\t}\n\t\t}\n\t\n\t\tsettings.aLastSort = sort;\n\t}\n\t\n\t\n\t// Get the data to sort a column, be it from cache, fresh (populating the\n\t// cache), or from a sort formatter\n\tfunction _fnSortData( settings, idx )\n\t{\n\t\t// Custom sorting function - provided by the sort data type\n\t\tvar column = settings.aoColumns[ idx ];\n\t\tvar customSort = DataTable.ext.order[ column.sSortDataType ];\n\t\tvar customData;\n\t\n\t\tif ( customSort ) {\n\t\t\tcustomData = customSort.call( settings.oInstance, settings, idx,\n\t\t\t\t_fnColumnIndexToVisible( settings, idx )\n\t\t\t);\n\t\t}\n\t\n\t\t// Use / populate cache\n\t\tvar row, cellData;\n\t\tvar formatter = DataTable.ext.type.order[ column.sType+\"-pre\" ];\n\t\n\t\tfor ( var i=0, ien=settings.aoData.length ; i<ien ; i++ ) {\n\t\t\trow = settings.aoData[i];\n\t\n\t\t\tif ( ! row._aSortData ) {\n\t\t\t\trow._aSortData = [];\n\t\t\t}\n\t\n\t\t\tif ( ! row._aSortData[idx] || customSort ) {\n\t\t\t\tcellData = customSort ?\n\t\t\t\t\tcustomData[i] : // If there was a custom sort function, use data from there\n\t\t\t\t\t_fnGetCellData( settings, i, idx, 'sort' );\n\t\n\t\t\t\trow._aSortData[ idx ] = formatter ?\n\t\t\t\t\tformatter( cellData ) :\n\t\t\t\t\tcellData;\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\t\n\t/**\n\t * Save the state of a table\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnSaveState ( settings )\n\t{\n\t\tif ( !settings.oFeatures.bStateSave || settings.bDestroying )\n\t\t{\n\t\t\treturn;\n\t\t}\n\t\n\t\t/* Store the interesting variables */\n\t\tvar state = {\n\t\t\ttime:    +new Date(),\n\t\t\tstart:   settings._iDisplayStart,\n\t\t\tlength:  settings._iDisplayLength,\n\t\t\torder:   $.extend( true, [], settings.aaSorting ),\n\t\t\tsearch:  _fnSearchToCamel( settings.oPreviousSearch ),\n\t\t\tcolumns: $.map( settings.aoColumns, function ( col, i ) {\n\t\t\t\treturn {\n\t\t\t\t\tvisible: col.bVisible,\n\t\t\t\t\tsearch: _fnSearchToCamel( settings.aoPreSearchCols[i] )\n\t\t\t\t};\n\t\t\t} )\n\t\t};\n\t\n\t\t_fnCallbackFire( settings, \"aoStateSaveParams\", 'stateSaveParams', [settings, state] );\n\t\n\t\tsettings.oSavedState = state;\n\t\tsettings.fnStateSaveCallback.call( settings.oInstance, settings, state );\n\t}\n\t\n\t\n\t/**\n\t * Attempt to load a saved table state\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {object} oInit DataTables init object so we can override settings\n\t *  @param {function} callback Callback to execute when the state has been loaded\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnLoadState ( settings, oInit, callback )\n\t{\n\t\tvar i, ien;\n\t\tvar columns = settings.aoColumns;\n\t\tvar loaded = function ( s ) {\n\t\t\tif ( ! s || ! s.time ) {\n\t\t\t\tcallback();\n\t\t\t\treturn;\n\t\t\t}\n\t\n\t\t\t// Allow custom and plug-in manipulation functions to alter the saved data set and\n\t\t\t// cancelling of loading by returning false\n\t\t\tvar abStateLoad = _fnCallbackFire( settings, 'aoStateLoadParams', 'stateLoadParams', [settings, s] );\n\t\t\tif ( $.inArray( false, abStateLoad ) !== -1 ) {\n\t\t\t\tcallback();\n\t\t\t\treturn;\n\t\t\t}\n\t\n\t\t\t// Reject old data\n\t\t\tvar duration = settings.iStateDuration;\n\t\t\tif ( duration > 0 && s.time < +new Date() - (duration*1000) ) {\n\t\t\t\tcallback();\n\t\t\t\treturn;\n\t\t\t}\n\t\n\t\t\t// Number of columns have changed - all bets are off, no restore of settings\n\t\t\tif ( s.columns && columns.length !== s.columns.length ) {\n\t\t\t\tcallback();\n\t\t\t\treturn;\n\t\t\t}\n\t\n\t\t\t// Store the saved state so it might be accessed at any time\n\t\t\tsettings.oLoadedState = $.extend( true, {}, s );\n\t\n\t\t\t// Restore key features - todo - for 1.11 this needs to be done by\n\t\t\t// subscribed events\n\t\t\tif ( s.start !== undefined ) {\n\t\t\t\tsettings._iDisplayStart    = s.start;\n\t\t\t\tsettings.iInitDisplayStart = s.start;\n\t\t\t}\n\t\t\tif ( s.length !== undefined ) {\n\t\t\t\tsettings._iDisplayLength   = s.length;\n\t\t\t}\n\t\n\t\t\t// Order\n\t\t\tif ( s.order !== undefined ) {\n\t\t\t\tsettings.aaSorting = [];\n\t\t\t\t$.each( s.order, function ( i, col ) {\n\t\t\t\t\tsettings.aaSorting.push( col[0] >= columns.length ?\n\t\t\t\t\t\t[ 0, col[1] ] :\n\t\t\t\t\t\tcol\n\t\t\t\t\t);\n\t\t\t\t} );\n\t\t\t}\n\t\n\t\t\t// Search\n\t\t\tif ( s.search !== undefined ) {\n\t\t\t\t$.extend( settings.oPreviousSearch, _fnSearchToHung( s.search ) );\n\t\t\t}\n\t\n\t\t\t// Columns\n\t\t\t//\n\t\t\tif ( s.columns ) {\n\t\t\t\tfor ( i=0, ien=s.columns.length ; i<ien ; i++ ) {\n\t\t\t\t\tvar col = s.columns[i];\n\t\n\t\t\t\t\t// Visibility\n\t\t\t\t\tif ( col.visible !== undefined ) {\n\t\t\t\t\t\tcolumns[i].bVisible = col.visible;\n\t\t\t\t\t}\n\t\n\t\t\t\t\t// Search\n\t\t\t\t\tif ( col.search !== undefined ) {\n\t\t\t\t\t\t$.extend( settings.aoPreSearchCols[i], _fnSearchToHung( col.search ) );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t_fnCallbackFire( settings, 'aoStateLoaded', 'stateLoaded', [settings, s] );\n\t\t\tcallback();\n\t\t};\n\t\n\t\tif ( ! settings.oFeatures.bStateSave ) {\n\t\t\tcallback();\n\t\t\treturn;\n\t\t}\n\t\n\t\tvar state = settings.fnStateLoadCallback.call( settings.oInstance, settings, loaded );\n\t\n\t\tif ( state !== undefined ) {\n\t\t\tloaded( state );\n\t\t}\n\t\t// otherwise, wait for the loaded callback to be executed\n\t}\n\t\n\t\n\t/**\n\t * Return the settings object for a particular table\n\t *  @param {node} table table we are using as a dataTable\n\t *  @returns {object} Settings object - or null if not found\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnSettingsFromNode ( table )\n\t{\n\t\tvar settings = DataTable.settings;\n\t\tvar idx = $.inArray( table, _pluck( settings, 'nTable' ) );\n\t\n\t\treturn idx !== -1 ?\n\t\t\tsettings[ idx ] :\n\t\t\tnull;\n\t}\n\t\n\t\n\t/**\n\t * Log an error message\n\t *  @param {object} settings dataTables settings object\n\t *  @param {int} level log error messages, or display them to the user\n\t *  @param {string} msg error message\n\t *  @param {int} tn Technical note id to get more information about the error.\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnLog( settings, level, msg, tn )\n\t{\n\t\tmsg = 'DataTables warning: '+\n\t\t\t(settings ? 'table id='+settings.sTableId+' - ' : '')+msg;\n\t\n\t\tif ( tn ) {\n\t\t\tmsg += '. For more information about this error, please see '+\n\t\t\t'http://datatables.net/tn/'+tn;\n\t\t}\n\t\n\t\tif ( ! level  ) {\n\t\t\t// Backwards compatibility pre 1.10\n\t\t\tvar ext = DataTable.ext;\n\t\t\tvar type = ext.sErrMode || ext.errMode;\n\t\n\t\t\tif ( settings ) {\n\t\t\t\t_fnCallbackFire( settings, null, 'error', [ settings, tn, msg ] );\n\t\t\t}\n\t\n\t\t\tif ( type == 'alert' ) {\n\t\t\t\talert( msg );\n\t\t\t}\n\t\t\telse if ( type == 'throw' ) {\n\t\t\t\tthrow new Error(msg);\n\t\t\t}\n\t\t\telse if ( typeof type == 'function' ) {\n\t\t\t\ttype( settings, tn, msg );\n\t\t\t}\n\t\t}\n\t\telse if ( window.console && console.log ) {\n\t\t\tconsole.log( msg );\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * See if a property is defined on one object, if so assign it to the other object\n\t *  @param {object} ret target object\n\t *  @param {object} src source object\n\t *  @param {string} name property\n\t *  @param {string} [mappedName] name to map too - optional, name used if not given\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnMap( ret, src, name, mappedName )\n\t{\n\t\tif ( $.isArray( name ) ) {\n\t\t\t$.each( name, function (i, val) {\n\t\t\t\tif ( $.isArray( val ) ) {\n\t\t\t\t\t_fnMap( ret, src, val[0], val[1] );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t_fnMap( ret, src, val );\n\t\t\t\t}\n\t\t\t} );\n\t\n\t\t\treturn;\n\t\t}\n\t\n\t\tif ( mappedName === undefined ) {\n\t\t\tmappedName = name;\n\t\t}\n\t\n\t\tif ( src[name] !== undefined ) {\n\t\t\tret[mappedName] = src[name];\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Extend objects - very similar to jQuery.extend, but deep copy objects, and\n\t * shallow copy arrays. The reason we need to do this, is that we don't want to\n\t * deep copy array init values (such as aaSorting) since the dev wouldn't be\n\t * able to override them, but we do want to deep copy arrays.\n\t *  @param {object} out Object to extend\n\t *  @param {object} extender Object from which the properties will be applied to\n\t *      out\n\t *  @param {boolean} breakRefs If true, then arrays will be sliced to take an\n\t *      independent copy with the exception of the `data` or `aaData` parameters\n\t *      if they are present. This is so you can pass in a collection to\n\t *      DataTables and have that used as your data source without breaking the\n\t *      references\n\t *  @returns {object} out Reference, just for convenience - out === the return.\n\t *  @memberof DataTable#oApi\n\t *  @todo This doesn't take account of arrays inside the deep copied objects.\n\t */\n\tfunction _fnExtend( out, extender, breakRefs )\n\t{\n\t\tvar val;\n\t\n\t\tfor ( var prop in extender ) {\n\t\t\tif ( extender.hasOwnProperty(prop) ) {\n\t\t\t\tval = extender[prop];\n\t\n\t\t\t\tif ( $.isPlainObject( val ) ) {\n\t\t\t\t\tif ( ! $.isPlainObject( out[prop] ) ) {\n\t\t\t\t\t\tout[prop] = {};\n\t\t\t\t\t}\n\t\t\t\t\t$.extend( true, out[prop], val );\n\t\t\t\t}\n\t\t\t\telse if ( breakRefs && prop !== 'data' && prop !== 'aaData' && $.isArray(val) ) {\n\t\t\t\t\tout[prop] = val.slice();\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tout[prop] = val;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\treturn out;\n\t}\n\t\n\t\n\t/**\n\t * Bind an event handers to allow a click or return key to activate the callback.\n\t * This is good for accessibility since a return on the keyboard will have the\n\t * same effect as a click, if the element has focus.\n\t *  @param {element} n Element to bind the action to\n\t *  @param {object} oData Data object to pass to the triggered function\n\t *  @param {function} fn Callback function for when the event is triggered\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnBindAction( n, oData, fn )\n\t{\n\t\t$(n)\n\t\t\t.on( 'click.DT', oData, function (e) {\n\t\t\t\t\t$(n).trigger('blur'); // Remove focus outline for mouse users\n\t\t\t\t\tfn(e);\n\t\t\t\t} )\n\t\t\t.on( 'keypress.DT', oData, function (e){\n\t\t\t\t\tif ( e.which === 13 ) {\n\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\tfn(e);\n\t\t\t\t\t}\n\t\t\t\t} )\n\t\t\t.on( 'selectstart.DT', function () {\n\t\t\t\t\t/* Take the brutal approach to cancelling text selection */\n\t\t\t\t\treturn false;\n\t\t\t\t} );\n\t}\n\t\n\t\n\t/**\n\t * Register a callback function. Easily allows a callback function to be added to\n\t * an array store of callback functions that can then all be called together.\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {string} sStore Name of the array storage for the callbacks in oSettings\n\t *  @param {function} fn Function to be called back\n\t *  @param {string} sName Identifying name for the callback (i.e. a label)\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnCallbackReg( oSettings, sStore, fn, sName )\n\t{\n\t\tif ( fn )\n\t\t{\n\t\t\toSettings[sStore].push( {\n\t\t\t\t\"fn\": fn,\n\t\t\t\t\"sName\": sName\n\t\t\t} );\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Fire callback functions and trigger events. Note that the loop over the\n\t * callback array store is done backwards! Further note that you do not want to\n\t * fire off triggers in time sensitive applications (for example cell creation)\n\t * as its slow.\n\t *  @param {object} settings dataTables settings object\n\t *  @param {string} callbackArr Name of the array storage for the callbacks in\n\t *      oSettings\n\t *  @param {string} eventName Name of the jQuery custom event to trigger. If\n\t *      null no trigger is fired\n\t *  @param {array} args Array of arguments to pass to the callback function /\n\t *      trigger\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnCallbackFire( settings, callbackArr, eventName, args )\n\t{\n\t\tvar ret = [];\n\t\n\t\tif ( callbackArr ) {\n\t\t\tret = $.map( settings[callbackArr].slice().reverse(), function (val, i) {\n\t\t\t\treturn val.fn.apply( settings.oInstance, args );\n\t\t\t} );\n\t\t}\n\t\n\t\tif ( eventName !== null ) {\n\t\t\tvar e = $.Event( eventName+'.dt' );\n\t\n\t\t\t$(settings.nTable).trigger( e, args );\n\t\n\t\t\tret.push( e.result );\n\t\t}\n\t\n\t\treturn ret;\n\t}\n\t\n\t\n\tfunction _fnLengthOverflow ( settings )\n\t{\n\t\tvar\n\t\t\tstart = settings._iDisplayStart,\n\t\t\tend = settings.fnDisplayEnd(),\n\t\t\tlen = settings._iDisplayLength;\n\t\n\t\t/* If we have space to show extra rows (backing up from the end point - then do so */\n\t\tif ( start >= end )\n\t\t{\n\t\t\tstart = end - len;\n\t\t}\n\t\n\t\t// Keep the start record on the current page\n\t\tstart -= (start % len);\n\t\n\t\tif ( len === -1 || start < 0 )\n\t\t{\n\t\t\tstart = 0;\n\t\t}\n\t\n\t\tsettings._iDisplayStart = start;\n\t}\n\t\n\t\n\tfunction _fnRenderer( settings, type )\n\t{\n\t\tvar renderer = settings.renderer;\n\t\tvar host = DataTable.ext.renderer[type];\n\t\n\t\tif ( $.isPlainObject( renderer ) && renderer[type] ) {\n\t\t\t// Specific renderer for this type. If available use it, otherwise use\n\t\t\t// the default.\n\t\t\treturn host[renderer[type]] || host._;\n\t\t}\n\t\telse if ( typeof renderer === 'string' ) {\n\t\t\t// Common renderer - if there is one available for this type use it,\n\t\t\t// otherwise use the default\n\t\t\treturn host[renderer] || host._;\n\t\t}\n\t\n\t\t// Use the default\n\t\treturn host._;\n\t}\n\t\n\t\n\t/**\n\t * Detect the data source being used for the table. Used to simplify the code\n\t * a little (ajax) and to make it compress a little smaller.\n\t *\n\t *  @param {object} settings dataTables settings object\n\t *  @returns {string} Data source\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnDataSource ( settings )\n\t{\n\t\tif ( settings.oFeatures.bServerSide ) {\n\t\t\treturn 'ssp';\n\t\t}\n\t\telse if ( settings.ajax || settings.sAjaxSource ) {\n\t\t\treturn 'ajax';\n\t\t}\n\t\treturn 'dom';\n\t}\n\t\n\n\t\n\t\n\t/**\n\t * Computed structure of the DataTables API, defined by the options passed to\n\t * `DataTable.Api.register()` when building the API.\n\t *\n\t * The structure is built in order to speed creation and extension of the Api\n\t * objects since the extensions are effectively pre-parsed.\n\t *\n\t * The array is an array of objects with the following structure, where this\n\t * base array represents the Api prototype base:\n\t *\n\t *     [\n\t *       {\n\t *         name:      'data'                -- string   - Property name\n\t *         val:       function () {},       -- function - Api method (or undefined if just an object\n\t *         methodExt: [ ... ],              -- array    - Array of Api object definitions to extend the method result\n\t *         propExt:   [ ... ]               -- array    - Array of Api object definitions to extend the property\n\t *       },\n\t *       {\n\t *         name:     'row'\n\t *         val:       {},\n\t *         methodExt: [ ... ],\n\t *         propExt:   [\n\t *           {\n\t *             name:      'data'\n\t *             val:       function () {},\n\t *             methodExt: [ ... ],\n\t *             propExt:   [ ... ]\n\t *           },\n\t *           ...\n\t *         ]\n\t *       }\n\t *     ]\n\t *\n\t * @type {Array}\n\t * @ignore\n\t */\n\tvar __apiStruct = [];\n\t\n\t\n\t/**\n\t * `Array.prototype` reference.\n\t *\n\t * @type object\n\t * @ignore\n\t */\n\tvar __arrayProto = Array.prototype;\n\t\n\t\n\t/**\n\t * Abstraction for `context` parameter of the `Api` constructor to allow it to\n\t * take several different forms for ease of use.\n\t *\n\t * Each of the input parameter types will be converted to a DataTables settings\n\t * object where possible.\n\t *\n\t * @param  {string|node|jQuery|object} mixed DataTable identifier. Can be one\n\t *   of:\n\t *\n\t *   * `string` - jQuery selector. Any DataTables' matching the given selector\n\t *     with be found and used.\n\t *   * `node` - `TABLE` node which has already been formed into a DataTable.\n\t *   * `jQuery` - A jQuery object of `TABLE` nodes.\n\t *   * `object` - DataTables settings object\n\t *   * `DataTables.Api` - API instance\n\t * @return {array|null} Matching DataTables settings objects. `null` or\n\t *   `undefined` is returned if no matching DataTable is found.\n\t * @ignore\n\t */\n\tvar _toSettings = function ( mixed )\n\t{\n\t\tvar idx, jq;\n\t\tvar settings = DataTable.settings;\n\t\tvar tables = $.map( settings, function (el, i) {\n\t\t\treturn el.nTable;\n\t\t} );\n\t\n\t\tif ( ! mixed ) {\n\t\t\treturn [];\n\t\t}\n\t\telse if ( mixed.nTable && mixed.oApi ) {\n\t\t\t// DataTables settings object\n\t\t\treturn [ mixed ];\n\t\t}\n\t\telse if ( mixed.nodeName && mixed.nodeName.toLowerCase() === 'table' ) {\n\t\t\t// Table node\n\t\t\tidx = $.inArray( mixed, tables );\n\t\t\treturn idx !== -1 ? [ settings[idx] ] : null;\n\t\t}\n\t\telse if ( mixed && typeof mixed.settings === 'function' ) {\n\t\t\treturn mixed.settings().toArray();\n\t\t}\n\t\telse if ( typeof mixed === 'string' ) {\n\t\t\t// jQuery selector\n\t\t\tjq = $(mixed);\n\t\t}\n\t\telse if ( mixed instanceof $ ) {\n\t\t\t// jQuery object (also DataTables instance)\n\t\t\tjq = mixed;\n\t\t}\n\t\n\t\tif ( jq ) {\n\t\t\treturn jq.map( function(i) {\n\t\t\t\tidx = $.inArray( this, tables );\n\t\t\t\treturn idx !== -1 ? settings[idx] : null;\n\t\t\t} ).toArray();\n\t\t}\n\t};\n\t\n\t\n\t/**\n\t * DataTables API class - used to control and interface with  one or more\n\t * DataTables enhanced tables.\n\t *\n\t * The API class is heavily based on jQuery, presenting a chainable interface\n\t * that you can use to interact with tables. Each instance of the API class has\n\t * a \"context\" - i.e. the tables that it will operate on. This could be a single\n\t * table, all tables on a page or a sub-set thereof.\n\t *\n\t * Additionally the API is designed to allow you to easily work with the data in\n\t * the tables, retrieving and manipulating it as required. This is done by\n\t * presenting the API class as an array like interface. The contents of the\n\t * array depend upon the actions requested by each method (for example\n\t * `rows().nodes()` will return an array of nodes, while `rows().data()` will\n\t * return an array of objects or arrays depending upon your table's\n\t * configuration). The API object has a number of array like methods (`push`,\n\t * `pop`, `reverse` etc) as well as additional helper methods (`each`, `pluck`,\n\t * `unique` etc) to assist your working with the data held in a table.\n\t *\n\t * Most methods (those which return an Api instance) are chainable, which means\n\t * the return from a method call also has all of the methods available that the\n\t * top level object had. For example, these two calls are equivalent:\n\t *\n\t *     // Not chained\n\t *     api.row.add( {...} );\n\t *     api.draw();\n\t *\n\t *     // Chained\n\t *     api.row.add( {...} ).draw();\n\t *\n\t * @class DataTable.Api\n\t * @param {array|object|string|jQuery} context DataTable identifier. This is\n\t *   used to define which DataTables enhanced tables this API will operate on.\n\t *   Can be one of:\n\t *\n\t *   * `string` - jQuery selector. Any DataTables' matching the given selector\n\t *     with be found and used.\n\t *   * `node` - `TABLE` node which has already been formed into a DataTable.\n\t *   * `jQuery` - A jQuery object of `TABLE` nodes.\n\t *   * `object` - DataTables settings object\n\t * @param {array} [data] Data to initialise the Api instance with.\n\t *\n\t * @example\n\t *   // Direct initialisation during DataTables construction\n\t *   var api = $('#example').DataTable();\n\t *\n\t * @example\n\t *   // Initialisation using a DataTables jQuery object\n\t *   var api = $('#example').dataTable().api();\n\t *\n\t * @example\n\t *   // Initialisation as a constructor\n\t *   var api = new $.fn.DataTable.Api( 'table.dataTable' );\n\t */\n\t_Api = function ( context, data )\n\t{\n\t\tif ( ! (this instanceof _Api) ) {\n\t\t\treturn new _Api( context, data );\n\t\t}\n\t\n\t\tvar settings = [];\n\t\tvar ctxSettings = function ( o ) {\n\t\t\tvar a = _toSettings( o );\n\t\t\tif ( a ) {\n\t\t\t\tsettings.push.apply( settings, a );\n\t\t\t}\n\t\t};\n\t\n\t\tif ( $.isArray( context ) ) {\n\t\t\tfor ( var i=0, ien=context.length ; i<ien ; i++ ) {\n\t\t\t\tctxSettings( context[i] );\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tctxSettings( context );\n\t\t}\n\t\n\t\t// Remove duplicates\n\t\tthis.context = _unique( settings );\n\t\n\t\t// Initial data\n\t\tif ( data ) {\n\t\t\t$.merge( this, data );\n\t\t}\n\t\n\t\t// selector\n\t\tthis.selector = {\n\t\t\trows: null,\n\t\t\tcols: null,\n\t\t\topts: null\n\t\t};\n\t\n\t\t_Api.extend( this, this, __apiStruct );\n\t};\n\t\n\tDataTable.Api = _Api;\n\t\n\t// Don't destroy the existing prototype, just extend it. Required for jQuery 2's\n\t// isPlainObject.\n\t$.extend( _Api.prototype, {\n\t\tany: function ()\n\t\t{\n\t\t\treturn this.count() !== 0;\n\t\t},\n\t\n\t\n\t\tconcat:  __arrayProto.concat,\n\t\n\t\n\t\tcontext: [], // array of table settings objects\n\t\n\t\n\t\tcount: function ()\n\t\t{\n\t\t\treturn this.flatten().length;\n\t\t},\n\t\n\t\n\t\teach: function ( fn )\n\t\t{\n\t\t\tfor ( var i=0, ien=this.length ; i<ien; i++ ) {\n\t\t\t\tfn.call( this, this[i], i, this );\n\t\t\t}\n\t\n\t\t\treturn this;\n\t\t},\n\t\n\t\n\t\teq: function ( idx )\n\t\t{\n\t\t\tvar ctx = this.context;\n\t\n\t\t\treturn ctx.length > idx ?\n\t\t\t\tnew _Api( ctx[idx], this[idx] ) :\n\t\t\t\tnull;\n\t\t},\n\t\n\t\n\t\tfilter: function ( fn )\n\t\t{\n\t\t\tvar a = [];\n\t\n\t\t\tif ( __arrayProto.filter ) {\n\t\t\t\ta = __arrayProto.filter.call( this, fn, this );\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// Compatibility for browsers without EMCA-252-5 (JS 1.6)\n\t\t\t\tfor ( var i=0, ien=this.length ; i<ien ; i++ ) {\n\t\t\t\t\tif ( fn.call( this, this[i], i, this ) ) {\n\t\t\t\t\t\ta.push( this[i] );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\treturn new _Api( this.context, a );\n\t\t},\n\t\n\t\n\t\tflatten: function ()\n\t\t{\n\t\t\tvar a = [];\n\t\t\treturn new _Api( this.context, a.concat.apply( a, this.toArray() ) );\n\t\t},\n\t\n\t\n\t\tjoin:    __arrayProto.join,\n\t\n\t\n\t\tindexOf: __arrayProto.indexOf || function (obj, start)\n\t\t{\n\t\t\tfor ( var i=(start || 0), ien=this.length ; i<ien ; i++ ) {\n\t\t\t\tif ( this[i] === obj ) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn -1;\n\t\t},\n\t\n\t\titerator: function ( flatten, type, fn, alwaysNew ) {\n\t\t\tvar\n\t\t\t\ta = [], ret,\n\t\t\t\ti, ien, j, jen,\n\t\t\t\tcontext = this.context,\n\t\t\t\trows, items, item,\n\t\t\t\tselector = this.selector;\n\t\n\t\t\t// Argument shifting\n\t\t\tif ( typeof flatten === 'string' ) {\n\t\t\t\talwaysNew = fn;\n\t\t\t\tfn = type;\n\t\t\t\ttype = flatten;\n\t\t\t\tflatten = false;\n\t\t\t}\n\t\n\t\t\tfor ( i=0, ien=context.length ; i<ien ; i++ ) {\n\t\t\t\tvar apiInst = new _Api( context[i] );\n\t\n\t\t\t\tif ( type === 'table' ) {\n\t\t\t\t\tret = fn.call( apiInst, context[i], i );\n\t\n\t\t\t\t\tif ( ret !== undefined ) {\n\t\t\t\t\t\ta.push( ret );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if ( type === 'columns' || type === 'rows' ) {\n\t\t\t\t\t// this has same length as context - one entry for each table\n\t\t\t\t\tret = fn.call( apiInst, context[i], this[i], i );\n\t\n\t\t\t\t\tif ( ret !== undefined ) {\n\t\t\t\t\t\ta.push( ret );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if ( type === 'column' || type === 'column-rows' || type === 'row' || type === 'cell' ) {\n\t\t\t\t\t// columns and rows share the same structure.\n\t\t\t\t\t// 'this' is an array of column indexes for each context\n\t\t\t\t\titems = this[i];\n\t\n\t\t\t\t\tif ( type === 'column-rows' ) {\n\t\t\t\t\t\trows = _selector_row_indexes( context[i], selector.opts );\n\t\t\t\t\t}\n\t\n\t\t\t\t\tfor ( j=0, jen=items.length ; j<jen ; j++ ) {\n\t\t\t\t\t\titem = items[j];\n\t\n\t\t\t\t\t\tif ( type === 'cell' ) {\n\t\t\t\t\t\t\tret = fn.call( apiInst, context[i], item.row, item.column, i, j );\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tret = fn.call( apiInst, context[i], item, i, j, rows );\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\tif ( ret !== undefined ) {\n\t\t\t\t\t\t\ta.push( ret );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\tif ( a.length || alwaysNew ) {\n\t\t\t\tvar api = new _Api( context, flatten ? a.concat.apply( [], a ) : a );\n\t\t\t\tvar apiSelector = api.selector;\n\t\t\t\tapiSelector.rows = selector.rows;\n\t\t\t\tapiSelector.cols = selector.cols;\n\t\t\t\tapiSelector.opts = selector.opts;\n\t\t\t\treturn api;\n\t\t\t}\n\t\t\treturn this;\n\t\t},\n\t\n\t\n\t\tlastIndexOf: __arrayProto.lastIndexOf || function (obj, start)\n\t\t{\n\t\t\t// Bit cheeky...\n\t\t\treturn this.indexOf.apply( this.toArray.reverse(), arguments );\n\t\t},\n\t\n\t\n\t\tlength:  0,\n\t\n\t\n\t\tmap: function ( fn )\n\t\t{\n\t\t\tvar a = [];\n\t\n\t\t\tif ( __arrayProto.map ) {\n\t\t\t\ta = __arrayProto.map.call( this, fn, this );\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// Compatibility for browsers without EMCA-252-5 (JS 1.6)\n\t\t\t\tfor ( var i=0, ien=this.length ; i<ien ; i++ ) {\n\t\t\t\t\ta.push( fn.call( this, this[i], i ) );\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\treturn new _Api( this.context, a );\n\t\t},\n\t\n\t\n\t\tpluck: function ( prop )\n\t\t{\n\t\t\treturn this.map( function ( el ) {\n\t\t\t\treturn el[ prop ];\n\t\t\t} );\n\t\t},\n\t\n\t\tpop:     __arrayProto.pop,\n\t\n\t\n\t\tpush:    __arrayProto.push,\n\t\n\t\n\t\t// Does not return an API instance\n\t\treduce: __arrayProto.reduce || function ( fn, init )\n\t\t{\n\t\t\treturn _fnReduce( this, fn, init, 0, this.length, 1 );\n\t\t},\n\t\n\t\n\t\treduceRight: __arrayProto.reduceRight || function ( fn, init )\n\t\t{\n\t\t\treturn _fnReduce( this, fn, init, this.length-1, -1, -1 );\n\t\t},\n\t\n\t\n\t\treverse: __arrayProto.reverse,\n\t\n\t\n\t\t// Object with rows, columns and opts\n\t\tselector: null,\n\t\n\t\n\t\tshift:   __arrayProto.shift,\n\t\n\t\n\t\tslice: function () {\n\t\t\treturn new _Api( this.context, this );\n\t\t},\n\t\n\t\n\t\tsort:    __arrayProto.sort, // ? name - order?\n\t\n\t\n\t\tsplice:  __arrayProto.splice,\n\t\n\t\n\t\ttoArray: function ()\n\t\t{\n\t\t\treturn __arrayProto.slice.call( this );\n\t\t},\n\t\n\t\n\t\tto$: function ()\n\t\t{\n\t\t\treturn $( this );\n\t\t},\n\t\n\t\n\t\ttoJQuery: function ()\n\t\t{\n\t\t\treturn $( this );\n\t\t},\n\t\n\t\n\t\tunique: function ()\n\t\t{\n\t\t\treturn new _Api( this.context, _unique(this) );\n\t\t},\n\t\n\t\n\t\tunshift: __arrayProto.unshift\n\t} );\n\t\n\t\n\t_Api.extend = function ( scope, obj, ext )\n\t{\n\t\t// Only extend API instances and static properties of the API\n\t\tif ( ! ext.length || ! obj || ( ! (obj instanceof _Api) && ! obj.__dt_wrapper ) ) {\n\t\t\treturn;\n\t\t}\n\t\n\t\tvar\n\t\t\ti, ien,\n\t\t\tstruct,\n\t\t\tmethodScoping = function ( scope, fn, struc ) {\n\t\t\t\treturn function () {\n\t\t\t\t\tvar ret = fn.apply( scope, arguments );\n\t\n\t\t\t\t\t// Method extension\n\t\t\t\t\t_Api.extend( ret, ret, struc.methodExt );\n\t\t\t\t\treturn ret;\n\t\t\t\t};\n\t\t\t};\n\t\n\t\tfor ( i=0, ien=ext.length ; i<ien ; i++ ) {\n\t\t\tstruct = ext[i];\n\t\n\t\t\t// Value\n\t\t\tobj[ struct.name ] = struct.type === 'function' ?\n\t\t\t\tmethodScoping( scope, struct.val, struct ) :\n\t\t\t\tstruct.type === 'object' ?\n\t\t\t\t\t{} :\n\t\t\t\t\tstruct.val;\n\t\n\t\t\tobj[ struct.name ].__dt_wrapper = true;\n\t\n\t\t\t// Property extension\n\t\t\t_Api.extend( scope, obj[ struct.name ], struct.propExt );\n\t\t}\n\t};\n\t\n\t\n\t// @todo - Is there need for an augment function?\n\t// _Api.augment = function ( inst, name )\n\t// {\n\t// \t// Find src object in the structure from the name\n\t// \tvar parts = name.split('.');\n\t\n\t// \t_Api.extend( inst, obj );\n\t// };\n\t\n\t\n\t//     [\n\t//       {\n\t//         name:      'data'                -- string   - Property name\n\t//         val:       function () {},       -- function - Api method (or undefined if just an object\n\t//         methodExt: [ ... ],              -- array    - Array of Api object definitions to extend the method result\n\t//         propExt:   [ ... ]               -- array    - Array of Api object definitions to extend the property\n\t//       },\n\t//       {\n\t//         name:     'row'\n\t//         val:       {},\n\t//         methodExt: [ ... ],\n\t//         propExt:   [\n\t//           {\n\t//             name:      'data'\n\t//             val:       function () {},\n\t//             methodExt: [ ... ],\n\t//             propExt:   [ ... ]\n\t//           },\n\t//           ...\n\t//         ]\n\t//       }\n\t//     ]\n\t\n\t_Api.register = _api_register = function ( name, val )\n\t{\n\t\tif ( $.isArray( name ) ) {\n\t\t\tfor ( var j=0, jen=name.length ; j<jen ; j++ ) {\n\t\t\t\t_Api.register( name[j], val );\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\n\t\tvar\n\t\t\ti, ien,\n\t\t\their = name.split('.'),\n\t\t\tstruct = __apiStruct,\n\t\t\tkey, method;\n\t\n\t\tvar find = function ( src, name ) {\n\t\t\tfor ( var i=0, ien=src.length ; i<ien ; i++ ) {\n\t\t\t\tif ( src[i].name === name ) {\n\t\t\t\t\treturn src[i];\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t};\n\t\n\t\tfor ( i=0, ien=heir.length ; i<ien ; i++ ) {\n\t\t\tmethod = heir[i].indexOf('()') !== -1;\n\t\t\tkey = method ?\n\t\t\t\their[i].replace('()', '') :\n\t\t\t\their[i];\n\t\n\t\t\tvar src = find( struct, key );\n\t\t\tif ( ! src ) {\n\t\t\t\tsrc = {\n\t\t\t\t\tname:      key,\n\t\t\t\t\tval:       {},\n\t\t\t\t\tmethodExt: [],\n\t\t\t\t\tpropExt:   [],\n\t\t\t\t\ttype:      'object'\n\t\t\t\t};\n\t\t\t\tstruct.push( src );\n\t\t\t}\n\t\n\t\t\tif ( i === ien-1 ) {\n\t\t\t\tsrc.val = val;\n\t\t\t\tsrc.type = typeof val === 'function' ?\n\t\t\t\t\t'function' :\n\t\t\t\t\t$.isPlainObject( val ) ?\n\t\t\t\t\t\t'object' :\n\t\t\t\t\t\t'other';\n\t\t\t}\n\t\t\telse {\n\t\t\t\tstruct = method ?\n\t\t\t\t\tsrc.methodExt :\n\t\t\t\t\tsrc.propExt;\n\t\t\t}\n\t\t}\n\t};\n\t\n\t_Api.registerPlural = _api_registerPlural = function ( pluralName, singularName, val ) {\n\t\t_Api.register( pluralName, val );\n\t\n\t\t_Api.register( singularName, function () {\n\t\t\tvar ret = val.apply( this, arguments );\n\t\n\t\t\tif ( ret === this ) {\n\t\t\t\t// Returned item is the API instance that was passed in, return it\n\t\t\t\treturn this;\n\t\t\t}\n\t\t\telse if ( ret instanceof _Api ) {\n\t\t\t\t// New API instance returned, want the value from the first item\n\t\t\t\t// in the returned array for the singular result.\n\t\t\t\treturn ret.length ?\n\t\t\t\t\t$.isArray( ret[0] ) ?\n\t\t\t\t\t\tnew _Api( ret.context, ret[0] ) : // Array results are 'enhanced'\n\t\t\t\t\t\tret[0] :\n\t\t\t\t\tundefined;\n\t\t\t}\n\t\n\t\t\t// Non-API return - just fire it back\n\t\t\treturn ret;\n\t\t} );\n\t};\n\t\n\t\n\t/**\n\t * Selector for HTML tables. Apply the given selector to the give array of\n\t * DataTables settings objects.\n\t *\n\t * @param {string|integer} [selector] jQuery selector string or integer\n\t * @param  {array} Array of DataTables settings objects to be filtered\n\t * @return {array}\n\t * @ignore\n\t */\n\tvar __table_selector = function ( selector, a )\n\t{\n\t\tif ( $.isArray(selector) ) {\n\t\t\treturn $.map( selector, function (item) {\n\t\t\t\treturn __table_selector(item, a);\n\t\t\t} );\n\t\t}\n\t\n\t\t// Integer is used to pick out a table by index\n\t\tif ( typeof selector === 'number' ) {\n\t\t\treturn [ a[ selector ] ];\n\t\t}\n\t\n\t\t// Perform a jQuery selector on the table nodes\n\t\tvar nodes = $.map( a, function (el, i) {\n\t\t\treturn el.nTable;\n\t\t} );\n\t\n\t\treturn $(nodes)\n\t\t\t.filter( selector )\n\t\t\t.map( function (i) {\n\t\t\t\t// Need to translate back from the table node to the settings\n\t\t\t\tvar idx = $.inArray( this, nodes );\n\t\t\t\treturn a[ idx ];\n\t\t\t} )\n\t\t\t.toArray();\n\t};\n\t\n\t\n\t\n\t/**\n\t * Context selector for the API's context (i.e. the tables the API instance\n\t * refers to.\n\t *\n\t * @name    DataTable.Api#tables\n\t * @param {string|integer} [selector] Selector to pick which tables the iterator\n\t *   should operate on. If not given, all tables in the current context are\n\t *   used. This can be given as a jQuery selector (for example `':gt(0)'`) to\n\t *   select multiple tables or as an integer to select a single table.\n\t * @returns {DataTable.Api} Returns a new API instance if a selector is given.\n\t */\n\t_api_register( 'tables()', function ( selector ) {\n\t\t// A new instance is created if there was a selector specified\n\t\treturn selector !== undefined && selector !== null ?\n\t\t\tnew _Api( __table_selector( selector, this.context ) ) :\n\t\t\tthis;\n\t} );\n\t\n\t\n\t_api_register( 'table()', function ( selector ) {\n\t\tvar tables = this.tables( selector );\n\t\tvar ctx = tables.context;\n\t\n\t\t// Truncate to the first matched table\n\t\treturn ctx.length ?\n\t\t\tnew _Api( ctx[0] ) :\n\t\t\ttables;\n\t} );\n\t\n\t\n\t_api_registerPlural( 'tables().nodes()', 'table().node()' , function () {\n\t\treturn this.iterator( 'table', function ( ctx ) {\n\t\t\treturn ctx.nTable;\n\t\t}, 1 );\n\t} );\n\t\n\t\n\t_api_registerPlural( 'tables().body()', 'table().body()' , function () {\n\t\treturn this.iterator( 'table', function ( ctx ) {\n\t\t\treturn ctx.nTBody;\n\t\t}, 1 );\n\t} );\n\t\n\t\n\t_api_registerPlural( 'tables().header()', 'table().header()' , function () {\n\t\treturn this.iterator( 'table', function ( ctx ) {\n\t\t\treturn ctx.nTHead;\n\t\t}, 1 );\n\t} );\n\t\n\t\n\t_api_registerPlural( 'tables().footer()', 'table().footer()' , function () {\n\t\treturn this.iterator( 'table', function ( ctx ) {\n\t\t\treturn ctx.nTFoot;\n\t\t}, 1 );\n\t} );\n\t\n\t\n\t_api_registerPlural( 'tables().containers()', 'table().container()' , function () {\n\t\treturn this.iterator( 'table', function ( ctx ) {\n\t\t\treturn ctx.nTableWrapper;\n\t\t}, 1 );\n\t} );\n\t\n\t\n\t\n\t/**\n\t * Redraw the tables in the current context.\n\t */\n\t_api_register( 'draw()', function ( paging ) {\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\tif ( paging === 'page' ) {\n\t\t\t\t_fnDraw( settings );\n\t\t\t}\n\t\t\telse {\n\t\t\t\tif ( typeof paging === 'string' ) {\n\t\t\t\t\tpaging = paging === 'full-hold' ?\n\t\t\t\t\t\tfalse :\n\t\t\t\t\t\ttrue;\n\t\t\t\t}\n\t\n\t\t\t\t_fnReDraw( settings, paging===false );\n\t\t\t}\n\t\t} );\n\t} );\n\t\n\t\n\t\n\t/**\n\t * Get the current page index.\n\t *\n\t * @return {integer} Current page index (zero based)\n\t *//**\n\t * Set the current page.\n\t *\n\t * Note that if you attempt to show a page which does not exist, DataTables will\n\t * not throw an error, but rather reset the paging.\n\t *\n\t * @param {integer|string} action The paging action to take. This can be one of:\n\t *  * `integer` - The page index to jump to\n\t *  * `string` - An action to take:\n\t *    * `first` - Jump to first page.\n\t *    * `next` - Jump to the next page\n\t *    * `previous` - Jump to previous page\n\t *    * `last` - Jump to the last page.\n\t * @returns {DataTables.Api} this\n\t */\n\t_api_register( 'page()', function ( action ) {\n\t\tif ( action === undefined ) {\n\t\t\treturn this.page.info().page; // not an expensive call\n\t\t}\n\t\n\t\t// else, have an action to take on all tables\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t_fnPageChange( settings, action );\n\t\t} );\n\t} );\n\t\n\t\n\t/**\n\t * Paging information for the first table in the current context.\n\t *\n\t * If you require paging information for another table, use the `table()` method\n\t * with a suitable selector.\n\t *\n\t * @return {object} Object with the following properties set:\n\t *  * `page` - Current page index (zero based - i.e. the first page is `0`)\n\t *  * `pages` - Total number of pages\n\t *  * `start` - Display index for the first record shown on the current page\n\t *  * `end` - Display index for the last record shown on the current page\n\t *  * `length` - Display length (number of records). Note that generally `start\n\t *    + length = end`, but this is not always true, for example if there are\n\t *    only 2 records to show on the final page, with a length of 10.\n\t *  * `recordsTotal` - Full data set length\n\t *  * `recordsDisplay` - Data set length once the current filtering criterion\n\t *    are applied.\n\t */\n\t_api_register( 'page.info()', function ( action ) {\n\t\tif ( this.context.length === 0 ) {\n\t\t\treturn undefined;\n\t\t}\n\t\n\t\tvar\n\t\t\tsettings   = this.context[0],\n\t\t\tstart      = settings._iDisplayStart,\n\t\t\tlen        = settings.oFeatures.bPaginate ? settings._iDisplayLength : -1,\n\t\t\tvisRecords = settings.fnRecordsDisplay(),\n\t\t\tall        = len === -1;\n\t\n\t\treturn {\n\t\t\t\"page\":           all ? 0 : Math.floor( start / len ),\n\t\t\t\"pages\":          all ? 1 : Math.ceil( visRecords / len ),\n\t\t\t\"start\":          start,\n\t\t\t\"end\":            settings.fnDisplayEnd(),\n\t\t\t\"length\":         len,\n\t\t\t\"recordsTotal\":   settings.fnRecordsTotal(),\n\t\t\t\"recordsDisplay\": visRecords,\n\t\t\t\"serverSide\":     _fnDataSource( settings ) === 'ssp'\n\t\t};\n\t} );\n\t\n\t\n\t/**\n\t * Get the current page length.\n\t *\n\t * @return {integer} Current page length. Note `-1` indicates that all records\n\t *   are to be shown.\n\t *//**\n\t * Set the current page length.\n\t *\n\t * @param {integer} Page length to set. Use `-1` to show all records.\n\t * @returns {DataTables.Api} this\n\t */\n\t_api_register( 'page.len()', function ( len ) {\n\t\t// Note that we can't call this function 'length()' because `length`\n\t\t// is a Javascript property of functions which defines how many arguments\n\t\t// the function expects.\n\t\tif ( len === undefined ) {\n\t\t\treturn this.context.length !== 0 ?\n\t\t\t\tthis.context[0]._iDisplayLength :\n\t\t\t\tundefined;\n\t\t}\n\t\n\t\t// else, set the page length\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t_fnLengthChange( settings, len );\n\t\t} );\n\t} );\n\t\n\t\n\t\n\tvar __reload = function ( settings, holdPosition, callback ) {\n\t\t// Use the draw event to trigger a callback\n\t\tif ( callback ) {\n\t\t\tvar api = new _Api( settings );\n\t\n\t\t\tapi.one( 'draw', function () {\n\t\t\t\tcallback( api.ajax.json() );\n\t\t\t} );\n\t\t}\n\t\n\t\tif ( _fnDataSource( settings ) == 'ssp' ) {\n\t\t\t_fnReDraw( settings, holdPosition );\n\t\t}\n\t\telse {\n\t\t\t_fnProcessingDisplay( settings, true );\n\t\n\t\t\t// Cancel an existing request\n\t\t\tvar xhr = settings.jqXHR;\n\t\t\tif ( xhr && xhr.readyState !== 4 ) {\n\t\t\t\txhr.abort();\n\t\t\t}\n\t\n\t\t\t// Trigger xhr\n\t\t\t_fnBuildAjax( settings, [], function( json ) {\n\t\t\t\t_fnClearTable( settings );\n\t\n\t\t\t\tvar data = _fnAjaxDataSrc( settings, json );\n\t\t\t\tfor ( var i=0, ien=data.length ; i<ien ; i++ ) {\n\t\t\t\t\t_fnAddData( settings, data[i] );\n\t\t\t\t}\n\t\n\t\t\t\t_fnReDraw( settings, holdPosition );\n\t\t\t\t_fnProcessingDisplay( settings, false );\n\t\t\t} );\n\t\t}\n\t};\n\t\n\t\n\t/**\n\t * Get the JSON response from the last Ajax request that DataTables made to the\n\t * server. Note that this returns the JSON from the first table in the current\n\t * context.\n\t *\n\t * @return {object} JSON received from the server.\n\t */\n\t_api_register( 'ajax.json()', function () {\n\t\tvar ctx = this.context;\n\t\n\t\tif ( ctx.length > 0 ) {\n\t\t\treturn ctx[0].json;\n\t\t}\n\t\n\t\t// else return undefined;\n\t} );\n\t\n\t\n\t/**\n\t * Get the data submitted in the last Ajax request\n\t */\n\t_api_register( 'ajax.params()', function () {\n\t\tvar ctx = this.context;\n\t\n\t\tif ( ctx.length > 0 ) {\n\t\t\treturn ctx[0].oAjaxData;\n\t\t}\n\t\n\t\t// else return undefined;\n\t} );\n\t\n\t\n\t/**\n\t * Reload tables from the Ajax data source. Note that this function will\n\t * automatically re-draw the table when the remote data has been loaded.\n\t *\n\t * @param {boolean} [reset=true] Reset (default) or hold the current paging\n\t *   position. A full re-sort and re-filter is performed when this method is\n\t *   called, which is why the pagination reset is the default action.\n\t * @returns {DataTables.Api} this\n\t */\n\t_api_register( 'ajax.reload()', function ( callback, resetPaging ) {\n\t\treturn this.iterator( 'table', function (settings) {\n\t\t\t__reload( settings, resetPaging===false, callback );\n\t\t} );\n\t} );\n\t\n\t\n\t/**\n\t * Get the current Ajax URL. Note that this returns the URL from the first\n\t * table in the current context.\n\t *\n\t * @return {string} Current Ajax source URL\n\t *//**\n\t * Set the Ajax URL. Note that this will set the URL for all tables in the\n\t * current context.\n\t *\n\t * @param {string} url URL to set.\n\t * @returns {DataTables.Api} this\n\t */\n\t_api_register( 'ajax.url()', function ( url ) {\n\t\tvar ctx = this.context;\n\t\n\t\tif ( url === undefined ) {\n\t\t\t// get\n\t\t\tif ( ctx.length === 0 ) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tctx = ctx[0];\n\t\n\t\t\treturn ctx.ajax ?\n\t\t\t\t$.isPlainObject( ctx.ajax ) ?\n\t\t\t\t\tctx.ajax.url :\n\t\t\t\t\tctx.ajax :\n\t\t\t\tctx.sAjaxSource;\n\t\t}\n\t\n\t\t// set\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\tif ( $.isPlainObject( settings.ajax ) ) {\n\t\t\t\tsettings.ajax.url = url;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tsettings.ajax = url;\n\t\t\t}\n\t\t\t// No need to consider sAjaxSource here since DataTables gives priority\n\t\t\t// to `ajax` over `sAjaxSource`. So setting `ajax` here, renders any\n\t\t\t// value of `sAjaxSource` redundant.\n\t\t} );\n\t} );\n\t\n\t\n\t/**\n\t * Load data from the newly set Ajax URL. Note that this method is only\n\t * available when `ajax.url()` is used to set a URL. Additionally, this method\n\t * has the same effect as calling `ajax.reload()` but is provided for\n\t * convenience when setting a new URL. Like `ajax.reload()` it will\n\t * automatically redraw the table once the remote data has been loaded.\n\t *\n\t * @returns {DataTables.Api} this\n\t */\n\t_api_register( 'ajax.url().load()', function ( callback, resetPaging ) {\n\t\t// Same as a reload, but makes sense to present it for easy access after a\n\t\t// url change\n\t\treturn this.iterator( 'table', function ( ctx ) {\n\t\t\t__reload( ctx, resetPaging===false, callback );\n\t\t} );\n\t} );\n\t\n\t\n\t\n\t\n\tvar _selector_run = function ( type, selector, selectFn, settings, opts )\n\t{\n\t\tvar\n\t\t\tout = [], res,\n\t\t\ta, i, ien, j, jen,\n\t\t\tselectorType = typeof selector;\n\t\n\t\t// Can't just check for isArray here, as an API or jQuery instance might be\n\t\t// given with their array like look\n\t\tif ( ! selector || selectorType === 'string' || selectorType === 'function' || selector.length === undefined ) {\n\t\t\tselector = [ selector ];\n\t\t}\n\t\n\t\tfor ( i=0, ien=selector.length ; i<ien ; i++ ) {\n\t\t\t// Only split on simple strings - complex expressions will be jQuery selectors\n\t\t\ta = selector[i] && selector[i].split && ! selector[i].match(/[\\[\\(:]/) ?\n\t\t\t\tselector[i].split(',') :\n\t\t\t\t[ selector[i] ];\n\t\n\t\t\tfor ( j=0, jen=a.length ; j<jen ; j++ ) {\n\t\t\t\tres = selectFn( typeof a[j] === 'string' ? $.trim(a[j]) : a[j] );\n\t\n\t\t\t\tif ( res && res.length ) {\n\t\t\t\t\tout = out.concat( res );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\t// selector extensions\n\t\tvar ext = _ext.selector[ type ];\n\t\tif ( ext.length ) {\n\t\t\tfor ( i=0, ien=ext.length ; i<ien ; i++ ) {\n\t\t\t\tout = ext[i]( settings, opts, out );\n\t\t\t}\n\t\t}\n\t\n\t\treturn _unique( out );\n\t};\n\t\n\t\n\tvar _selector_opts = function ( opts )\n\t{\n\t\tif ( ! opts ) {\n\t\t\topts = {};\n\t\t}\n\t\n\t\t// Backwards compatibility for 1.9- which used the terminology filter rather\n\t\t// than search\n\t\tif ( opts.filter && opts.search === undefined ) {\n\t\t\topts.search = opts.filter;\n\t\t}\n\t\n\t\treturn $.extend( {\n\t\t\tsearch: 'none',\n\t\t\torder: 'current',\n\t\t\tpage: 'all'\n\t\t}, opts );\n\t};\n\t\n\t\n\tvar _selector_first = function ( inst )\n\t{\n\t\t// Reduce the API instance to the first item found\n\t\tfor ( var i=0, ien=inst.length ; i<ien ; i++ ) {\n\t\t\tif ( inst[i].length > 0 ) {\n\t\t\t\t// Assign the first element to the first item in the instance\n\t\t\t\t// and truncate the instance and context\n\t\t\t\tinst[0] = inst[i];\n\t\t\t\tinst[0].length = 1;\n\t\t\t\tinst.length = 1;\n\t\t\t\tinst.context = [ inst.context[i] ];\n\t\n\t\t\t\treturn inst;\n\t\t\t}\n\t\t}\n\t\n\t\t// Not found - return an empty instance\n\t\tinst.length = 0;\n\t\treturn inst;\n\t};\n\t\n\t\n\tvar _selector_row_indexes = function ( settings, opts )\n\t{\n\t\tvar\n\t\t\ti, ien, tmp, a=[],\n\t\t\tdisplayFiltered = settings.aiDisplay,\n\t\t\tdisplayMaster = settings.aiDisplayMaster;\n\t\n\t\tvar\n\t\t\tsearch = opts.search,  // none, applied, removed\n\t\t\torder  = opts.order,   // applied, current, index (original - compatibility with 1.9)\n\t\t\tpage   = opts.page;    // all, current\n\t\n\t\tif ( _fnDataSource( settings ) == 'ssp' ) {\n\t\t\t// In server-side processing mode, most options are irrelevant since\n\t\t\t// rows not shown don't exist and the index order is the applied order\n\t\t\t// Removed is a special case - for consistency just return an empty\n\t\t\t// array\n\t\t\treturn search === 'removed' ?\n\t\t\t\t[] :\n\t\t\t\t_range( 0, displayMaster.length );\n\t\t}\n\t\telse if ( page == 'current' ) {\n\t\t\t// Current page implies that order=current and fitler=applied, since it is\n\t\t\t// fairly senseless otherwise, regardless of what order and search actually\n\t\t\t// are\n\t\t\tfor ( i=settings._iDisplayStart, ien=settings.fnDisplayEnd() ; i<ien ; i++ ) {\n\t\t\t\ta.push( displayFiltered[i] );\n\t\t\t}\n\t\t}\n\t\telse if ( order == 'current' || order == 'applied' ) {\n\t\t\tif ( search == 'none') {\n\t\t\t\ta = displayMaster.slice();\n\t\t\t}\n\t\t\telse if ( search == 'applied' ) {\n\t\t\t\ta = displayFiltered.slice();\n\t\t\t}\n\t\t\telse if ( search == 'removed' ) {\n\t\t\t\t// O(n+m) solution by creating a hash map\n\t\t\t\tvar displayFilteredMap = {};\n\t\n\t\t\t\tfor ( var i=0, ien=displayFiltered.length ; i<ien ; i++ ) {\n\t\t\t\t\tdisplayFilteredMap[displayFiltered[i]] = null;\n\t\t\t\t}\n\t\n\t\t\t\ta = $.map( displayMaster, function (el) {\n\t\t\t\t\treturn ! displayFilteredMap.hasOwnProperty(el) ?\n\t\t\t\t\t\tel :\n\t\t\t\t\t\tnull;\n\t\t\t\t} );\n\t\t\t}\n\t\t}\n\t\telse if ( order == 'index' || order == 'original' ) {\n\t\t\tfor ( i=0, ien=settings.aoData.length ; i<ien ; i++ ) {\n\t\t\t\tif ( search == 'none' ) {\n\t\t\t\t\ta.push( i );\n\t\t\t\t}\n\t\t\t\telse { // applied | removed\n\t\t\t\t\ttmp = $.inArray( i, displayFiltered );\n\t\n\t\t\t\t\tif ((tmp === -1 && search == 'removed') ||\n\t\t\t\t\t\t(tmp >= 0   && search == 'applied') )\n\t\t\t\t\t{\n\t\t\t\t\t\ta.push( i );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\treturn a;\n\t};\n\t\n\t\n\t/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n\t * Rows\n\t *\n\t * {}          - no selector - use all available rows\n\t * {integer}   - row aoData index\n\t * {node}      - TR node\n\t * {string}    - jQuery selector to apply to the TR elements\n\t * {array}     - jQuery array of nodes, or simply an array of TR nodes\n\t *\n\t */\n\tvar __row_selector = function ( settings, selector, opts )\n\t{\n\t\tvar rows;\n\t\tvar run = function ( sel ) {\n\t\t\tvar selInt = _intVal( sel );\n\t\t\tvar i, ien;\n\t\t\tvar aoData = settings.aoData;\n\t\n\t\t\t// Short cut - selector is a number and no options provided (default is\n\t\t\t// all records, so no need to check if the index is in there, since it\n\t\t\t// must be - dev error if the index doesn't exist).\n\t\t\tif ( selInt !== null && ! opts ) {\n\t\t\t\treturn [ selInt ];\n\t\t\t}\n\t\n\t\t\tif ( ! rows ) {\n\t\t\t\trows = _selector_row_indexes( settings, opts );\n\t\t\t}\n\t\n\t\t\tif ( selInt !== null && $.inArray( selInt, rows ) !== -1 ) {\n\t\t\t\t// Selector - integer\n\t\t\t\treturn [ selInt ];\n\t\t\t}\n\t\t\telse if ( sel === null || sel === undefined || sel === '' ) {\n\t\t\t\t// Selector - none\n\t\t\t\treturn rows;\n\t\t\t}\n\t\n\t\t\t// Selector - function\n\t\t\tif ( typeof sel === 'function' ) {\n\t\t\t\treturn $.map( rows, function (idx) {\n\t\t\t\t\tvar row = aoData[ idx ];\n\t\t\t\t\treturn sel( idx, row._aData, row.nTr ) ? idx : null;\n\t\t\t\t} );\n\t\t\t}\n\t\n\t\t\t// Selector - node\n\t\t\tif ( sel.nodeName ) {\n\t\t\t\tvar rowIdx = sel._DT_RowIndex;  // Property added by DT for fast lookup\n\t\t\t\tvar cellIdx = sel._DT_CellIndex;\n\t\n\t\t\t\tif ( rowIdx !== undefined ) {\n\t\t\t\t\t// Make sure that the row is actually still present in the table\n\t\t\t\t\treturn aoData[ rowIdx ] && aoData[ rowIdx ].nTr === sel ?\n\t\t\t\t\t\t[ rowIdx ] :\n\t\t\t\t\t\t[];\n\t\t\t\t}\n\t\t\t\telse if ( cellIdx ) {\n\t\t\t\t\treturn aoData[ cellIdx.row ] && aoData[ cellIdx.row ].nTr === sel.parentNode ?\n\t\t\t\t\t\t[ cellIdx.row ] :\n\t\t\t\t\t\t[];\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tvar host = $(sel).closest('*[data-dt-row]');\n\t\t\t\t\treturn host.length ?\n\t\t\t\t\t\t[ host.data('dt-row') ] :\n\t\t\t\t\t\t[];\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t// ID selector. Want to always be able to select rows by id, regardless\n\t\t\t// of if the tr element has been created or not, so can't rely upon\n\t\t\t// jQuery here - hence a custom implementation. This does not match\n\t\t\t// Sizzle's fast selector or HTML4 - in HTML5 the ID can be anything,\n\t\t\t// but to select it using a CSS selector engine (like Sizzle or\n\t\t\t// querySelect) it would need to need to be escaped for some characters.\n\t\t\t// DataTables simplifies this for row selectors since you can select\n\t\t\t// only a row. A # indicates an id any anything that follows is the id -\n\t\t\t// unescaped.\n\t\t\tif ( typeof sel === 'string' && sel.charAt(0) === '#' ) {\n\t\t\t\t// get row index from id\n\t\t\t\tvar rowObj = settings.aIds[ sel.replace( /^#/, '' ) ];\n\t\t\t\tif ( rowObj !== undefined ) {\n\t\t\t\t\treturn [ rowObj.idx ];\n\t\t\t\t}\n\t\n\t\t\t\t// need to fall through to jQuery in case there is DOM id that\n\t\t\t\t// matches\n\t\t\t}\n\t\t\t\n\t\t\t// Get nodes in the order from the `rows` array with null values removed\n\t\t\tvar nodes = _removeEmpty(\n\t\t\t\t_pluck_order( settings.aoData, rows, 'nTr' )\n\t\t\t);\n\t\n\t\t\t// Selector - jQuery selector string, array of nodes or jQuery object/\n\t\t\t// As jQuery's .filter() allows jQuery objects to be passed in filter,\n\t\t\t// it also allows arrays, so this will cope with all three options\n\t\t\treturn $(nodes)\n\t\t\t\t.filter( sel )\n\t\t\t\t.map( function () {\n\t\t\t\t\treturn this._DT_RowIndex;\n\t\t\t\t} )\n\t\t\t\t.toArray();\n\t\t};\n\t\n\t\treturn _selector_run( 'row', selector, run, settings, opts );\n\t};\n\t\n\t\n\t_api_register( 'rows()', function ( selector, opts ) {\n\t\t// argument shifting\n\t\tif ( selector === undefined ) {\n\t\t\tselector = '';\n\t\t}\n\t\telse if ( $.isPlainObject( selector ) ) {\n\t\t\topts = selector;\n\t\t\tselector = '';\n\t\t}\n\t\n\t\topts = _selector_opts( opts );\n\t\n\t\tvar inst = this.iterator( 'table', function ( settings ) {\n\t\t\treturn __row_selector( settings, selector, opts );\n\t\t}, 1 );\n\t\n\t\t// Want argument shifting here and in __row_selector?\n\t\tinst.selector.rows = selector;\n\t\tinst.selector.opts = opts;\n\t\n\t\treturn inst;\n\t} );\n\t\n\t_api_register( 'rows().nodes()', function () {\n\t\treturn this.iterator( 'row', function ( settings, row ) {\n\t\t\treturn settings.aoData[ row ].nTr || undefined;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_register( 'rows().data()', function () {\n\t\treturn this.iterator( true, 'rows', function ( settings, rows ) {\n\t\t\treturn _pluck_order( settings.aoData, rows, '_aData' );\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'rows().cache()', 'row().cache()', function ( type ) {\n\t\treturn this.iterator( 'row', function ( settings, row ) {\n\t\t\tvar r = settings.aoData[ row ];\n\t\t\treturn type === 'search' ? r._aFilterData : r._aSortData;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'rows().invalidate()', 'row().invalidate()', function ( src ) {\n\t\treturn this.iterator( 'row', function ( settings, row ) {\n\t\t\t_fnInvalidate( settings, row, src );\n\t\t} );\n\t} );\n\t\n\t_api_registerPlural( 'rows().indexes()', 'row().index()', function () {\n\t\treturn this.iterator( 'row', function ( settings, row ) {\n\t\t\treturn row;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'rows().ids()', 'row().id()', function ( hash ) {\n\t\tvar a = [];\n\t\tvar context = this.context;\n\t\n\t\t// `iterator` will drop undefined values, but in this case we want them\n\t\tfor ( var i=0, ien=context.length ; i<ien ; i++ ) {\n\t\t\tfor ( var j=0, jen=this[i].length ; j<jen ; j++ ) {\n\t\t\t\tvar id = context[i].rowIdFn( context[i].aoData[ this[i][j] ]._aData );\n\t\t\t\ta.push( (hash === true ? '#' : '' )+ id );\n\t\t\t}\n\t\t}\n\t\n\t\treturn new _Api( context, a );\n\t} );\n\t\n\t_api_registerPlural( 'rows().remove()', 'row().remove()', function () {\n\t\tvar that = this;\n\t\n\t\tthis.iterator( 'row', function ( settings, row, thatIdx ) {\n\t\t\tvar data = settings.aoData;\n\t\t\tvar rowData = data[ row ];\n\t\t\tvar i, ien, j, jen;\n\t\t\tvar loopRow, loopCells;\n\t\n\t\t\tdata.splice( row, 1 );\n\t\n\t\t\t// Update the cached indexes\n\t\t\tfor ( i=0, ien=data.length ; i<ien ; i++ ) {\n\t\t\t\tloopRow = data[i];\n\t\t\t\tloopCells = loopRow.anCells;\n\t\n\t\t\t\t// Rows\n\t\t\t\tif ( loopRow.nTr !== null ) {\n\t\t\t\t\tloopRow.nTr._DT_RowIndex = i;\n\t\t\t\t}\n\t\n\t\t\t\t// Cells\n\t\t\t\tif ( loopCells !== null ) {\n\t\t\t\t\tfor ( j=0, jen=loopCells.length ; j<jen ; j++ ) {\n\t\t\t\t\t\tloopCells[j]._DT_CellIndex.row = i;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t// Delete from the display arrays\n\t\t\t_fnDeleteIndex( settings.aiDisplayMaster, row );\n\t\t\t_fnDeleteIndex( settings.aiDisplay, row );\n\t\t\t_fnDeleteIndex( that[ thatIdx ], row, false ); // maintain local indexes\n\t\n\t\t\t// For server-side processing tables - subtract the deleted row from the count\n\t\t\tif ( settings._iRecordsDisplay > 0 ) {\n\t\t\t\tsettings._iRecordsDisplay--;\n\t\t\t}\n\t\n\t\t\t// Check for an 'overflow' they case for displaying the table\n\t\t\t_fnLengthOverflow( settings );\n\t\n\t\t\t// Remove the row's ID reference if there is one\n\t\t\tvar id = settings.rowIdFn( rowData._aData );\n\t\t\tif ( id !== undefined ) {\n\t\t\t\tdelete settings.aIds[ id ];\n\t\t\t}\n\t\t} );\n\t\n\t\tthis.iterator( 'table', function ( settings ) {\n\t\t\tfor ( var i=0, ien=settings.aoData.length ; i<ien ; i++ ) {\n\t\t\t\tsettings.aoData[i].idx = i;\n\t\t\t}\n\t\t} );\n\t\n\t\treturn this;\n\t} );\n\t\n\t\n\t_api_register( 'rows.add()', function ( rows ) {\n\t\tvar newRows = this.iterator( 'table', function ( settings ) {\n\t\t\t\tvar row, i, ien;\n\t\t\t\tvar out = [];\n\t\n\t\t\t\tfor ( i=0, ien=rows.length ; i<ien ; i++ ) {\n\t\t\t\t\trow = rows[i];\n\t\n\t\t\t\t\tif ( row.nodeName && row.nodeName.toUpperCase() === 'TR' ) {\n\t\t\t\t\t\tout.push( _fnAddTr( settings, row )[0] );\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tout.push( _fnAddData( settings, row ) );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\treturn out;\n\t\t\t}, 1 );\n\t\n\t\t// Return an Api.rows() extended instance, so rows().nodes() etc can be used\n\t\tvar modRows = this.rows( -1 );\n\t\tmodRows.pop();\n\t\t$.merge( modRows, newRows );\n\t\n\t\treturn modRows;\n\t} );\n\t\n\t\n\t\n\t\n\t\n\t/**\n\t *\n\t */\n\t_api_register( 'row()', function ( selector, opts ) {\n\t\treturn _selector_first( this.rows( selector, opts ) );\n\t} );\n\t\n\t\n\t_api_register( 'row().data()', function ( data ) {\n\t\tvar ctx = this.context;\n\t\n\t\tif ( data === undefined ) {\n\t\t\t// Get\n\t\t\treturn ctx.length && this.length ?\n\t\t\t\tctx[0].aoData[ this[0] ]._aData :\n\t\t\t\tundefined;\n\t\t}\n\t\n\t\t// Set\n\t\tvar row = ctx[0].aoData[ this[0] ];\n\t\trow._aData = data;\n\t\n\t\t// If the DOM has an id, and the data source is an array\n\t\tif ( $.isArray( data ) && row.nTr && row.nTr.id ) {\n\t\t\t_fnSetObjectDataFn( ctx[0].rowId )( data, row.nTr.id );\n\t\t}\n\t\n\t\t// Automatically invalidate\n\t\t_fnInvalidate( ctx[0], this[0], 'data' );\n\t\n\t\treturn this;\n\t} );\n\t\n\t\n\t_api_register( 'row().node()', function () {\n\t\tvar ctx = this.context;\n\t\n\t\treturn ctx.length && this.length ?\n\t\t\tctx[0].aoData[ this[0] ].nTr || null :\n\t\t\tnull;\n\t} );\n\t\n\t\n\t_api_register( 'row.add()', function ( row ) {\n\t\t// Allow a jQuery object to be passed in - only a single row is added from\n\t\t// it though - the first element in the set\n\t\tif ( row instanceof $ && row.length ) {\n\t\t\trow = row[0];\n\t\t}\n\t\n\t\tvar rows = this.iterator( 'table', function ( settings ) {\n\t\t\tif ( row.nodeName && row.nodeName.toUpperCase() === 'TR' ) {\n\t\t\t\treturn _fnAddTr( settings, row )[0];\n\t\t\t}\n\t\t\treturn _fnAddData( settings, row );\n\t\t} );\n\t\n\t\t// Return an Api.rows() extended instance, with the newly added row selected\n\t\treturn this.row( rows[0] );\n\t} );\n\t\n\t\n\t\n\tvar __details_add = function ( ctx, row, data, klass )\n\t{\n\t\t// Convert to array of TR elements\n\t\tvar rows = [];\n\t\tvar addRow = function ( r, k ) {\n\t\t\t// Recursion to allow for arrays of jQuery objects\n\t\t\tif ( $.isArray( r ) || r instanceof $ ) {\n\t\t\t\tfor ( var i=0, ien=r.length ; i<ien ; i++ ) {\n\t\t\t\t\taddRow( r[i], k );\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\n\t\t\t// If we get a TR element, then just add it directly - up to the dev\n\t\t\t// to add the correct number of columns etc\n\t\t\tif ( r.nodeName && r.nodeName.toLowerCase() === 'tr' ) {\n\t\t\t\trows.push( r );\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// Otherwise create a row with a wrapper\n\t\t\t\tvar created = $('<tr><td/></tr>').addClass( k );\n\t\t\t\t$('td', created)\n\t\t\t\t\t.addClass( k )\n\t\t\t\t\t.html( r )\n\t\t\t\t\t[0].colSpan = _fnVisbleColumns( ctx );\n\t\n\t\t\t\trows.push( created[0] );\n\t\t\t}\n\t\t};\n\t\n\t\taddRow( data, klass );\n\t\n\t\tif ( row._details ) {\n\t\t\trow._details.detach();\n\t\t}\n\t\n\t\trow._details = $(rows);\n\t\n\t\t// If the children were already shown, that state should be retained\n\t\tif ( row._detailsShow ) {\n\t\t\trow._details.insertAfter( row.nTr );\n\t\t}\n\t};\n\t\n\t\n\tvar __details_remove = function ( api, idx )\n\t{\n\t\tvar ctx = api.context;\n\t\n\t\tif ( ctx.length ) {\n\t\t\tvar row = ctx[0].aoData[ idx !== undefined ? idx : api[0] ];\n\t\n\t\t\tif ( row && row._details ) {\n\t\t\t\trow._details.remove();\n\t\n\t\t\t\trow._detailsShow = undefined;\n\t\t\t\trow._details = undefined;\n\t\t\t}\n\t\t}\n\t};\n\t\n\t\n\tvar __details_display = function ( api, show ) {\n\t\tvar ctx = api.context;\n\t\n\t\tif ( ctx.length && api.length ) {\n\t\t\tvar row = ctx[0].aoData[ api[0] ];\n\t\n\t\t\tif ( row._details ) {\n\t\t\t\trow._detailsShow = show;\n\t\n\t\t\t\tif ( show ) {\n\t\t\t\t\trow._details.insertAfter( row.nTr );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\trow._details.detach();\n\t\t\t\t}\n\t\n\t\t\t\t__details_events( ctx[0] );\n\t\t\t}\n\t\t}\n\t};\n\t\n\t\n\tvar __details_events = function ( settings )\n\t{\n\t\tvar api = new _Api( settings );\n\t\tvar namespace = '.dt.DT_details';\n\t\tvar drawEvent = 'draw'+namespace;\n\t\tvar colvisEvent = 'column-visibility'+namespace;\n\t\tvar destroyEvent = 'destroy'+namespace;\n\t\tvar data = settings.aoData;\n\t\n\t\tapi.off( drawEvent +' '+ colvisEvent +' '+ destroyEvent );\n\t\n\t\tif ( _pluck( data, '_details' ).length > 0 ) {\n\t\t\t// On each draw, insert the required elements into the document\n\t\t\tapi.on( drawEvent, function ( e, ctx ) {\n\t\t\t\tif ( settings !== ctx ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\n\t\t\t\tapi.rows( {page:'current'} ).eq(0).each( function (idx) {\n\t\t\t\t\t// Internal data grab\n\t\t\t\t\tvar row = data[ idx ];\n\t\n\t\t\t\t\tif ( row._detailsShow ) {\n\t\t\t\t\t\trow._details.insertAfter( row.nTr );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t} );\n\t\n\t\t\t// Column visibility change - update the colspan\n\t\t\tapi.on( colvisEvent, function ( e, ctx, idx, vis ) {\n\t\t\t\tif ( settings !== ctx ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\n\t\t\t\t// Update the colspan for the details rows (note, only if it already has\n\t\t\t\t// a colspan)\n\t\t\t\tvar row, visible = _fnVisbleColumns( ctx );\n\t\n\t\t\t\tfor ( var i=0, ien=data.length ; i<ien ; i++ ) {\n\t\t\t\t\trow = data[i];\n\t\n\t\t\t\t\tif ( row._details ) {\n\t\t\t\t\t\trow._details.children('td[colspan]').attr('colspan', visible );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\t\n\t\t\t// Table destroyed - nuke any child rows\n\t\t\tapi.on( destroyEvent, function ( e, ctx ) {\n\t\t\t\tif ( settings !== ctx ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\n\t\t\t\tfor ( var i=0, ien=data.length ; i<ien ; i++ ) {\n\t\t\t\t\tif ( data[i]._details ) {\n\t\t\t\t\t\t__details_remove( api, i );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\t};\n\t\n\t// Strings for the method names to help minification\n\tvar _emp = '';\n\tvar _child_obj = _emp+'row().child';\n\tvar _child_mth = _child_obj+'()';\n\t\n\t// data can be:\n\t//  tr\n\t//  string\n\t//  jQuery or array of any of the above\n\t_api_register( _child_mth, function ( data, klass ) {\n\t\tvar ctx = this.context;\n\t\n\t\tif ( data === undefined ) {\n\t\t\t// get\n\t\t\treturn ctx.length && this.length ?\n\t\t\t\tctx[0].aoData[ this[0] ]._details :\n\t\t\t\tundefined;\n\t\t}\n\t\telse if ( data === true ) {\n\t\t\t// show\n\t\t\tthis.child.show();\n\t\t}\n\t\telse if ( data === false ) {\n\t\t\t// remove\n\t\t\t__details_remove( this );\n\t\t}\n\t\telse if ( ctx.length && this.length ) {\n\t\t\t// set\n\t\t\t__details_add( ctx[0], ctx[0].aoData[ this[0] ], data, klass );\n\t\t}\n\t\n\t\treturn this;\n\t} );\n\t\n\t\n\t_api_register( [\n\t\t_child_obj+'.show()',\n\t\t_child_mth+'.show()' // only when `child()` was called with parameters (without\n\t], function ( show ) {   // it returns an object and this method is not executed)\n\t\t__details_display( this, true );\n\t\treturn this;\n\t} );\n\t\n\t\n\t_api_register( [\n\t\t_child_obj+'.hide()',\n\t\t_child_mth+'.hide()' // only when `child()` was called with parameters (without\n\t], function () {         // it returns an object and this method is not executed)\n\t\t__details_display( this, false );\n\t\treturn this;\n\t} );\n\t\n\t\n\t_api_register( [\n\t\t_child_obj+'.remove()',\n\t\t_child_mth+'.remove()' // only when `child()` was called with parameters (without\n\t], function () {           // it returns an object and this method is not executed)\n\t\t__details_remove( this );\n\t\treturn this;\n\t} );\n\t\n\t\n\t_api_register( _child_obj+'.isShown()', function () {\n\t\tvar ctx = this.context;\n\t\n\t\tif ( ctx.length && this.length ) {\n\t\t\t// _detailsShown as false or undefined will fall through to return false\n\t\t\treturn ctx[0].aoData[ this[0] ]._detailsShow || false;\n\t\t}\n\t\treturn false;\n\t} );\n\t\n\t\n\t\n\t/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n\t * Columns\n\t *\n\t * {integer}           - column index (>=0 count from left, <0 count from right)\n\t * \"{integer}:visIdx\"  - visible column index (i.e. translate to column index)  (>=0 count from left, <0 count from right)\n\t * \"{integer}:visible\" - alias for {integer}:visIdx  (>=0 count from left, <0 count from right)\n\t * \"{string}:name\"     - column name\n\t * \"{string}\"          - jQuery selector on column header nodes\n\t *\n\t */\n\t\n\t// can be an array of these items, comma separated list, or an array of comma\n\t// separated lists\n\t\n\tvar __re_column_selector = /^([^:]+):(name|visIdx|visible)$/;\n\t\n\t\n\t// r1 and r2 are redundant - but it means that the parameters match for the\n\t// iterator callback in columns().data()\n\tvar __columnData = function ( settings, column, r1, r2, rows ) {\n\t\tvar a = [];\n\t\tfor ( var row=0, ien=rows.length ; row<ien ; row++ ) {\n\t\t\ta.push( _fnGetCellData( settings, rows[row], column ) );\n\t\t}\n\t\treturn a;\n\t};\n\t\n\t\n\tvar __column_selector = function ( settings, selector, opts )\n\t{\n\t\tvar\n\t\t\tcolumns = settings.aoColumns,\n\t\t\tnames = _pluck( columns, 'sName' ),\n\t\t\tnodes = _pluck( columns, 'nTh' );\n\t\n\t\tvar run = function ( s ) {\n\t\t\tvar selInt = _intVal( s );\n\t\n\t\t\t// Selector - all\n\t\t\tif ( s === '' ) {\n\t\t\t\treturn _range( columns.length );\n\t\t\t}\n\t\n\t\t\t// Selector - index\n\t\t\tif ( selInt !== null ) {\n\t\t\t\treturn [ selInt >= 0 ?\n\t\t\t\t\tselInt : // Count from left\n\t\t\t\t\tcolumns.length + selInt // Count from right (+ because its a negative value)\n\t\t\t\t];\n\t\t\t}\n\t\n\t\t\t// Selector = function\n\t\t\tif ( typeof s === 'function' ) {\n\t\t\t\tvar rows = _selector_row_indexes( settings, opts );\n\t\n\t\t\t\treturn $.map( columns, function (col, idx) {\n\t\t\t\t\treturn s(\n\t\t\t\t\t\t\tidx,\n\t\t\t\t\t\t\t__columnData( settings, idx, 0, 0, rows ),\n\t\t\t\t\t\t\tnodes[ idx ]\n\t\t\t\t\t\t) ? idx : null;\n\t\t\t\t} );\n\t\t\t}\n\t\n\t\t\t// jQuery or string selector\n\t\t\tvar match = typeof s === 'string' ?\n\t\t\t\ts.match( __re_column_selector ) :\n\t\t\t\t'';\n\t\n\t\t\tif ( match ) {\n\t\t\t\tswitch( match[2] ) {\n\t\t\t\t\tcase 'visIdx':\n\t\t\t\t\tcase 'visible':\n\t\t\t\t\t\tvar idx = parseInt( match[1], 10 );\n\t\t\t\t\t\t// Visible index given, convert to column index\n\t\t\t\t\t\tif ( idx < 0 ) {\n\t\t\t\t\t\t\t// Counting from the right\n\t\t\t\t\t\t\tvar visColumns = $.map( columns, function (col,i) {\n\t\t\t\t\t\t\t\treturn col.bVisible ? i : null;\n\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t\treturn [ visColumns[ visColumns.length + idx ] ];\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Counting from the left\n\t\t\t\t\t\treturn [ _fnVisibleToColumnIndex( settings, idx ) ];\n\t\n\t\t\t\t\tcase 'name':\n\t\t\t\t\t\t// match by name. `names` is column index complete and in order\n\t\t\t\t\t\treturn $.map( names, function (name, i) {\n\t\t\t\t\t\t\treturn name === match[1] ? i : null;\n\t\t\t\t\t\t} );\n\t\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn [];\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t// Cell in the table body\n\t\t\tif ( s.nodeName && s._DT_CellIndex ) {\n\t\t\t\treturn [ s._DT_CellIndex.column ];\n\t\t\t}\n\t\n\t\t\t// jQuery selector on the TH elements for the columns\n\t\t\tvar jqResult = $( nodes )\n\t\t\t\t.filter( s )\n\t\t\t\t.map( function () {\n\t\t\t\t\treturn $.inArray( this, nodes ); // `nodes` is column index complete and in order\n\t\t\t\t} )\n\t\t\t\t.toArray();\n\t\n\t\t\tif ( jqResult.length || ! s.nodeName ) {\n\t\t\t\treturn jqResult;\n\t\t\t}\n\t\n\t\t\t// Otherwise a node which might have a `dt-column` data attribute, or be\n\t\t\t// a child or such an element\n\t\t\tvar host = $(s).closest('*[data-dt-column]');\n\t\t\treturn host.length ?\n\t\t\t\t[ host.data('dt-column') ] :\n\t\t\t\t[];\n\t\t};\n\t\n\t\treturn _selector_run( 'column', selector, run, settings, opts );\n\t};\n\t\n\t\n\tvar __setColumnVis = function ( settings, column, vis ) {\n\t\tvar\n\t\t\tcols = settings.aoColumns,\n\t\t\tcol  = cols[ column ],\n\t\t\tdata = settings.aoData,\n\t\t\trow, cells, i, ien, tr;\n\t\n\t\t// Get\n\t\tif ( vis === undefined ) {\n\t\t\treturn col.bVisible;\n\t\t}\n\t\n\t\t// Set\n\t\t// No change\n\t\tif ( col.bVisible === vis ) {\n\t\t\treturn;\n\t\t}\n\t\n\t\tif ( vis ) {\n\t\t\t// Insert column\n\t\t\t// Need to decide if we should use appendChild or insertBefore\n\t\t\tvar insertBefore = $.inArray( true, _pluck(cols, 'bVisible'), column+1 );\n\t\n\t\t\tfor ( i=0, ien=data.length ; i<ien ; i++ ) {\n\t\t\t\ttr = data[i].nTr;\n\t\t\t\tcells = data[i].anCells;\n\t\n\t\t\t\tif ( tr ) {\n\t\t\t\t\t// insertBefore can act like appendChild if 2nd arg is null\n\t\t\t\t\ttr.insertBefore( cells[ column ], cells[ insertBefore ] || null );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\t// Remove column\n\t\t\t$( _pluck( settings.aoData, 'anCells', column ) ).detach();\n\t\t}\n\t\n\t\t// Common actions\n\t\tcol.bVisible = vis;\n\t};\n\t\n\t\n\t_api_register( 'columns()', function ( selector, opts ) {\n\t\t// argument shifting\n\t\tif ( selector === undefined ) {\n\t\t\tselector = '';\n\t\t}\n\t\telse if ( $.isPlainObject( selector ) ) {\n\t\t\topts = selector;\n\t\t\tselector = '';\n\t\t}\n\t\n\t\topts = _selector_opts( opts );\n\t\n\t\tvar inst = this.iterator( 'table', function ( settings ) {\n\t\t\treturn __column_selector( settings, selector, opts );\n\t\t}, 1 );\n\t\n\t\t// Want argument shifting here and in _row_selector?\n\t\tinst.selector.cols = selector;\n\t\tinst.selector.opts = opts;\n\t\n\t\treturn inst;\n\t} );\n\t\n\t_api_registerPlural( 'columns().header()', 'column().header()', function ( selector, opts ) {\n\t\treturn this.iterator( 'column', function ( settings, column ) {\n\t\t\treturn settings.aoColumns[column].nTh;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().footer()', 'column().footer()', function ( selector, opts ) {\n\t\treturn this.iterator( 'column', function ( settings, column ) {\n\t\t\treturn settings.aoColumns[column].nTf;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().data()', 'column().data()', function () {\n\t\treturn this.iterator( 'column-rows', __columnData, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().dataSrc()', 'column().dataSrc()', function () {\n\t\treturn this.iterator( 'column', function ( settings, column ) {\n\t\t\treturn settings.aoColumns[column].mData;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().cache()', 'column().cache()', function ( type ) {\n\t\treturn this.iterator( 'column-rows', function ( settings, column, i, j, rows ) {\n\t\t\treturn _pluck_order( settings.aoData, rows,\n\t\t\t\ttype === 'search' ? '_aFilterData' : '_aSortData', column\n\t\t\t);\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().nodes()', 'column().nodes()', function () {\n\t\treturn this.iterator( 'column-rows', function ( settings, column, i, j, rows ) {\n\t\t\treturn _pluck_order( settings.aoData, rows, 'anCells', column ) ;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().visible()', 'column().visible()', function ( vis, calc ) {\n\t\tvar that = this;\n\t\tvar ret = this.iterator( 'column', function ( settings, column ) {\n\t\t\tif ( vis === undefined ) {\n\t\t\t\treturn settings.aoColumns[ column ].bVisible;\n\t\t\t} // else\n\t\t\t__setColumnVis( settings, column, vis );\n\t\t} );\n\t\n\t\t// Group the column visibility changes\n\t\tif ( vis !== undefined ) {\n\t\t\tthis.iterator( 'table', function ( settings ) {\n\t\t\t\t// Redraw the header after changes\n\t\t\t\t_fnDrawHead( settings, settings.aoHeader );\n\t\t\t\t_fnDrawHead( settings, settings.aoFooter );\n\t\t\n\t\t\t\t// Update colspan for no records display. Child rows and extensions will use their own\n\t\t\t\t// listeners to do this - only need to update the empty table item here\n\t\t\t\tif ( ! settings.aiDisplay.length ) {\n\t\t\t\t\t$(settings.nTBody).find('td[colspan]').attr('colspan', _fnVisbleColumns(settings));\n\t\t\t\t}\n\t\t\n\t\t\t\t_fnSaveState( settings );\n\t\n\t\t\t\t// Second loop once the first is done for events\n\t\t\t\tthat.iterator( 'column', function ( settings, column ) {\n\t\t\t\t\t_fnCallbackFire( settings, null, 'column-visibility', [settings, column, vis, calc] );\n\t\t\t\t} );\n\t\n\t\t\t\tif ( calc === undefined || calc ) {\n\t\t\t\t\tthat.columns.adjust();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\n\t\treturn ret;\n\t} );\n\t\n\t_api_registerPlural( 'columns().indexes()', 'column().index()', function ( type ) {\n\t\treturn this.iterator( 'column', function ( settings, column ) {\n\t\t\treturn type === 'visible' ?\n\t\t\t\t_fnColumnIndexToVisible( settings, column ) :\n\t\t\t\tcolumn;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_register( 'columns.adjust()', function () {\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t_fnAdjustColumnSizing( settings );\n\t\t}, 1 );\n\t} );\n\t\n\t_api_register( 'column.index()', function ( type, idx ) {\n\t\tif ( this.context.length !== 0 ) {\n\t\t\tvar ctx = this.context[0];\n\t\n\t\t\tif ( type === 'fromVisible' || type === 'toData' ) {\n\t\t\t\treturn _fnVisibleToColumnIndex( ctx, idx );\n\t\t\t}\n\t\t\telse if ( type === 'fromData' || type === 'toVisible' ) {\n\t\t\t\treturn _fnColumnIndexToVisible( ctx, idx );\n\t\t\t}\n\t\t}\n\t} );\n\t\n\t_api_register( 'column()', function ( selector, opts ) {\n\t\treturn _selector_first( this.columns( selector, opts ) );\n\t} );\n\t\n\t\n\t\n\tvar __cell_selector = function ( settings, selector, opts )\n\t{\n\t\tvar data = settings.aoData;\n\t\tvar rows = _selector_row_indexes( settings, opts );\n\t\tvar cells = _removeEmpty( _pluck_order( data, rows, 'anCells' ) );\n\t\tvar allCells = $( [].concat.apply([], cells) );\n\t\tvar row;\n\t\tvar columns = settings.aoColumns.length;\n\t\tvar a, i, ien, j, o, host;\n\t\n\t\tvar run = function ( s ) {\n\t\t\tvar fnSelector = typeof s === 'function';\n\t\n\t\t\tif ( s === null || s === undefined || fnSelector ) {\n\t\t\t\t// All cells and function selectors\n\t\t\t\ta = [];\n\t\n\t\t\t\tfor ( i=0, ien=rows.length ; i<ien ; i++ ) {\n\t\t\t\t\trow = rows[i];\n\t\n\t\t\t\t\tfor ( j=0 ; j<columns ; j++ ) {\n\t\t\t\t\t\to = {\n\t\t\t\t\t\t\trow: row,\n\t\t\t\t\t\t\tcolumn: j\n\t\t\t\t\t\t};\n\t\n\t\t\t\t\t\tif ( fnSelector ) {\n\t\t\t\t\t\t\t// Selector - function\n\t\t\t\t\t\t\thost = data[ row ];\n\t\n\t\t\t\t\t\t\tif ( s( o, _fnGetCellData(settings, row, j), host.anCells ? host.anCells[j] : null ) ) {\n\t\t\t\t\t\t\t\ta.push( o );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t// Selector - all\n\t\t\t\t\t\t\ta.push( o );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\treturn a;\n\t\t\t}\n\t\t\t\n\t\t\t// Selector - index\n\t\t\tif ( $.isPlainObject( s ) ) {\n\t\t\t\t// Valid cell index and its in the array of selectable rows\n\t\t\t\treturn s.column !== undefined && s.row !== undefined && $.inArray( s.row, rows ) !== -1 ?\n\t\t\t\t\t[s] :\n\t\t\t\t\t[];\n\t\t\t}\n\t\n\t\t\t// Selector - jQuery filtered cells\n\t\t\tvar jqResult = allCells\n\t\t\t\t.filter( s )\n\t\t\t\t.map( function (i, el) {\n\t\t\t\t\treturn { // use a new object, in case someone changes the values\n\t\t\t\t\t\trow:    el._DT_CellIndex.row,\n\t\t\t\t\t\tcolumn: el._DT_CellIndex.column\n\t \t\t\t\t};\n\t\t\t\t} )\n\t\t\t\t.toArray();\n\t\n\t\t\tif ( jqResult.length || ! s.nodeName ) {\n\t\t\t\treturn jqResult;\n\t\t\t}\n\t\n\t\t\t// Otherwise the selector is a node, and there is one last option - the\n\t\t\t// element might be a child of an element which has dt-row and dt-column\n\t\t\t// data attributes\n\t\t\thost = $(s).closest('*[data-dt-row]');\n\t\t\treturn host.length ?\n\t\t\t\t[ {\n\t\t\t\t\trow: host.data('dt-row'),\n\t\t\t\t\tcolumn: host.data('dt-column')\n\t\t\t\t} ] :\n\t\t\t\t[];\n\t\t};\n\t\n\t\treturn _selector_run( 'cell', selector, run, settings, opts );\n\t};\n\t\n\t\n\t\n\t\n\t_api_register( 'cells()', function ( rowSelector, columnSelector, opts ) {\n\t\t// Argument shifting\n\t\tif ( $.isPlainObject( rowSelector ) ) {\n\t\t\t// Indexes\n\t\t\tif ( rowSelector.row === undefined ) {\n\t\t\t\t// Selector options in first parameter\n\t\t\t\topts = rowSelector;\n\t\t\t\trowSelector = null;\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// Cell index objects in first parameter\n\t\t\t\topts = columnSelector;\n\t\t\t\tcolumnSelector = null;\n\t\t\t}\n\t\t}\n\t\tif ( $.isPlainObject( columnSelector ) ) {\n\t\t\topts = columnSelector;\n\t\t\tcolumnSelector = null;\n\t\t}\n\t\n\t\t// Cell selector\n\t\tif ( columnSelector === null || columnSelector === undefined ) {\n\t\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t\treturn __cell_selector( settings, rowSelector, _selector_opts( opts ) );\n\t\t\t} );\n\t\t}\n\t\n\t\t// The default built in options need to apply to row and columns\n\t\tvar internalOpts = opts ? {\n\t\t\tpage: opts.page,\n\t\t\torder: opts.order,\n\t\t\tsearch: opts.search\n\t\t} : {};\n\t\n\t\t// Row + column selector\n\t\tvar columns = this.columns( columnSelector, internalOpts );\n\t\tvar rows = this.rows( rowSelector, internalOpts );\n\t\tvar i, ien, j, jen;\n\t\n\t\tvar cellsNoOpts = this.iterator( 'table', function ( settings, idx ) {\n\t\t\tvar a = [];\n\t\n\t\t\tfor ( i=0, ien=rows[idx].length ; i<ien ; i++ ) {\n\t\t\t\tfor ( j=0, jen=columns[idx].length ; j<jen ; j++ ) {\n\t\t\t\t\ta.push( {\n\t\t\t\t\t\trow:    rows[idx][i],\n\t\t\t\t\t\tcolumn: columns[idx][j]\n\t\t\t\t\t} );\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\treturn a;\n\t\t}, 1 );\n\t\n\t\t// There is currently only one extension which uses a cell selector extension\n\t\t// It is a _major_ performance drag to run this if it isn't needed, so this is\n\t\t// an extension specific check at the moment\n\t\tvar cells = opts && opts.selected ?\n\t\t\tthis.cells( cellsNoOpts, opts ) :\n\t\t\tcellsNoOpts;\n\t\n\t\t$.extend( cells.selector, {\n\t\t\tcols: columnSelector,\n\t\t\trows: rowSelector,\n\t\t\topts: opts\n\t\t} );\n\t\n\t\treturn cells;\n\t} );\n\t\n\t\n\t_api_registerPlural( 'cells().nodes()', 'cell().node()', function () {\n\t\treturn this.iterator( 'cell', function ( settings, row, column ) {\n\t\t\tvar data = settings.aoData[ row ];\n\t\n\t\t\treturn data && data.anCells ?\n\t\t\t\tdata.anCells[ column ] :\n\t\t\t\tundefined;\n\t\t}, 1 );\n\t} );\n\t\n\t\n\t_api_register( 'cells().data()', function () {\n\t\treturn this.iterator( 'cell', function ( settings, row, column ) {\n\t\t\treturn _fnGetCellData( settings, row, column );\n\t\t}, 1 );\n\t} );\n\t\n\t\n\t_api_registerPlural( 'cells().cache()', 'cell().cache()', function ( type ) {\n\t\ttype = type === 'search' ? '_aFilterData' : '_aSortData';\n\t\n\t\treturn this.iterator( 'cell', function ( settings, row, column ) {\n\t\t\treturn settings.aoData[ row ][ type ][ column ];\n\t\t}, 1 );\n\t} );\n\t\n\t\n\t_api_registerPlural( 'cells().render()', 'cell().render()', function ( type ) {\n\t\treturn this.iterator( 'cell', function ( settings, row, column ) {\n\t\t\treturn _fnGetCellData( settings, row, column, type );\n\t\t}, 1 );\n\t} );\n\t\n\t\n\t_api_registerPlural( 'cells().indexes()', 'cell().index()', function () {\n\t\treturn this.iterator( 'cell', function ( settings, row, column ) {\n\t\t\treturn {\n\t\t\t\trow: row,\n\t\t\t\tcolumn: column,\n\t\t\t\tcolumnVisible: _fnColumnIndexToVisible( settings, column )\n\t\t\t};\n\t\t}, 1 );\n\t} );\n\t\n\t\n\t_api_registerPlural( 'cells().invalidate()', 'cell().invalidate()', function ( src ) {\n\t\treturn this.iterator( 'cell', function ( settings, row, column ) {\n\t\t\t_fnInvalidate( settings, row, src, column );\n\t\t} );\n\t} );\n\t\n\t\n\t\n\t_api_register( 'cell()', function ( rowSelector, columnSelector, opts ) {\n\t\treturn _selector_first( this.cells( rowSelector, columnSelector, opts ) );\n\t} );\n\t\n\t\n\t_api_register( 'cell().data()', function ( data ) {\n\t\tvar ctx = this.context;\n\t\tvar cell = this[0];\n\t\n\t\tif ( data === undefined ) {\n\t\t\t// Get\n\t\t\treturn ctx.length && cell.length ?\n\t\t\t\t_fnGetCellData( ctx[0], cell[0].row, cell[0].column ) :\n\t\t\t\tundefined;\n\t\t}\n\t\n\t\t// Set\n\t\t_fnSetCellData( ctx[0], cell[0].row, cell[0].column, data );\n\t\t_fnInvalidate( ctx[0], cell[0].row, 'data', cell[0].column );\n\t\n\t\treturn this;\n\t} );\n\t\n\t\n\t\n\t/**\n\t * Get current ordering (sorting) that has been applied to the table.\n\t *\n\t * @returns {array} 2D array containing the sorting information for the first\n\t *   table in the current context. Each element in the parent array represents\n\t *   a column being sorted upon (i.e. multi-sorting with two columns would have\n\t *   2 inner arrays). The inner arrays may have 2 or 3 elements. The first is\n\t *   the column index that the sorting condition applies to, the second is the\n\t *   direction of the sort (`desc` or `asc`) and, optionally, the third is the\n\t *   index of the sorting order from the `column.sorting` initialisation array.\n\t *//**\n\t * Set the ordering for the table.\n\t *\n\t * @param {integer} order Column index to sort upon.\n\t * @param {string} direction Direction of the sort to be applied (`asc` or `desc`)\n\t * @returns {DataTables.Api} this\n\t *//**\n\t * Set the ordering for the table.\n\t *\n\t * @param {array} order 1D array of sorting information to be applied.\n\t * @param {array} [...] Optional additional sorting conditions\n\t * @returns {DataTables.Api} this\n\t *//**\n\t * Set the ordering for the table.\n\t *\n\t * @param {array} order 2D array of sorting information to be applied.\n\t * @returns {DataTables.Api} this\n\t */\n\t_api_register( 'order()', function ( order, dir ) {\n\t\tvar ctx = this.context;\n\t\n\t\tif ( order === undefined ) {\n\t\t\t// get\n\t\t\treturn ctx.length !== 0 ?\n\t\t\t\tctx[0].aaSorting :\n\t\t\t\tundefined;\n\t\t}\n\t\n\t\t// set\n\t\tif ( typeof order === 'number' ) {\n\t\t\t// Simple column / direction passed in\n\t\t\torder = [ [ order, dir ] ];\n\t\t}\n\t\telse if ( order.length && ! $.isArray( order[0] ) ) {\n\t\t\t// Arguments passed in (list of 1D arrays)\n\t\t\torder = Array.prototype.slice.call( arguments );\n\t\t}\n\t\t// otherwise a 2D array was passed in\n\t\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\tsettings.aaSorting = order.slice();\n\t\t} );\n\t} );\n\t\n\t\n\t/**\n\t * Attach a sort listener to an element for a given column\n\t *\n\t * @param {node|jQuery|string} node Identifier for the element(s) to attach the\n\t *   listener to. This can take the form of a single DOM node, a jQuery\n\t *   collection of nodes or a jQuery selector which will identify the node(s).\n\t * @param {integer} column the column that a click on this node will sort on\n\t * @param {function} [callback] callback function when sort is run\n\t * @returns {DataTables.Api} this\n\t */\n\t_api_register( 'order.listener()', function ( node, column, callback ) {\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t_fnSortAttachListener( settings, node, column, callback );\n\t\t} );\n\t} );\n\t\n\t\n\t_api_register( 'order.fixed()', function ( set ) {\n\t\tif ( ! set ) {\n\t\t\tvar ctx = this.context;\n\t\t\tvar fixed = ctx.length ?\n\t\t\t\tctx[0].aaSortingFixed :\n\t\t\t\tundefined;\n\t\n\t\t\treturn $.isArray( fixed ) ?\n\t\t\t\t{ pre: fixed } :\n\t\t\t\tfixed;\n\t\t}\n\t\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\tsettings.aaSortingFixed = $.extend( true, {}, set );\n\t\t} );\n\t} );\n\t\n\t\n\t// Order by the selected column(s)\n\t_api_register( [\n\t\t'columns().order()',\n\t\t'column().order()'\n\t], function ( dir ) {\n\t\tvar that = this;\n\t\n\t\treturn this.iterator( 'table', function ( settings, i ) {\n\t\t\tvar sort = [];\n\t\n\t\t\t$.each( that[i], function (j, col) {\n\t\t\t\tsort.push( [ col, dir ] );\n\t\t\t} );\n\t\n\t\t\tsettings.aaSorting = sort;\n\t\t} );\n\t} );\n\t\n\t\n\t\n\t_api_register( 'search()', function ( input, regex, smart, caseInsen ) {\n\t\tvar ctx = this.context;\n\t\n\t\tif ( input === undefined ) {\n\t\t\t// get\n\t\t\treturn ctx.length !== 0 ?\n\t\t\t\tctx[0].oPreviousSearch.sSearch :\n\t\t\t\tundefined;\n\t\t}\n\t\n\t\t// set\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\tif ( ! settings.oFeatures.bFilter ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\n\t\t\t_fnFilterComplete( settings, $.extend( {}, settings.oPreviousSearch, {\n\t\t\t\t\"sSearch\": input+\"\",\n\t\t\t\t\"bRegex\":  regex === null ? false : regex,\n\t\t\t\t\"bSmart\":  smart === null ? true  : smart,\n\t\t\t\t\"bCaseInsensitive\": caseInsen === null ? true : caseInsen\n\t\t\t} ), 1 );\n\t\t} );\n\t} );\n\t\n\t\n\t_api_registerPlural(\n\t\t'columns().search()',\n\t\t'column().search()',\n\t\tfunction ( input, regex, smart, caseInsen ) {\n\t\t\treturn this.iterator( 'column', function ( settings, column ) {\n\t\t\t\tvar preSearch = settings.aoPreSearchCols;\n\t\n\t\t\t\tif ( input === undefined ) {\n\t\t\t\t\t// get\n\t\t\t\t\treturn preSearch[ column ].sSearch;\n\t\t\t\t}\n\t\n\t\t\t\t// set\n\t\t\t\tif ( ! settings.oFeatures.bFilter ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\n\t\t\t\t$.extend( preSearch[ column ], {\n\t\t\t\t\t\"sSearch\": input+\"\",\n\t\t\t\t\t\"bRegex\":  regex === null ? false : regex,\n\t\t\t\t\t\"bSmart\":  smart === null ? true  : smart,\n\t\t\t\t\t\"bCaseInsensitive\": caseInsen === null ? true : caseInsen\n\t\t\t\t} );\n\t\n\t\t\t\t_fnFilterComplete( settings, settings.oPreviousSearch, 1 );\n\t\t\t} );\n\t\t}\n\t);\n\t\n\t/*\n\t * State API methods\n\t */\n\t\n\t_api_register( 'state()', function () {\n\t\treturn this.context.length ?\n\t\t\tthis.context[0].oSavedState :\n\t\t\tnull;\n\t} );\n\t\n\t\n\t_api_register( 'state.clear()', function () {\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t// Save an empty object\n\t\t\tsettings.fnStateSaveCallback.call( settings.oInstance, settings, {} );\n\t\t} );\n\t} );\n\t\n\t\n\t_api_register( 'state.loaded()', function () {\n\t\treturn this.context.length ?\n\t\t\tthis.context[0].oLoadedState :\n\t\t\tnull;\n\t} );\n\t\n\t\n\t_api_register( 'state.save()', function () {\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t_fnSaveState( settings );\n\t\t} );\n\t} );\n\t\n\t\n\t\n\t/**\n\t * Provide a common method for plug-ins to check the version of DataTables being\n\t * used, in order to ensure compatibility.\n\t *\n\t *  @param {string} version Version string to check for, in the format \"X.Y.Z\".\n\t *    Note that the formats \"X\" and \"X.Y\" are also acceptable.\n\t *  @returns {boolean} true if this version of DataTables is greater or equal to\n\t *    the required version, or false if this version of DataTales is not\n\t *    suitable\n\t *  @static\n\t *  @dtopt API-Static\n\t *\n\t *  @example\n\t *    alert( $.fn.dataTable.versionCheck( '1.9.0' ) );\n\t */\n\tDataTable.versionCheck = DataTable.fnVersionCheck = function( version )\n\t{\n\t\tvar aThis = DataTable.version.split('.');\n\t\tvar aThat = version.split('.');\n\t\tvar iThis, iThat;\n\t\n\t\tfor ( var i=0, iLen=aThat.length ; i<iLen ; i++ ) {\n\t\t\tiThis = parseInt( aThis[i], 10 ) || 0;\n\t\t\tiThat = parseInt( aThat[i], 10 ) || 0;\n\t\n\t\t\t// Parts are the same, keep comparing\n\t\t\tif (iThis === iThat) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\n\t\t\t// Parts are different, return immediately\n\t\t\treturn iThis > iThat;\n\t\t}\n\t\n\t\treturn true;\n\t};\n\t\n\t\n\t/**\n\t * Check if a `<table>` node is a DataTable table already or not.\n\t *\n\t *  @param {node|jquery|string} table Table node, jQuery object or jQuery\n\t *      selector for the table to test. Note that if more than more than one\n\t *      table is passed on, only the first will be checked\n\t *  @returns {boolean} true the table given is a DataTable, or false otherwise\n\t *  @static\n\t *  @dtopt API-Static\n\t *\n\t *  @example\n\t *    if ( ! $.fn.DataTable.isDataTable( '#example' ) ) {\n\t *      $('#example').dataTable();\n\t *    }\n\t */\n\tDataTable.isDataTable = DataTable.fnIsDataTable = function ( table )\n\t{\n\t\tvar t = $(table).get(0);\n\t\tvar is = false;\n\t\n\t\tif ( table instanceof DataTable.Api ) {\n\t\t\treturn true;\n\t\t}\n\t\n\t\t$.each( DataTable.settings, function (i, o) {\n\t\t\tvar head = o.nScrollHead ? $('table', o.nScrollHead)[0] : null;\n\t\t\tvar foot = o.nScrollFoot ? $('table', o.nScrollFoot)[0] : null;\n\t\n\t\t\tif ( o.nTable === t || head === t || foot === t ) {\n\t\t\t\tis = true;\n\t\t\t}\n\t\t} );\n\t\n\t\treturn is;\n\t};\n\t\n\t\n\t/**\n\t * Get all DataTable tables that have been initialised - optionally you can\n\t * select to get only currently visible tables.\n\t *\n\t *  @param {boolean} [visible=false] Flag to indicate if you want all (default)\n\t *    or visible tables only.\n\t *  @returns {array} Array of `table` nodes (not DataTable instances) which are\n\t *    DataTables\n\t *  @static\n\t *  @dtopt API-Static\n\t *\n\t *  @example\n\t *    $.each( $.fn.dataTable.tables(true), function () {\n\t *      $(table).DataTable().columns.adjust();\n\t *    } );\n\t */\n\tDataTable.tables = DataTable.fnTables = function ( visible )\n\t{\n\t\tvar api = false;\n\t\n\t\tif ( $.isPlainObject( visible ) ) {\n\t\t\tapi = visible.api;\n\t\t\tvisible = visible.visible;\n\t\t}\n\t\n\t\tvar a = $.map( DataTable.settings, function (o) {\n\t\t\tif ( !visible || (visible && $(o.nTable).is(':visible')) ) {\n\t\t\t\treturn o.nTable;\n\t\t\t}\n\t\t} );\n\t\n\t\treturn api ?\n\t\t\tnew _Api( a ) :\n\t\t\ta;\n\t};\n\t\n\t\n\t/**\n\t * Convert from camel case parameters to Hungarian notation. This is made public\n\t * for the extensions to provide the same ability as DataTables core to accept\n\t * either the 1.9 style Hungarian notation, or the 1.10+ style camelCase\n\t * parameters.\n\t *\n\t *  @param {object} src The model object which holds all parameters that can be\n\t *    mapped.\n\t *  @param {object} user The object to convert from camel case to Hungarian.\n\t *  @param {boolean} force When set to `true`, properties which already have a\n\t *    Hungarian value in the `user` object will be overwritten. Otherwise they\n\t *    won't be.\n\t */\n\tDataTable.camelToHungarian = _fnCamelToHungarian;\n\t\n\t\n\t\n\t/**\n\t *\n\t */\n\t_api_register( '$()', function ( selector, opts ) {\n\t\tvar\n\t\t\trows   = this.rows( opts ).nodes(), // Get all rows\n\t\t\tjqRows = $(rows);\n\t\n\t\treturn $( [].concat(\n\t\t\tjqRows.filter( selector ).toArray(),\n\t\t\tjqRows.find( selector ).toArray()\n\t\t) );\n\t} );\n\t\n\t\n\t// jQuery functions to operate on the tables\n\t$.each( [ 'on', 'one', 'off' ], function (i, key) {\n\t\t_api_register( key+'()', function ( /* event, handler */ ) {\n\t\t\tvar args = Array.prototype.slice.call(arguments);\n\t\n\t\t\t// Add the `dt` namespace automatically if it isn't already present\n\t\t\targs[0] = $.map( args[0].split( /\\s/ ), function ( e ) {\n\t\t\t\treturn ! e.match(/\\.dt\\b/) ?\n\t\t\t\t\te+'.dt' :\n\t\t\t\t\te;\n\t\t\t\t} ).join( ' ' );\n\t\n\t\t\tvar inst = $( this.tables().nodes() );\n\t\t\tinst[key].apply( inst, args );\n\t\t\treturn this;\n\t\t} );\n\t} );\n\t\n\t\n\t_api_register( 'clear()', function () {\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t_fnClearTable( settings );\n\t\t} );\n\t} );\n\t\n\t\n\t_api_register( 'settings()', function () {\n\t\treturn new _Api( this.context, this.context );\n\t} );\n\t\n\t\n\t_api_register( 'init()', function () {\n\t\tvar ctx = this.context;\n\t\treturn ctx.length ? ctx[0].oInit : null;\n\t} );\n\t\n\t\n\t_api_register( 'data()', function () {\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\treturn _pluck( settings.aoData, '_aData' );\n\t\t} ).flatten();\n\t} );\n\t\n\t\n\t_api_register( 'destroy()', function ( remove ) {\n\t\tremove = remove || false;\n\t\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\tvar orig      = settings.nTableWrapper.parentNode;\n\t\t\tvar classes   = settings.oClasses;\n\t\t\tvar table     = settings.nTable;\n\t\t\tvar tbody     = settings.nTBody;\n\t\t\tvar thead     = settings.nTHead;\n\t\t\tvar tfoot     = settings.nTFoot;\n\t\t\tvar jqTable   = $(table);\n\t\t\tvar jqTbody   = $(tbody);\n\t\t\tvar jqWrapper = $(settings.nTableWrapper);\n\t\t\tvar rows      = $.map( settings.aoData, function (r) { return r.nTr; } );\n\t\t\tvar i, ien;\n\t\n\t\t\t// Flag to note that the table is currently being destroyed - no action\n\t\t\t// should be taken\n\t\t\tsettings.bDestroying = true;\n\t\n\t\t\t// Fire off the destroy callbacks for plug-ins etc\n\t\t\t_fnCallbackFire( settings, \"aoDestroyCallback\", \"destroy\", [settings] );\n\t\n\t\t\t// If not being removed from the document, make all columns visible\n\t\t\tif ( ! remove ) {\n\t\t\t\tnew _Api( settings ).columns().visible( true );\n\t\t\t}\n\t\n\t\t\t// Blitz all `DT` namespaced events (these are internal events, the\n\t\t\t// lowercase, `dt` events are user subscribed and they are responsible\n\t\t\t// for removing them\n\t\t\tjqWrapper.off('.DT').find(':not(tbody *)').off('.DT');\n\t\t\t$(window).off('.DT-'+settings.sInstance);\n\t\n\t\t\t// When scrolling we had to break the table up - restore it\n\t\t\tif ( table != thead.parentNode ) {\n\t\t\t\tjqTable.children('thead').detach();\n\t\t\t\tjqTable.append( thead );\n\t\t\t}\n\t\n\t\t\tif ( tfoot && table != tfoot.parentNode ) {\n\t\t\t\tjqTable.children('tfoot').detach();\n\t\t\t\tjqTable.append( tfoot );\n\t\t\t}\n\t\n\t\t\tsettings.aaSorting = [];\n\t\t\tsettings.aaSortingFixed = [];\n\t\t\t_fnSortingClasses( settings );\n\t\n\t\t\t$( rows ).removeClass( settings.asStripeClasses.join(' ') );\n\t\n\t\t\t$('th, td', thead).removeClass( classes.sSortable+' '+\n\t\t\t\tclasses.sSortableAsc+' '+classes.sSortableDesc+' '+classes.sSortableNone\n\t\t\t);\n\t\n\t\t\t// Add the TR elements back into the table in their original order\n\t\t\tjqTbody.children().detach();\n\t\t\tjqTbody.append( rows );\n\t\n\t\t\t// Remove the DataTables generated nodes, events and classes\n\t\t\tvar removedMethod = remove ? 'remove' : 'detach';\n\t\t\tjqTable[ removedMethod ]();\n\t\t\tjqWrapper[ removedMethod ]();\n\t\n\t\t\t// If we need to reattach the table to the document\n\t\t\tif ( ! remove && orig ) {\n\t\t\t\t// insertBefore acts like appendChild if !arg[1]\n\t\t\t\torig.insertBefore( table, settings.nTableReinsertBefore );\n\t\n\t\t\t\t// Restore the width of the original table - was read from the style property,\n\t\t\t\t// so we can restore directly to that\n\t\t\t\tjqTable\n\t\t\t\t\t.css( 'width', settings.sDestroyWidth )\n\t\t\t\t\t.removeClass( classes.sTable );\n\t\n\t\t\t\t// If the were originally stripe classes - then we add them back here.\n\t\t\t\t// Note this is not fool proof (for example if not all rows had stripe\n\t\t\t\t// classes - but it's a good effort without getting carried away\n\t\t\t\tien = settings.asDestroyStripes.length;\n\t\n\t\t\t\tif ( ien ) {\n\t\t\t\t\tjqTbody.children().each( function (i) {\n\t\t\t\t\t\t$(this).addClass( settings.asDestroyStripes[i % ien] );\n\t\t\t\t\t} );\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t/* Remove the settings object from the settings array */\n\t\t\tvar idx = $.inArray( settings, DataTable.settings );\n\t\t\tif ( idx !== -1 ) {\n\t\t\t\tDataTable.settings.splice( idx, 1 );\n\t\t\t}\n\t\t} );\n\t} );\n\t\n\t\n\t// Add the `every()` method for rows, columns and cells in a compact form\n\t$.each( [ 'column', 'row', 'cell' ], function ( i, type ) {\n\t\t_api_register( type+'s().every()', function ( fn ) {\n\t\t\tvar opts = this.selector.opts;\n\t\t\tvar api = this;\n\t\n\t\t\treturn this.iterator( type, function ( settings, arg1, arg2, arg3, arg4 ) {\n\t\t\t\t// Rows and columns:\n\t\t\t\t//  arg1 - index\n\t\t\t\t//  arg2 - table counter\n\t\t\t\t//  arg3 - loop counter\n\t\t\t\t//  arg4 - undefined\n\t\t\t\t// Cells:\n\t\t\t\t//  arg1 - row index\n\t\t\t\t//  arg2 - column index\n\t\t\t\t//  arg3 - table counter\n\t\t\t\t//  arg4 - loop counter\n\t\t\t\tfn.call(\n\t\t\t\t\tapi[ type ](\n\t\t\t\t\t\targ1,\n\t\t\t\t\t\ttype==='cell' ? arg2 : opts,\n\t\t\t\t\t\ttype==='cell' ? opts : undefined\n\t\t\t\t\t),\n\t\t\t\t\targ1, arg2, arg3, arg4\n\t\t\t\t);\n\t\t\t} );\n\t\t} );\n\t} );\n\t\n\t\n\t// i18n method for extensions to be able to use the language object from the\n\t// DataTable\n\t_api_register( 'i18n()', function ( token, def, plural ) {\n\t\tvar ctx = this.context[0];\n\t\tvar resolved = _fnGetObjectDataFn( token )( ctx.oLanguage );\n\t\n\t\tif ( resolved === undefined ) {\n\t\t\tresolved = def;\n\t\t}\n\t\n\t\tif ( plural !== undefined && $.isPlainObject( resolved ) ) {\n\t\t\tresolved = resolved[ plural ] !== undefined ?\n\t\t\t\tresolved[ plural ] :\n\t\t\t\tresolved._;\n\t\t}\n\t\n\t\treturn resolved.replace( '%d', plural ); // nb: plural might be undefined,\n\t} );\n\t/**\n\t * Version string for plug-ins to check compatibility. Allowed format is\n\t * `a.b.c-d` where: a:int, b:int, c:int, d:string(dev|beta|alpha). `d` is used\n\t * only for non-release builds. See http://semver.org/ for more information.\n\t *  @member\n\t *  @type string\n\t *  @default Version number\n\t */\n\tDataTable.version = \"1.10.21\";\n\n\t/**\n\t * Private data store, containing all of the settings objects that are\n\t * created for the tables on a given page.\n\t *\n\t * Note that the `DataTable.settings` object is aliased to\n\t * `jQuery.fn.dataTableExt` through which it may be accessed and\n\t * manipulated, or `jQuery.fn.dataTable.settings`.\n\t *  @member\n\t *  @type array\n\t *  @default []\n\t *  @private\n\t */\n\tDataTable.settings = [];\n\n\t/**\n\t * Object models container, for the various models that DataTables has\n\t * available to it. These models define the objects that are used to hold\n\t * the active state and configuration of the table.\n\t *  @namespace\n\t */\n\tDataTable.models = {};\n\t\n\t\n\t\n\t/**\n\t * Template object for the way in which DataTables holds information about\n\t * search information for the global filter and individual column filters.\n\t *  @namespace\n\t */\n\tDataTable.models.oSearch = {\n\t\t/**\n\t\t * Flag to indicate if the filtering should be case insensitive or not\n\t\t *  @type boolean\n\t\t *  @default true\n\t\t */\n\t\t\"bCaseInsensitive\": true,\n\t\n\t\t/**\n\t\t * Applied search term\n\t\t *  @type string\n\t\t *  @default <i>Empty string</i>\n\t\t */\n\t\t\"sSearch\": \"\",\n\t\n\t\t/**\n\t\t * Flag to indicate if the search term should be interpreted as a\n\t\t * regular expression (true) or not (false) and therefore and special\n\t\t * regex characters escaped.\n\t\t *  @type boolean\n\t\t *  @default false\n\t\t */\n\t\t\"bRegex\": false,\n\t\n\t\t/**\n\t\t * Flag to indicate if DataTables is to use its smart filtering or not.\n\t\t *  @type boolean\n\t\t *  @default true\n\t\t */\n\t\t\"bSmart\": true\n\t};\n\t\n\t\n\t\n\t\n\t/**\n\t * Template object for the way in which DataTables holds information about\n\t * each individual row. This is the object format used for the settings\n\t * aoData array.\n\t *  @namespace\n\t */\n\tDataTable.models.oRow = {\n\t\t/**\n\t\t * TR element for the row\n\t\t *  @type node\n\t\t *  @default null\n\t\t */\n\t\t\"nTr\": null,\n\t\n\t\t/**\n\t\t * Array of TD elements for each row. This is null until the row has been\n\t\t * created.\n\t\t *  @type array nodes\n\t\t *  @default []\n\t\t */\n\t\t\"anCells\": null,\n\t\n\t\t/**\n\t\t * Data object from the original data source for the row. This is either\n\t\t * an array if using the traditional form of DataTables, or an object if\n\t\t * using mData options. The exact type will depend on the passed in\n\t\t * data from the data source, or will be an array if using DOM a data\n\t\t * source.\n\t\t *  @type array|object\n\t\t *  @default []\n\t\t */\n\t\t\"_aData\": [],\n\t\n\t\t/**\n\t\t * Sorting data cache - this array is ostensibly the same length as the\n\t\t * number of columns (although each index is generated only as it is\n\t\t * needed), and holds the data that is used for sorting each column in the\n\t\t * row. We do this cache generation at the start of the sort in order that\n\t\t * the formatting of the sort data need be done only once for each cell\n\t\t * per sort. This array should not be read from or written to by anything\n\t\t * other than the master sorting methods.\n\t\t *  @type array\n\t\t *  @default null\n\t\t *  @private\n\t\t */\n\t\t\"_aSortData\": null,\n\t\n\t\t/**\n\t\t * Per cell filtering data cache. As per the sort data cache, used to\n\t\t * increase the performance of the filtering in DataTables\n\t\t *  @type array\n\t\t *  @default null\n\t\t *  @private\n\t\t */\n\t\t\"_aFilterData\": null,\n\t\n\t\t/**\n\t\t * Filtering data cache. This is the same as the cell filtering cache, but\n\t\t * in this case a string rather than an array. This is easily computed with\n\t\t * a join on `_aFilterData`, but is provided as a cache so the join isn't\n\t\t * needed on every search (memory traded for performance)\n\t\t *  @type array\n\t\t *  @default null\n\t\t *  @private\n\t\t */\n\t\t\"_sFilterRow\": null,\n\t\n\t\t/**\n\t\t * Cache of the class name that DataTables has applied to the row, so we\n\t\t * can quickly look at this variable rather than needing to do a DOM check\n\t\t * on className for the nTr property.\n\t\t *  @type string\n\t\t *  @default <i>Empty string</i>\n\t\t *  @private\n\t\t */\n\t\t\"_sRowStripe\": \"\",\n\t\n\t\t/**\n\t\t * Denote if the original data source was from the DOM, or the data source\n\t\t * object. This is used for invalidating data, so DataTables can\n\t\t * automatically read data from the original source, unless uninstructed\n\t\t * otherwise.\n\t\t *  @type string\n\t\t *  @default null\n\t\t *  @private\n\t\t */\n\t\t\"src\": null,\n\t\n\t\t/**\n\t\t * Index in the aoData array. This saves an indexOf lookup when we have the\n\t\t * object, but want to know the index\n\t\t *  @type integer\n\t\t *  @default -1\n\t\t *  @private\n\t\t */\n\t\t\"idx\": -1\n\t};\n\t\n\t\n\t/**\n\t * Template object for the column information object in DataTables. This object\n\t * is held in the settings aoColumns array and contains all the information that\n\t * DataTables needs about each individual column.\n\t *\n\t * Note that this object is related to {@link DataTable.defaults.column}\n\t * but this one is the internal data store for DataTables's cache of columns.\n\t * It should NOT be manipulated outside of DataTables. Any configuration should\n\t * be done through the initialisation options.\n\t *  @namespace\n\t */\n\tDataTable.models.oColumn = {\n\t\t/**\n\t\t * Column index. This could be worked out on-the-fly with $.inArray, but it\n\t\t * is faster to just hold it as a variable\n\t\t *  @type integer\n\t\t *  @default null\n\t\t */\n\t\t\"idx\": null,\n\t\n\t\t/**\n\t\t * A list of the columns that sorting should occur on when this column\n\t\t * is sorted. That this property is an array allows multi-column sorting\n\t\t * to be defined for a column (for example first name / last name columns\n\t\t * would benefit from this). The values are integers pointing to the\n\t\t * columns to be sorted on (typically it will be a single integer pointing\n\t\t * at itself, but that doesn't need to be the case).\n\t\t *  @type array\n\t\t */\n\t\t\"aDataSort\": null,\n\t\n\t\t/**\n\t\t * Define the sorting directions that are applied to the column, in sequence\n\t\t * as the column is repeatedly sorted upon - i.e. the first value is used\n\t\t * as the sorting direction when the column if first sorted (clicked on).\n\t\t * Sort it again (click again) and it will move on to the next index.\n\t\t * Repeat until loop.\n\t\t *  @type array\n\t\t */\n\t\t\"asSorting\": null,\n\t\n\t\t/**\n\t\t * Flag to indicate if the column is searchable, and thus should be included\n\t\t * in the filtering or not.\n\t\t *  @type boolean\n\t\t */\n\t\t\"bSearchable\": null,\n\t\n\t\t/**\n\t\t * Flag to indicate if the column is sortable or not.\n\t\t *  @type boolean\n\t\t */\n\t\t\"bSortable\": null,\n\t\n\t\t/**\n\t\t * Flag to indicate if the column is currently visible in the table or not\n\t\t *  @type boolean\n\t\t */\n\t\t\"bVisible\": null,\n\t\n\t\t/**\n\t\t * Store for manual type assignment using the `column.type` option. This\n\t\t * is held in store so we can manipulate the column's `sType` property.\n\t\t *  @type string\n\t\t *  @default null\n\t\t *  @private\n\t\t */\n\t\t\"_sManualType\": null,\n\t\n\t\t/**\n\t\t * Flag to indicate if HTML5 data attributes should be used as the data\n\t\t * source for filtering or sorting. True is either are.\n\t\t *  @type boolean\n\t\t *  @default false\n\t\t *  @private\n\t\t */\n\t\t\"_bAttrSrc\": false,\n\t\n\t\t/**\n\t\t * Developer definable function that is called whenever a cell is created (Ajax source,\n\t\t * etc) or processed for input (DOM source). This can be used as a compliment to mRender\n\t\t * allowing you to modify the DOM element (add background colour for example) when the\n\t\t * element is available.\n\t\t *  @type function\n\t\t *  @param {element} nTd The TD node that has been created\n\t\t *  @param {*} sData The Data for the cell\n\t\t *  @param {array|object} oData The data for the whole row\n\t\t *  @param {int} iRow The row index for the aoData data store\n\t\t *  @default null\n\t\t */\n\t\t\"fnCreatedCell\": null,\n\t\n\t\t/**\n\t\t * Function to get data from a cell in a column. You should <b>never</b>\n\t\t * access data directly through _aData internally in DataTables - always use\n\t\t * the method attached to this property. It allows mData to function as\n\t\t * required. This function is automatically assigned by the column\n\t\t * initialisation method\n\t\t *  @type function\n\t\t *  @param {array|object} oData The data array/object for the array\n\t\t *    (i.e. aoData[]._aData)\n\t\t *  @param {string} sSpecific The specific data type you want to get -\n\t\t *    'display', 'type' 'filter' 'sort'\n\t\t *  @returns {*} The data for the cell from the given row's data\n\t\t *  @default null\n\t\t */\n\t\t\"fnGetData\": null,\n\t\n\t\t/**\n\t\t * Function to set data for a cell in the column. You should <b>never</b>\n\t\t * set the data directly to _aData internally in DataTables - always use\n\t\t * this method. It allows mData to function as required. This function\n\t\t * is automatically assigned by the column initialisation method\n\t\t *  @type function\n\t\t *  @param {array|object} oData The data array/object for the array\n\t\t *    (i.e. aoData[]._aData)\n\t\t *  @param {*} sValue Value to set\n\t\t *  @default null\n\t\t */\n\t\t\"fnSetData\": null,\n\t\n\t\t/**\n\t\t * Property to read the value for the cells in the column from the data\n\t\t * source array / object. If null, then the default content is used, if a\n\t\t * function is given then the return from the function is used.\n\t\t *  @type function|int|string|null\n\t\t *  @default null\n\t\t */\n\t\t\"mData\": null,\n\t\n\t\t/**\n\t\t * Partner property to mData which is used (only when defined) to get\n\t\t * the data - i.e. it is basically the same as mData, but without the\n\t\t * 'set' option, and also the data fed to it is the result from mData.\n\t\t * This is the rendering method to match the data method of mData.\n\t\t *  @type function|int|string|null\n\t\t *  @default null\n\t\t */\n\t\t\"mRender\": null,\n\t\n\t\t/**\n\t\t * Unique header TH/TD element for this column - this is what the sorting\n\t\t * listener is attached to (if sorting is enabled.)\n\t\t *  @type node\n\t\t *  @default null\n\t\t */\n\t\t\"nTh\": null,\n\t\n\t\t/**\n\t\t * Unique footer TH/TD element for this column (if there is one). Not used\n\t\t * in DataTables as such, but can be used for plug-ins to reference the\n\t\t * footer for each column.\n\t\t *  @type node\n\t\t *  @default null\n\t\t */\n\t\t\"nTf\": null,\n\t\n\t\t/**\n\t\t * The class to apply to all TD elements in the table's TBODY for the column\n\t\t *  @type string\n\t\t *  @default null\n\t\t */\n\t\t\"sClass\": null,\n\t\n\t\t/**\n\t\t * When DataTables calculates the column widths to assign to each column,\n\t\t * it finds the longest string in each column and then constructs a\n\t\t * temporary table and reads the widths from that. The problem with this\n\t\t * is that \"mmm\" is much wider then \"iiii\", but the latter is a longer\n\t\t * string - thus the calculation can go wrong (doing it properly and putting\n\t\t * it into an DOM object and measuring that is horribly(!) slow). Thus as\n\t\t * a \"work around\" we provide this option. It will append its value to the\n\t\t * text that is found to be the longest string for the column - i.e. padding.\n\t\t *  @type string\n\t\t */\n\t\t\"sContentPadding\": null,\n\t\n\t\t/**\n\t\t * Allows a default value to be given for a column's data, and will be used\n\t\t * whenever a null data source is encountered (this can be because mData\n\t\t * is set to null, or because the data source itself is null).\n\t\t *  @type string\n\t\t *  @default null\n\t\t */\n\t\t\"sDefaultContent\": null,\n\t\n\t\t/**\n\t\t * Name for the column, allowing reference to the column by name as well as\n\t\t * by index (needs a lookup to work by name).\n\t\t *  @type string\n\t\t */\n\t\t\"sName\": null,\n\t\n\t\t/**\n\t\t * Custom sorting data type - defines which of the available plug-ins in\n\t\t * afnSortData the custom sorting will use - if any is defined.\n\t\t *  @type string\n\t\t *  @default std\n\t\t */\n\t\t\"sSortDataType\": 'std',\n\t\n\t\t/**\n\t\t * Class to be applied to the header element when sorting on this column\n\t\t *  @type string\n\t\t *  @default null\n\t\t */\n\t\t\"sSortingClass\": null,\n\t\n\t\t/**\n\t\t * Class to be applied to the header element when sorting on this column -\n\t\t * when jQuery UI theming is used.\n\t\t *  @type string\n\t\t *  @default null\n\t\t */\n\t\t\"sSortingClassJUI\": null,\n\t\n\t\t/**\n\t\t * Title of the column - what is seen in the TH element (nTh).\n\t\t *  @type string\n\t\t */\n\t\t\"sTitle\": null,\n\t\n\t\t/**\n\t\t * Column sorting and filtering type\n\t\t *  @type string\n\t\t *  @default null\n\t\t */\n\t\t\"sType\": null,\n\t\n\t\t/**\n\t\t * Width of the column\n\t\t *  @type string\n\t\t *  @default null\n\t\t */\n\t\t\"sWidth\": null,\n\t\n\t\t/**\n\t\t * Width of the column when it was first \"encountered\"\n\t\t *  @type string\n\t\t *  @default null\n\t\t */\n\t\t\"sWidthOrig\": null\n\t};\n\t\n\t\n\t/*\n\t * Developer note: The properties of the object below are given in Hungarian\n\t * notation, that was used as the interface for DataTables prior to v1.10, however\n\t * from v1.10 onwards the primary interface is camel case. In order to avoid\n\t * breaking backwards compatibility utterly with this change, the Hungarian\n\t * version is still, internally the primary interface, but is is not documented\n\t * - hence the @name tags in each doc comment. This allows a Javascript function\n\t * to create a map from Hungarian notation to camel case (going the other direction\n\t * would require each property to be listed, which would at around 3K to the size\n\t * of DataTables, while this method is about a 0.5K hit.\n\t *\n\t * Ultimately this does pave the way for Hungarian notation to be dropped\n\t * completely, but that is a massive amount of work and will break current\n\t * installs (therefore is on-hold until v2).\n\t */\n\t\n\t/**\n\t * Initialisation options that can be given to DataTables at initialisation\n\t * time.\n\t *  @namespace\n\t */\n\tDataTable.defaults = {\n\t\t/**\n\t\t * An array of data to use for the table, passed in at initialisation which\n\t\t * will be used in preference to any data which is already in the DOM. This is\n\t\t * particularly useful for constructing tables purely in Javascript, for\n\t\t * example with a custom Ajax call.\n\t\t *  @type array\n\t\t *  @default null\n\t\t *\n\t\t *  @dtopt Option\n\t\t *  @name DataTable.defaults.data\n\t\t *\n\t\t *  @example\n\t\t *    // Using a 2D array data source\n\t\t *    $(document).ready( function () {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"data\": [\n\t\t *          ['Trident', 'Internet Explorer 4.0', 'Win 95+', 4, 'X'],\n\t\t *          ['Trident', 'Internet Explorer 5.0', 'Win 95+', 5, 'C'],\n\t\t *        ],\n\t\t *        \"columns\": [\n\t\t *          { \"title\": \"Engine\" },\n\t\t *          { \"title\": \"Browser\" },\n\t\t *          { \"title\": \"Platform\" },\n\t\t *          { \"title\": \"Version\" },\n\t\t *          { \"title\": \"Grade\" }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Using an array of objects as a data source (`data`)\n\t\t *    $(document).ready( function () {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"data\": [\n\t\t *          {\n\t\t *            \"engine\":   \"Trident\",\n\t\t *            \"browser\":  \"Internet Explorer 4.0\",\n\t\t *            \"platform\": \"Win 95+\",\n\t\t *            \"version\":  4,\n\t\t *            \"grade\":    \"X\"\n\t\t *          },\n\t\t *          {\n\t\t *            \"engine\":   \"Trident\",\n\t\t *            \"browser\":  \"Internet Explorer 5.0\",\n\t\t *            \"platform\": \"Win 95+\",\n\t\t *            \"version\":  5,\n\t\t *            \"grade\":    \"C\"\n\t\t *          }\n\t\t *        ],\n\t\t *        \"columns\": [\n\t\t *          { \"title\": \"Engine\",   \"data\": \"engine\" },\n\t\t *          { \"title\": \"Browser\",  \"data\": \"browser\" },\n\t\t *          { \"title\": \"Platform\", \"data\": \"platform\" },\n\t\t *          { \"title\": \"Version\",  \"data\": \"version\" },\n\t\t *          { \"title\": \"Grade\",    \"data\": \"grade\" }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"aaData\": null,\n\t\n\t\n\t\t/**\n\t\t * If ordering is enabled, then DataTables will perform a first pass sort on\n\t\t * initialisation. You can define which column(s) the sort is performed\n\t\t * upon, and the sorting direction, with this variable. The `sorting` array\n\t\t * should contain an array for each column to be sorted initially containing\n\t\t * the column's index and a direction string ('asc' or 'desc').\n\t\t *  @type array\n\t\t *  @default [[0,'asc']]\n\t\t *\n\t\t *  @dtopt Option\n\t\t *  @name DataTable.defaults.order\n\t\t *\n\t\t *  @example\n\t\t *    // Sort by 3rd column first, and then 4th column\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"order\": [[2,'asc'], [3,'desc']]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *    // No initial sorting\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"order\": []\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"aaSorting\": [[0,'asc']],\n\t\n\t\n\t\t/**\n\t\t * This parameter is basically identical to the `sorting` parameter, but\n\t\t * cannot be overridden by user interaction with the table. What this means\n\t\t * is that you could have a column (visible or hidden) which the sorting\n\t\t * will always be forced on first - any sorting after that (from the user)\n\t\t * will then be performed as required. This can be useful for grouping rows\n\t\t * together.\n\t\t *  @type array\n\t\t *  @default null\n\t\t *\n\t\t *  @dtopt Option\n\t\t *  @name DataTable.defaults.orderFixed\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"orderFixed\": [[0,'asc']]\n\t\t *      } );\n\t\t *    } )\n\t\t */\n\t\t\"aaSortingFixed\": [],\n\t\n\t\n\t\t/**\n\t\t * DataTables can be instructed to load data to display in the table from a\n\t\t * Ajax source. This option defines how that Ajax call is made and where to.\n\t\t *\n\t\t * The `ajax` property has three different modes of operation, depending on\n\t\t * how it is defined. These are:\n\t\t *\n\t\t * * `string` - Set the URL from where the data should be loaded from.\n\t\t * * `object` - Define properties for `jQuery.ajax`.\n\t\t * * `function` - Custom data get function\n\t\t *\n\t\t * `string`\n\t\t * --------\n\t\t *\n\t\t * As a string, the `ajax` property simply defines the URL from which\n\t\t * DataTables will load data.\n\t\t *\n\t\t * `object`\n\t\t * --------\n\t\t *\n\t\t * As an object, the parameters in the object are passed to\n\t\t * [jQuery.ajax](http://api.jquery.com/jQuery.ajax/) allowing fine control\n\t\t * of the Ajax request. DataTables has a number of default parameters which\n\t\t * you can override using this option. Please refer to the jQuery\n\t\t * documentation for a full description of the options available, although\n\t\t * the following parameters provide additional options in DataTables or\n\t\t * require special consideration:\n\t\t *\n\t\t * * `data` - As with jQuery, `data` can be provided as an object, but it\n\t\t *   can also be used as a function to manipulate the data DataTables sends\n\t\t *   to the server. The function takes a single parameter, an object of\n\t\t *   parameters with the values that DataTables has readied for sending. An\n\t\t *   object may be returned which will be merged into the DataTables\n\t\t *   defaults, or you can add the items to the object that was passed in and\n\t\t *   not return anything from the function. This supersedes `fnServerParams`\n\t\t *   from DataTables 1.9-.\n\t\t *\n\t\t * * `dataSrc` - By default DataTables will look for the property `data` (or\n\t\t *   `aaData` for compatibility with DataTables 1.9-) when obtaining data\n\t\t *   from an Ajax source or for server-side processing - this parameter\n\t\t *   allows that property to be changed. You can use Javascript dotted\n\t\t *   object notation to get a data source for multiple levels of nesting, or\n\t\t *   it my be used as a function. As a function it takes a single parameter,\n\t\t *   the JSON returned from the server, which can be manipulated as\n\t\t *   required, with the returned value being that used by DataTables as the\n\t\t *   data source for the table. This supersedes `sAjaxDataProp` from\n\t\t *   DataTables 1.9-.\n\t\t *\n\t\t * * `success` - Should not be overridden it is used internally in\n\t\t *   DataTables. To manipulate / transform the data returned by the server\n\t\t *   use `ajax.dataSrc`, or use `ajax` as a function (see below).\n\t\t *\n\t\t * `function`\n\t\t * ----------\n\t\t *\n\t\t * As a function, making the Ajax call is left up to yourself allowing\n\t\t * complete control of the Ajax request. Indeed, if desired, a method other\n\t\t * than Ajax could be used to obtain the required data, such as Web storage\n\t\t * or an AIR database.\n\t\t *\n\t\t * The function is given four parameters and no return is required. The\n\t\t * parameters are:\n\t\t *\n\t\t * 1. _object_ - Data to send to the server\n\t\t * 2. _function_ - Callback function that must be executed when the required\n\t\t *    data has been obtained. That data should be passed into the callback\n\t\t *    as the only parameter\n\t\t * 3. _object_ - DataTables settings object for the table\n\t\t *\n\t\t * Note that this supersedes `fnServerData` from DataTables 1.9-.\n\t\t *\n\t\t *  @type string|object|function\n\t\t *  @default null\n\t\t *\n\t\t *  @dtopt Option\n\t\t *  @name DataTable.defaults.ajax\n\t\t *  @since 1.10.0\n\t\t *\n\t\t * @example\n\t\t *   // Get JSON data from a file via Ajax.\n\t\t *   // Note DataTables expects data in the form `{ data: [ ...data... ] }` by default).\n\t\t *   $('#example').dataTable( {\n\t\t *     \"ajax\": \"data.json\"\n\t\t *   } );\n\t\t *\n\t\t * @example\n\t\t *   // Get JSON data from a file via Ajax, using `dataSrc` to change\n\t\t *   // `data` to `tableData` (i.e. `{ tableData: [ ...data... ] }`)\n\t\t *   $('#example').dataTable( {\n\t\t *     \"ajax\": {\n\t\t *       \"url\": \"data.json\",\n\t\t *       \"dataSrc\": \"tableData\"\n\t\t *     }\n\t\t *   } );\n\t\t *\n\t\t * @example\n\t\t *   // Get JSON data from a file via Ajax, using `dataSrc` to read data\n\t\t *   // from a plain array rather than an array in an object\n\t\t *   $('#example').dataTable( {\n\t\t *     \"ajax\": {\n\t\t *       \"url\": \"data.json\",\n\t\t *       \"dataSrc\": \"\"\n\t\t *     }\n\t\t *   } );\n\t\t *\n\t\t * @example\n\t\t *   // Manipulate the data returned from the server - add a link to data\n\t\t *   // (note this can, should, be done using `render` for the column - this\n\t\t *   // is just a simple example of how the data can be manipulated).\n\t\t *   $('#example').dataTable( {\n\t\t *     \"ajax\": {\n\t\t *       \"url\": \"data.json\",\n\t\t *       \"dataSrc\": function ( json ) {\n\t\t *         for ( var i=0, ien=json.length ; i<ien ; i++ ) {\n\t\t *           json[i][0] = '<a href=\"/message/'+json[i][0]+'>View message</a>';\n\t\t *         }\n\t\t *         return json;\n\t\t *       }\n\t\t *     }\n\t\t *   } );\n\t\t *\n\t\t * @example\n\t\t *   // Add data to the request\n\t\t *   $('#example').dataTable( {\n\t\t *     \"ajax\": {\n\t\t *       \"url\": \"data.json\",\n\t\t *       \"data\": function ( d ) {\n\t\t *         return {\n\t\t *           \"extra_search\": $('#extra').val()\n\t\t *         };\n\t\t *       }\n\t\t *     }\n\t\t *   } );\n\t\t *\n\t\t * @example\n\t\t *   // Send request as POST\n\t\t *   $('#example').dataTable( {\n\t\t *     \"ajax\": {\n\t\t *       \"url\": \"data.json\",\n\t\t *       \"type\": \"POST\"\n\t\t *     }\n\t\t *   } );\n\t\t *\n\t\t * @example\n\t\t *   // Get the data from localStorage (could interface with a form for\n\t\t *   // adding, editing and removing rows).\n\t\t *   $('#example').dataTable( {\n\t\t *     \"ajax\": function (data, callback, settings) {\n\t\t *       callback(\n\t\t *         JSON.parse( localStorage.getItem('dataTablesData') )\n\t\t *       );\n\t\t *     }\n\t\t *   } );\n\t\t */\n\t\t\"ajax\": null,\n\t\n\t\n\t\t/**\n\t\t * This parameter allows you to readily specify the entries in the length drop\n\t\t * down menu that DataTables shows when pagination is enabled. It can be\n\t\t * either a 1D array of options which will be used for both the displayed\n\t\t * option and the value, or a 2D array which will use the array in the first\n\t\t * position as the value, and the array in the second position as the\n\t\t * displayed options (useful for language strings such as 'All').\n\t\t *\n\t\t * Note that the `pageLength` property will be automatically set to the\n\t\t * first value given in this array, unless `pageLength` is also provided.\n\t\t *  @type array\n\t\t *  @default [ 10, 25, 50, 100 ]\n\t\t *\n\t\t *  @dtopt Option\n\t\t *  @name DataTable.defaults.lengthMenu\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"lengthMenu\": [[10, 25, 50, -1], [10, 25, 50, \"All\"]]\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"aLengthMenu\": [ 10, 25, 50, 100 ],\n\t\n\t\n\t\t/**\n\t\t * The `columns` option in the initialisation parameter allows you to define\n\t\t * details about the way individual columns behave. For a full list of\n\t\t * column options that can be set, please see\n\t\t * {@link DataTable.defaults.column}. Note that if you use `columns` to\n\t\t * define your columns, you must have an entry in the array for every single\n\t\t * column that you have in your table (these can be null if you don't which\n\t\t * to specify any options).\n\t\t *  @member\n\t\t *\n\t\t *  @name DataTable.defaults.column\n\t\t */\n\t\t\"aoColumns\": null,\n\t\n\t\t/**\n\t\t * Very similar to `columns`, `columnDefs` allows you to target a specific\n\t\t * column, multiple columns, or all columns, using the `targets` property of\n\t\t * each object in the array. This allows great flexibility when creating\n\t\t * tables, as the `columnDefs` arrays can be of any length, targeting the\n\t\t * columns you specifically want. `columnDefs` may use any of the column\n\t\t * options available: {@link DataTable.defaults.column}, but it _must_\n\t\t * have `targets` defined in each object in the array. Values in the `targets`\n\t\t * array may be:\n\t\t *   <ul>\n\t\t *     <li>a string - class name will be matched on the TH for the column</li>\n\t\t *     <li>0 or a positive integer - column index counting from the left</li>\n\t\t *     <li>a negative integer - column index counting from the right</li>\n\t\t *     <li>the string \"_all\" - all columns (i.e. assign a default)</li>\n\t\t *   </ul>\n\t\t *  @member\n\t\t *\n\t\t *  @name DataTable.defaults.columnDefs\n\t\t */\n\t\t\"aoColumnDefs\": null,\n\t\n\t\n\t\t/**\n\t\t * Basically the same as `search`, this parameter defines the individual column\n\t\t * filtering state at initialisation time. The array must be of the same size\n\t\t * as the number of columns, and each element be an object with the parameters\n\t\t * `search` and `escapeRegex` (the latter is optional). 'null' is also\n\t\t * accepted and the default will be used.\n\t\t *  @type array\n\t\t *  @default []\n\t\t *\n\t\t *  @dtopt Option\n\t\t *  @name DataTable.defaults.searchCols\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"searchCols\": [\n\t\t *          null,\n\t\t *          { \"search\": \"My filter\" },\n\t\t *          null,\n\t\t *          { \"search\": \"^[0-9]\", \"escapeRegex\": false }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } )\n\t\t */\n\t\t\"aoSearchCols\": [],\n\t\n\t\n\t\t/**\n\t\t * An array of CSS classes that should be applied to displayed rows. This\n\t\t * array may be of any length, and DataTables will apply each class\n\t\t * sequentially, looping when required.\n\t\t *  @type array\n\t\t *  @default null <i>Will take the values determined by the `oClasses.stripe*`\n\t\t *    options</i>\n\t\t *\n\t\t *  @dtopt Option\n\t\t *  @name DataTable.defaults.stripeClasses\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"stripeClasses\": [ 'strip1', 'strip2', 'strip3' ]\n\t\t *      } );\n\t\t *    } )\n\t\t */\n\t\t\"asStripeClasses\": null,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable automatic column width calculation. This can be disabled\n\t\t * as an optimisation (it takes some time to calculate the widths) if the\n\t\t * tables widths are passed in using `columns`.\n\t\t *  @type boolean\n\t\t *  @default true\n\t\t *\n\t\t *  @dtopt Features\n\t\t *  @name DataTable.defaults.autoWidth\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function () {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"autoWidth\": false\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"bAutoWidth\": true,\n\t\n\t\n\t\t/**\n\t\t * Deferred rendering can provide DataTables with a huge speed boost when you\n\t\t * are using an Ajax or JS data source for the table. This option, when set to\n\t\t * true, will cause DataTables to defer the creation of the table elements for\n\t\t * each row until they are needed for a draw - saving a significant amount of\n\t\t * time.\n\t\t *  @type boolean\n\t\t *  @default false\n\t\t *\n\t\t *  @dtopt Features\n\t\t *  @name DataTable.defaults.deferRender\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"ajax\": \"sources/arrays.txt\",\n\t\t *        \"deferRender\": true\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"bDeferRender\": false,\n\t\n\t\n\t\t/**\n\t\t * Replace a DataTable which matches the given selector and replace it with\n\t\t * one which has the properties of the new initialisation object passed. If no\n\t\t * table matches the selector, then the new DataTable will be constructed as\n\t\t * per normal.\n\t\t *  @type boolean\n\t\t *  @default false\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @name DataTable.defaults.destroy\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"srollY\": \"200px\",\n\t\t *        \"paginate\": false\n\t\t *      } );\n\t\t *\n\t\t *      // Some time later....\n\t\t *      $('#example').dataTable( {\n\t\t *        \"filter\": false,\n\t\t *        \"destroy\": true\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"bDestroy\": false,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable filtering of data. Filtering in DataTables is \"smart\" in\n\t\t * that it allows the end user to input multiple words (space separated) and\n\t\t * will match a row containing those words, even if not in the order that was\n\t\t * specified (this allow matching across multiple columns). Note that if you\n\t\t * wish to use filtering in DataTables this must remain 'true' - to remove the\n\t\t * default filtering input box and retain filtering abilities, please use\n\t\t * {@link DataTable.defaults.dom}.\n\t\t *  @type boolean\n\t\t *  @default true\n\t\t *\n\t\t *  @dtopt Features\n\t\t *  @name DataTable.defaults.searching\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function () {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"searching\": false\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"bFilter\": true,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable the table information display. This shows information\n\t\t * about the data that is currently visible on the page, including information\n\t\t * about filtered data if that action is being performed.\n\t\t *  @type boolean\n\t\t *  @default true\n\t\t *\n\t\t *  @dtopt Features\n\t\t *  @name DataTable.defaults.info\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function () {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"info\": false\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"bInfo\": true,\n\t\n\t\n\t\t/**\n\t\t * Allows the end user to select the size of a formatted page from a select\n\t\t * menu (sizes are 10, 25, 50 and 100). Requires pagination (`paginate`).\n\t\t *  @type boolean\n\t\t *  @default true\n\t\t *\n\t\t *  @dtopt Features\n\t\t *  @name DataTable.defaults.lengthChange\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function () {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"lengthChange\": false\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"bLengthChange\": true,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable pagination.\n\t\t *  @type boolean\n\t\t *  @default true\n\t\t *\n\t\t *  @dtopt Features\n\t\t *  @name DataTable.defaults.paging\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function () {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"paging\": false\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"bPaginate\": true,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable the display of a 'processing' indicator when the table is\n\t\t * being processed (e.g. a sort). This is particularly useful for tables with\n\t\t * large amounts of data where it can take a noticeable amount of time to sort\n\t\t * the entries.\n\t\t *  @type boolean\n\t\t *  @default false\n\t\t *\n\t\t *  @dtopt Features\n\t\t *  @name DataTable.defaults.processing\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function () {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"processing\": true\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"bProcessing\": false,\n\t\n\t\n\t\t/**\n\t\t * Retrieve the DataTables object for the given selector. Note that if the\n\t\t * table has already been initialised, this parameter will cause DataTables\n\t\t * to simply return the object that has already been set up - it will not take\n\t\t * account of any changes you might have made to the initialisation object\n\t\t * passed to DataTables (setting this parameter to true is an acknowledgement\n\t\t * that you understand this). `destroy` can be used to reinitialise a table if\n\t\t * you need.\n\t\t *  @type boolean\n\t\t *  @default false\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @name DataTable.defaults.retrieve\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      initTable();\n\t\t *      tableActions();\n\t\t *    } );\n\t\t *\n\t\t *    function initTable ()\n\t\t *    {\n\t\t *      return $('#example').dataTable( {\n\t\t *        \"scrollY\": \"200px\",\n\t\t *        \"paginate\": false,\n\t\t *        \"retrieve\": true\n\t\t *      } );\n\t\t *    }\n\t\t *\n\t\t *    function tableActions ()\n\t\t *    {\n\t\t *      var table = initTable();\n\t\t *      // perform API operations with oTable\n\t\t *    }\n\t\t */\n\t\t\"bRetrieve\": false,\n\t\n\t\n\t\t/**\n\t\t * When vertical (y) scrolling is enabled, DataTables will force the height of\n\t\t * the table's viewport to the given height at all times (useful for layout).\n\t\t * However, this can look odd when filtering data down to a small data set,\n\t\t * and the footer is left \"floating\" further down. This parameter (when\n\t\t * enabled) will cause DataTables to collapse the table's viewport down when\n\t\t * the result set will fit within the given Y height.\n\t\t *  @type boolean\n\t\t *  @default false\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @name DataTable.defaults.scrollCollapse\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"scrollY\": \"200\",\n\t\t *        \"scrollCollapse\": true\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"bScrollCollapse\": false,\n\t\n\t\n\t\t/**\n\t\t * Configure DataTables to use server-side processing. Note that the\n\t\t * `ajax` parameter must also be given in order to give DataTables a\n\t\t * source to obtain the required data for each draw.\n\t\t *  @type boolean\n\t\t *  @default false\n\t\t *\n\t\t *  @dtopt Features\n\t\t *  @dtopt Server-side\n\t\t *  @name DataTable.defaults.serverSide\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function () {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"serverSide\": true,\n\t\t *        \"ajax\": \"xhr.php\"\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"bServerSide\": false,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable sorting of columns. Sorting of individual columns can be\n\t\t * disabled by the `sortable` option for each column.\n\t\t *  @type boolean\n\t\t *  @default true\n\t\t *\n\t\t *  @dtopt Features\n\t\t *  @name DataTable.defaults.ordering\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function () {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"ordering\": false\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"bSort\": true,\n\t\n\t\n\t\t/**\n\t\t * Enable or display DataTables' ability to sort multiple columns at the\n\t\t * same time (activated by shift-click by the user).\n\t\t *  @type boolean\n\t\t *  @default true\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @name DataTable.defaults.orderMulti\n\t\t *\n\t\t *  @example\n\t\t *    // Disable multiple column sorting ability\n\t\t *    $(document).ready( function () {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"orderMulti\": false\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"bSortMulti\": true,\n\t\n\t\n\t\t/**\n\t\t * Allows control over whether DataTables should use the top (true) unique\n\t\t * cell that is found for a single column, or the bottom (false - default).\n\t\t * This is useful when using complex headers.\n\t\t *  @type boolean\n\t\t *  @default false\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @name DataTable.defaults.orderCellsTop\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"orderCellsTop\": true\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"bSortCellsTop\": false,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable the addition of the classes `sorting\\_1`, `sorting\\_2` and\n\t\t * `sorting\\_3` to the columns which are currently being sorted on. This is\n\t\t * presented as a feature switch as it can increase processing time (while\n\t\t * classes are removed and added) so for large data sets you might want to\n\t\t * turn this off.\n\t\t *  @type boolean\n\t\t *  @default true\n\t\t *\n\t\t *  @dtopt Features\n\t\t *  @name DataTable.defaults.orderClasses\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function () {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"orderClasses\": false\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"bSortClasses\": true,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable state saving. When enabled HTML5 `localStorage` will be\n\t\t * used to save table display information such as pagination information,\n\t\t * display length, filtering and sorting. As such when the end user reloads\n\t\t * the page the display display will match what thy had previously set up.\n\t\t *\n\t\t * Due to the use of `localStorage` the default state saving is not supported\n\t\t * in IE6 or 7. If state saving is required in those browsers, use\n\t\t * `stateSaveCallback` to provide a storage solution such as cookies.\n\t\t *  @type boolean\n\t\t *  @default false\n\t\t *\n\t\t *  @dtopt Features\n\t\t *  @name DataTable.defaults.stateSave\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function () {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"stateSave\": true\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"bStateSave\": false,\n\t\n\t\n\t\t/**\n\t\t * This function is called when a TR element is created (and all TD child\n\t\t * elements have been inserted), or registered if using a DOM source, allowing\n\t\t * manipulation of the TR element (adding classes etc).\n\t\t *  @type function\n\t\t *  @param {node} row \"TR\" element for the current row\n\t\t *  @param {array} data Raw data array for this row\n\t\t *  @param {int} dataIndex The index of this row in the internal aoData array\n\t\t *\n\t\t *  @dtopt Callbacks\n\t\t *  @name DataTable.defaults.createdRow\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"createdRow\": function( row, data, dataIndex ) {\n\t\t *          // Bold the grade for all 'A' grade browsers\n\t\t *          if ( data[4] == \"A\" )\n\t\t *          {\n\t\t *            $('td:eq(4)', row).html( '<b>A</b>' );\n\t\t *          }\n\t\t *        }\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"fnCreatedRow\": null,\n\t\n\t\n\t\t/**\n\t\t * This function is called on every 'draw' event, and allows you to\n\t\t * dynamically modify any aspect you want about the created DOM.\n\t\t *  @type function\n\t\t *  @param {object} settings DataTables settings object\n\t\t *\n\t\t *  @dtopt Callbacks\n\t\t *  @name DataTable.defaults.drawCallback\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"drawCallback\": function( settings ) {\n\t\t *          alert( 'DataTables has redrawn the table' );\n\t\t *        }\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"fnDrawCallback\": null,\n\t\n\t\n\t\t/**\n\t\t * Identical to fnHeaderCallback() but for the table footer this function\n\t\t * allows you to modify the table footer on every 'draw' event.\n\t\t *  @type function\n\t\t *  @param {node} foot \"TR\" element for the footer\n\t\t *  @param {array} data Full table data (as derived from the original HTML)\n\t\t *  @param {int} start Index for the current display starting point in the\n\t\t *    display array\n\t\t *  @param {int} end Index for the current display ending point in the\n\t\t *    display array\n\t\t *  @param {array int} display Index array to translate the visual position\n\t\t *    to the full data array\n\t\t *\n\t\t *  @dtopt Callbacks\n\t\t *  @name DataTable.defaults.footerCallback\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"footerCallback\": function( tfoot, data, start, end, display ) {\n\t\t *          tfoot.getElementsByTagName('th')[0].innerHTML = \"Starting index is \"+start;\n\t\t *        }\n\t\t *      } );\n\t\t *    } )\n\t\t */\n\t\t\"fnFooterCallback\": null,\n\t\n\t\n\t\t/**\n\t\t * When rendering large numbers in the information element for the table\n\t\t * (i.e. \"Showing 1 to 10 of 57 entries\") DataTables will render large numbers\n\t\t * to have a comma separator for the 'thousands' units (e.g. 1 million is\n\t\t * rendered as \"1,000,000\") to help readability for the end user. This\n\t\t * function will override the default method DataTables uses.\n\t\t *  @type function\n\t\t *  @member\n\t\t *  @param {int} toFormat number to be formatted\n\t\t *  @returns {string} formatted string for DataTables to show the number\n\t\t *\n\t\t *  @dtopt Callbacks\n\t\t *  @name DataTable.defaults.formatNumber\n\t\t *\n\t\t *  @example\n\t\t *    // Format a number using a single quote for the separator (note that\n\t\t *    // this can also be done with the language.thousands option)\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"formatNumber\": function ( toFormat ) {\n\t\t *          return toFormat.toString().replace(\n\t\t *            /\\B(?=(\\d{3})+(?!\\d))/g, \"'\"\n\t\t *          );\n\t\t *        };\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"fnFormatNumber\": function ( toFormat ) {\n\t\t\treturn toFormat.toString().replace(\n\t\t\t\t/\\B(?=(\\d{3})+(?!\\d))/g,\n\t\t\t\tthis.oLanguage.sThousands\n\t\t\t);\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * This function is called on every 'draw' event, and allows you to\n\t\t * dynamically modify the header row. This can be used to calculate and\n\t\t * display useful information about the table.\n\t\t *  @type function\n\t\t *  @param {node} head \"TR\" element for the header\n\t\t *  @param {array} data Full table data (as derived from the original HTML)\n\t\t *  @param {int} start Index for the current display starting point in the\n\t\t *    display array\n\t\t *  @param {int} end Index for the current display ending point in the\n\t\t *    display array\n\t\t *  @param {array int} display Index array to translate the visual position\n\t\t *    to the full data array\n\t\t *\n\t\t *  @dtopt Callbacks\n\t\t *  @name DataTable.defaults.headerCallback\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"fheaderCallback\": function( head, data, start, end, display ) {\n\t\t *          head.getElementsByTagName('th')[0].innerHTML = \"Displaying \"+(end-start)+\" records\";\n\t\t *        }\n\t\t *      } );\n\t\t *    } )\n\t\t */\n\t\t\"fnHeaderCallback\": null,\n\t\n\t\n\t\t/**\n\t\t * The information element can be used to convey information about the current\n\t\t * state of the table. Although the internationalisation options presented by\n\t\t * DataTables are quite capable of dealing with most customisations, there may\n\t\t * be times where you wish to customise the string further. This callback\n\t\t * allows you to do exactly that.\n\t\t *  @type function\n\t\t *  @param {object} oSettings DataTables settings object\n\t\t *  @param {int} start Starting position in data for the draw\n\t\t *  @param {int} end End position in data for the draw\n\t\t *  @param {int} max Total number of rows in the table (regardless of\n\t\t *    filtering)\n\t\t *  @param {int} total Total number of rows in the data set, after filtering\n\t\t *  @param {string} pre The string that DataTables has formatted using it's\n\t\t *    own rules\n\t\t *  @returns {string} The string to be displayed in the information element.\n\t\t *\n\t\t *  @dtopt Callbacks\n\t\t *  @name DataTable.defaults.infoCallback\n\t\t *\n\t\t *  @example\n\t\t *    $('#example').dataTable( {\n\t\t *      \"infoCallback\": function( settings, start, end, max, total, pre ) {\n\t\t *        return start +\" to \"+ end;\n\t\t *      }\n\t\t *    } );\n\t\t */\n\t\t\"fnInfoCallback\": null,\n\t\n\t\n\t\t/**\n\t\t * Called when the table has been initialised. Normally DataTables will\n\t\t * initialise sequentially and there will be no need for this function,\n\t\t * however, this does not hold true when using external language information\n\t\t * since that is obtained using an async XHR call.\n\t\t *  @type function\n\t\t *  @param {object} settings DataTables settings object\n\t\t *  @param {object} json The JSON object request from the server - only\n\t\t *    present if client-side Ajax sourced data is used\n\t\t *\n\t\t *  @dtopt Callbacks\n\t\t *  @name DataTable.defaults.initComplete\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"initComplete\": function(settings, json) {\n\t\t *          alert( 'DataTables has finished its initialisation.' );\n\t\t *        }\n\t\t *      } );\n\t\t *    } )\n\t\t */\n\t\t\"fnInitComplete\": null,\n\t\n\t\n\t\t/**\n\t\t * Called at the very start of each table draw and can be used to cancel the\n\t\t * draw by returning false, any other return (including undefined) results in\n\t\t * the full draw occurring).\n\t\t *  @type function\n\t\t *  @param {object} settings DataTables settings object\n\t\t *  @returns {boolean} False will cancel the draw, anything else (including no\n\t\t *    return) will allow it to complete.\n\t\t *\n\t\t *  @dtopt Callbacks\n\t\t *  @name DataTable.defaults.preDrawCallback\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"preDrawCallback\": function( settings ) {\n\t\t *          if ( $('#test').val() == 1 ) {\n\t\t *            return false;\n\t\t *          }\n\t\t *        }\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"fnPreDrawCallback\": null,\n\t\n\t\n\t\t/**\n\t\t * This function allows you to 'post process' each row after it have been\n\t\t * generated for each table draw, but before it is rendered on screen. This\n\t\t * function might be used for setting the row class name etc.\n\t\t *  @type function\n\t\t *  @param {node} row \"TR\" element for the current row\n\t\t *  @param {array} data Raw data array for this row\n\t\t *  @param {int} displayIndex The display index for the current table draw\n\t\t *  @param {int} displayIndexFull The index of the data in the full list of\n\t\t *    rows (after filtering)\n\t\t *\n\t\t *  @dtopt Callbacks\n\t\t *  @name DataTable.defaults.rowCallback\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"rowCallback\": function( row, data, displayIndex, displayIndexFull ) {\n\t\t *          // Bold the grade for all 'A' grade browsers\n\t\t *          if ( data[4] == \"A\" ) {\n\t\t *            $('td:eq(4)', row).html( '<b>A</b>' );\n\t\t *          }\n\t\t *        }\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"fnRowCallback\": null,\n\t\n\t\n\t\t/**\n\t\t * __Deprecated__ The functionality provided by this parameter has now been\n\t\t * superseded by that provided through `ajax`, which should be used instead.\n\t\t *\n\t\t * This parameter allows you to override the default function which obtains\n\t\t * the data from the server so something more suitable for your application.\n\t\t * For example you could use POST data, or pull information from a Gears or\n\t\t * AIR database.\n\t\t *  @type function\n\t\t *  @member\n\t\t *  @param {string} source HTTP source to obtain the data from (`ajax`)\n\t\t *  @param {array} data A key/value pair object containing the data to send\n\t\t *    to the server\n\t\t *  @param {function} callback to be called on completion of the data get\n\t\t *    process that will draw the data on the page.\n\t\t *  @param {object} settings DataTables settings object\n\t\t *\n\t\t *  @dtopt Callbacks\n\t\t *  @dtopt Server-side\n\t\t *  @name DataTable.defaults.serverData\n\t\t *\n\t\t *  @deprecated 1.10. Please use `ajax` for this functionality now.\n\t\t */\n\t\t\"fnServerData\": null,\n\t\n\t\n\t\t/**\n\t\t * __Deprecated__ The functionality provided by this parameter has now been\n\t\t * superseded by that provided through `ajax`, which should be used instead.\n\t\t *\n\t\t *  It is often useful to send extra data to the server when making an Ajax\n\t\t * request - for example custom filtering information, and this callback\n\t\t * function makes it trivial to send extra information to the server. The\n\t\t * passed in parameter is the data set that has been constructed by\n\t\t * DataTables, and you can add to this or modify it as you require.\n\t\t *  @type function\n\t\t *  @param {array} data Data array (array of objects which are name/value\n\t\t *    pairs) that has been constructed by DataTables and will be sent to the\n\t\t *    server. In the case of Ajax sourced data with server-side processing\n\t\t *    this will be an empty array, for server-side processing there will be a\n\t\t *    significant number of parameters!\n\t\t *  @returns {undefined} Ensure that you modify the data array passed in,\n\t\t *    as this is passed by reference.\n\t\t *\n\t\t *  @dtopt Callbacks\n\t\t *  @dtopt Server-side\n\t\t *  @name DataTable.defaults.serverParams\n\t\t *\n\t\t *  @deprecated 1.10. Please use `ajax` for this functionality now.\n\t\t */\n\t\t\"fnServerParams\": null,\n\t\n\t\n\t\t/**\n\t\t * Load the table state. With this function you can define from where, and how, the\n\t\t * state of a table is loaded. By default DataTables will load from `localStorage`\n\t\t * but you might wish to use a server-side database or cookies.\n\t\t *  @type function\n\t\t *  @member\n\t\t *  @param {object} settings DataTables settings object\n\t\t *  @param {object} callback Callback that can be executed when done. It\n\t\t *    should be passed the loaded state object.\n\t\t *  @return {object} The DataTables state object to be loaded\n\t\t *\n\t\t *  @dtopt Callbacks\n\t\t *  @name DataTable.defaults.stateLoadCallback\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"stateSave\": true,\n\t\t *        \"stateLoadCallback\": function (settings, callback) {\n\t\t *          $.ajax( {\n\t\t *            \"url\": \"/state_load\",\n\t\t *            \"dataType\": \"json\",\n\t\t *            \"success\": function (json) {\n\t\t *              callback( json );\n\t\t *            }\n\t\t *          } );\n\t\t *        }\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"fnStateLoadCallback\": function ( settings ) {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(\n\t\t\t\t\t(settings.iStateDuration === -1 ? sessionStorage : localStorage).getItem(\n\t\t\t\t\t\t'DataTables_'+settings.sInstance+'_'+location.pathname\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t} catch (e) {\n\t\t\t\treturn {};\n\t\t\t}\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * Callback which allows modification of the saved state prior to loading that state.\n\t\t * This callback is called when the table is loading state from the stored data, but\n\t\t * prior to the settings object being modified by the saved state. Note that for\n\t\t * plug-in authors, you should use the `stateLoadParams` event to load parameters for\n\t\t * a plug-in.\n\t\t *  @type function\n\t\t *  @param {object} settings DataTables settings object\n\t\t *  @param {object} data The state object that is to be loaded\n\t\t *\n\t\t *  @dtopt Callbacks\n\t\t *  @name DataTable.defaults.stateLoadParams\n\t\t *\n\t\t *  @example\n\t\t *    // Remove a saved filter, so filtering is never loaded\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"stateSave\": true,\n\t\t *        \"stateLoadParams\": function (settings, data) {\n\t\t *          data.oSearch.sSearch = \"\";\n\t\t *        }\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Disallow state loading by returning false\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"stateSave\": true,\n\t\t *        \"stateLoadParams\": function (settings, data) {\n\t\t *          return false;\n\t\t *        }\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"fnStateLoadParams\": null,\n\t\n\t\n\t\t/**\n\t\t * Callback that is called when the state has been loaded from the state saving method\n\t\t * and the DataTables settings object has been modified as a result of the loaded state.\n\t\t *  @type function\n\t\t *  @param {object} settings DataTables settings object\n\t\t *  @param {object} data The state object that was loaded\n\t\t *\n\t\t *  @dtopt Callbacks\n\t\t *  @name DataTable.defaults.stateLoaded\n\t\t *\n\t\t *  @example\n\t\t *    // Show an alert with the filtering value that was saved\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"stateSave\": true,\n\t\t *        \"stateLoaded\": function (settings, data) {\n\t\t *          alert( 'Saved filter was: '+data.oSearch.sSearch );\n\t\t *        }\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"fnStateLoaded\": null,\n\t\n\t\n\t\t/**\n\t\t * Save the table state. This function allows you to define where and how the state\n\t\t * information for the table is stored By default DataTables will use `localStorage`\n\t\t * but you might wish to use a server-side database or cookies.\n\t\t *  @type function\n\t\t *  @member\n\t\t *  @param {object} settings DataTables settings object\n\t\t *  @param {object} data The state object to be saved\n\t\t *\n\t\t *  @dtopt Callbacks\n\t\t *  @name DataTable.defaults.stateSaveCallback\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"stateSave\": true,\n\t\t *        \"stateSaveCallback\": function (settings, data) {\n\t\t *          // Send an Ajax request to the server with the state object\n\t\t *          $.ajax( {\n\t\t *            \"url\": \"/state_save\",\n\t\t *            \"data\": data,\n\t\t *            \"dataType\": \"json\",\n\t\t *            \"method\": \"POST\"\n\t\t *            \"success\": function () {}\n\t\t *          } );\n\t\t *        }\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"fnStateSaveCallback\": function ( settings, data ) {\n\t\t\ttry {\n\t\t\t\t(settings.iStateDuration === -1 ? sessionStorage : localStorage).setItem(\n\t\t\t\t\t'DataTables_'+settings.sInstance+'_'+location.pathname,\n\t\t\t\t\tJSON.stringify( data )\n\t\t\t\t);\n\t\t\t} catch (e) {}\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * Callback which allows modification of the state to be saved. Called when the table\n\t\t * has changed state a new state save is required. This method allows modification of\n\t\t * the state saving object prior to actually doing the save, including addition or\n\t\t * other state properties or modification. Note that for plug-in authors, you should\n\t\t * use the `stateSaveParams` event to save parameters for a plug-in.\n\t\t *  @type function\n\t\t *  @param {object} settings DataTables settings object\n\t\t *  @param {object} data The state object to be saved\n\t\t *\n\t\t *  @dtopt Callbacks\n\t\t *  @name DataTable.defaults.stateSaveParams\n\t\t *\n\t\t *  @example\n\t\t *    // Remove a saved filter, so filtering is never saved\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"stateSave\": true,\n\t\t *        \"stateSaveParams\": function (settings, data) {\n\t\t *          data.oSearch.sSearch = \"\";\n\t\t *        }\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"fnStateSaveParams\": null,\n\t\n\t\n\t\t/**\n\t\t * Duration for which the saved state information is considered valid. After this period\n\t\t * has elapsed the state will be returned to the default.\n\t\t * Value is given in seconds.\n\t\t *  @type int\n\t\t *  @default 7200 <i>(2 hours)</i>\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @name DataTable.defaults.stateDuration\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"stateDuration\": 60*60*24; // 1 day\n\t\t *      } );\n\t\t *    } )\n\t\t */\n\t\t\"iStateDuration\": 7200,\n\t\n\t\n\t\t/**\n\t\t * When enabled DataTables will not make a request to the server for the first\n\t\t * page draw - rather it will use the data already on the page (no sorting etc\n\t\t * will be applied to it), thus saving on an XHR at load time. `deferLoading`\n\t\t * is used to indicate that deferred loading is required, but it is also used\n\t\t * to tell DataTables how many records there are in the full table (allowing\n\t\t * the information element and pagination to be displayed correctly). In the case\n\t\t * where a filtering is applied to the table on initial load, this can be\n\t\t * indicated by giving the parameter as an array, where the first element is\n\t\t * the number of records available after filtering and the second element is the\n\t\t * number of records without filtering (allowing the table information element\n\t\t * to be shown correctly).\n\t\t *  @type int | array\n\t\t *  @default null\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @name DataTable.defaults.deferLoading\n\t\t *\n\t\t *  @example\n\t\t *    // 57 records available in the table, no filtering applied\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"serverSide\": true,\n\t\t *        \"ajax\": \"scripts/server_processing.php\",\n\t\t *        \"deferLoading\": 57\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // 57 records after filtering, 100 without filtering (an initial filter applied)\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"serverSide\": true,\n\t\t *        \"ajax\": \"scripts/server_processing.php\",\n\t\t *        \"deferLoading\": [ 57, 100 ],\n\t\t *        \"search\": {\n\t\t *          \"search\": \"my_filter\"\n\t\t *        }\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"iDeferLoading\": null,\n\t\n\t\n\t\t/**\n\t\t * Number of rows to display on a single page when using pagination. If\n\t\t * feature enabled (`lengthChange`) then the end user will be able to override\n\t\t * this to a custom setting using a pop-up menu.\n\t\t *  @type int\n\t\t *  @default 10\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @name DataTable.defaults.pageLength\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"pageLength\": 50\n\t\t *      } );\n\t\t *    } )\n\t\t */\n\t\t\"iDisplayLength\": 10,\n\t\n\t\n\t\t/**\n\t\t * Define the starting point for data display when using DataTables with\n\t\t * pagination. Note that this parameter is the number of records, rather than\n\t\t * the page number, so if you have 10 records per page and want to start on\n\t\t * the third page, it should be \"20\".\n\t\t *  @type int\n\t\t *  @default 0\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @name DataTable.defaults.displayStart\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"displayStart\": 20\n\t\t *      } );\n\t\t *    } )\n\t\t */\n\t\t\"iDisplayStart\": 0,\n\t\n\t\n\t\t/**\n\t\t * By default DataTables allows keyboard navigation of the table (sorting, paging,\n\t\t * and filtering) by adding a `tabindex` attribute to the required elements. This\n\t\t * allows you to tab through the controls and press the enter key to activate them.\n\t\t * The tabindex is default 0, meaning that the tab follows the flow of the document.\n\t\t * You can overrule this using this parameter if you wish. Use a value of -1 to\n\t\t * disable built-in keyboard navigation.\n\t\t *  @type int\n\t\t *  @default 0\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @name DataTable.defaults.tabIndex\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"tabIndex\": 1\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"iTabIndex\": 0,\n\t\n\t\n\t\t/**\n\t\t * Classes that DataTables assigns to the various components and features\n\t\t * that it adds to the HTML table. This allows classes to be configured\n\t\t * during initialisation in addition to through the static\n\t\t * {@link DataTable.ext.oStdClasses} object).\n\t\t *  @namespace\n\t\t *  @name DataTable.defaults.classes\n\t\t */\n\t\t\"oClasses\": {},\n\t\n\t\n\t\t/**\n\t\t * All strings that DataTables uses in the user interface that it creates\n\t\t * are defined in this object, allowing you to modified them individually or\n\t\t * completely replace them all as required.\n\t\t *  @namespace\n\t\t *  @name DataTable.defaults.language\n\t\t */\n\t\t\"oLanguage\": {\n\t\t\t/**\n\t\t\t * Strings that are used for WAI-ARIA labels and controls only (these are not\n\t\t\t * actually visible on the page, but will be read by screenreaders, and thus\n\t\t\t * must be internationalised as well).\n\t\t\t *  @namespace\n\t\t\t *  @name DataTable.defaults.language.aria\n\t\t\t */\n\t\t\t\"oAria\": {\n\t\t\t\t/**\n\t\t\t\t * ARIA label that is added to the table headers when the column may be\n\t\t\t\t * sorted ascending by activing the column (click or return when focused).\n\t\t\t\t * Note that the column header is prefixed to this string.\n\t\t\t\t *  @type string\n\t\t\t\t *  @default : activate to sort column ascending\n\t\t\t\t *\n\t\t\t\t *  @dtopt Language\n\t\t\t\t *  @name DataTable.defaults.language.aria.sortAscending\n\t\t\t\t *\n\t\t\t\t *  @example\n\t\t\t\t *    $(document).ready( function() {\n\t\t\t\t *      $('#example').dataTable( {\n\t\t\t\t *        \"language\": {\n\t\t\t\t *          \"aria\": {\n\t\t\t\t *            \"sortAscending\": \" - click/return to sort ascending\"\n\t\t\t\t *          }\n\t\t\t\t *        }\n\t\t\t\t *      } );\n\t\t\t\t *    } );\n\t\t\t\t */\n\t\t\t\t\"sSortAscending\": \": activate to sort column ascending\",\n\t\n\t\t\t\t/**\n\t\t\t\t * ARIA label that is added to the table headers when the column may be\n\t\t\t\t * sorted descending by activing the column (click or return when focused).\n\t\t\t\t * Note that the column header is prefixed to this string.\n\t\t\t\t *  @type string\n\t\t\t\t *  @default : activate to sort column ascending\n\t\t\t\t *\n\t\t\t\t *  @dtopt Language\n\t\t\t\t *  @name DataTable.defaults.language.aria.sortDescending\n\t\t\t\t *\n\t\t\t\t *  @example\n\t\t\t\t *    $(document).ready( function() {\n\t\t\t\t *      $('#example').dataTable( {\n\t\t\t\t *        \"language\": {\n\t\t\t\t *          \"aria\": {\n\t\t\t\t *            \"sortDescending\": \" - click/return to sort descending\"\n\t\t\t\t *          }\n\t\t\t\t *        }\n\t\t\t\t *      } );\n\t\t\t\t *    } );\n\t\t\t\t */\n\t\t\t\t\"sSortDescending\": \": activate to sort column descending\"\n\t\t\t},\n\t\n\t\t\t/**\n\t\t\t * Pagination string used by DataTables for the built-in pagination\n\t\t\t * control types.\n\t\t\t *  @namespace\n\t\t\t *  @name DataTable.defaults.language.paginate\n\t\t\t */\n\t\t\t\"oPaginate\": {\n\t\t\t\t/**\n\t\t\t\t * Text to use when using the 'full_numbers' type of pagination for the\n\t\t\t\t * button to take the user to the first page.\n\t\t\t\t *  @type string\n\t\t\t\t *  @default First\n\t\t\t\t *\n\t\t\t\t *  @dtopt Language\n\t\t\t\t *  @name DataTable.defaults.language.paginate.first\n\t\t\t\t *\n\t\t\t\t *  @example\n\t\t\t\t *    $(document).ready( function() {\n\t\t\t\t *      $('#example').dataTable( {\n\t\t\t\t *        \"language\": {\n\t\t\t\t *          \"paginate\": {\n\t\t\t\t *            \"first\": \"First page\"\n\t\t\t\t *          }\n\t\t\t\t *        }\n\t\t\t\t *      } );\n\t\t\t\t *    } );\n\t\t\t\t */\n\t\t\t\t\"sFirst\": \"First\",\n\t\n\t\n\t\t\t\t/**\n\t\t\t\t * Text to use when using the 'full_numbers' type of pagination for the\n\t\t\t\t * button to take the user to the last page.\n\t\t\t\t *  @type string\n\t\t\t\t *  @default Last\n\t\t\t\t *\n\t\t\t\t *  @dtopt Language\n\t\t\t\t *  @name DataTable.defaults.language.paginate.last\n\t\t\t\t *\n\t\t\t\t *  @example\n\t\t\t\t *    $(document).ready( function() {\n\t\t\t\t *      $('#example').dataTable( {\n\t\t\t\t *        \"language\": {\n\t\t\t\t *          \"paginate\": {\n\t\t\t\t *            \"last\": \"Last page\"\n\t\t\t\t *          }\n\t\t\t\t *        }\n\t\t\t\t *      } );\n\t\t\t\t *    } );\n\t\t\t\t */\n\t\t\t\t\"sLast\": \"Last\",\n\t\n\t\n\t\t\t\t/**\n\t\t\t\t * Text to use for the 'next' pagination button (to take the user to the\n\t\t\t\t * next page).\n\t\t\t\t *  @type string\n\t\t\t\t *  @default Next\n\t\t\t\t *\n\t\t\t\t *  @dtopt Language\n\t\t\t\t *  @name DataTable.defaults.language.paginate.next\n\t\t\t\t *\n\t\t\t\t *  @example\n\t\t\t\t *    $(document).ready( function() {\n\t\t\t\t *      $('#example').dataTable( {\n\t\t\t\t *        \"language\": {\n\t\t\t\t *          \"paginate\": {\n\t\t\t\t *            \"next\": \"Next page\"\n\t\t\t\t *          }\n\t\t\t\t *        }\n\t\t\t\t *      } );\n\t\t\t\t *    } );\n\t\t\t\t */\n\t\t\t\t\"sNext\": \"Next\",\n\t\n\t\n\t\t\t\t/**\n\t\t\t\t * Text to use for the 'previous' pagination button (to take the user to\n\t\t\t\t * the previous page).\n\t\t\t\t *  @type string\n\t\t\t\t *  @default Previous\n\t\t\t\t *\n\t\t\t\t *  @dtopt Language\n\t\t\t\t *  @name DataTable.defaults.language.paginate.previous\n\t\t\t\t *\n\t\t\t\t *  @example\n\t\t\t\t *    $(document).ready( function() {\n\t\t\t\t *      $('#example').dataTable( {\n\t\t\t\t *        \"language\": {\n\t\t\t\t *          \"paginate\": {\n\t\t\t\t *            \"previous\": \"Previous page\"\n\t\t\t\t *          }\n\t\t\t\t *        }\n\t\t\t\t *      } );\n\t\t\t\t *    } );\n\t\t\t\t */\n\t\t\t\t\"sPrevious\": \"Previous\"\n\t\t\t},\n\t\n\t\t\t/**\n\t\t\t * This string is shown in preference to `zeroRecords` when the table is\n\t\t\t * empty of data (regardless of filtering). Note that this is an optional\n\t\t\t * parameter - if it is not given, the value of `zeroRecords` will be used\n\t\t\t * instead (either the default or given value).\n\t\t\t *  @type string\n\t\t\t *  @default No data available in table\n\t\t\t *\n\t\t\t *  @dtopt Language\n\t\t\t *  @name DataTable.defaults.language.emptyTable\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    $(document).ready( function() {\n\t\t\t *      $('#example').dataTable( {\n\t\t\t *        \"language\": {\n\t\t\t *          \"emptyTable\": \"No data available in table\"\n\t\t\t *        }\n\t\t\t *      } );\n\t\t\t *    } );\n\t\t\t */\n\t\t\t\"sEmptyTable\": \"No data available in table\",\n\t\n\t\n\t\t\t/**\n\t\t\t * This string gives information to the end user about the information\n\t\t\t * that is current on display on the page. The following tokens can be\n\t\t\t * used in the string and will be dynamically replaced as the table\n\t\t\t * display updates. This tokens can be placed anywhere in the string, or\n\t\t\t * removed as needed by the language requires:\n\t\t\t *\n\t\t\t * * `\\_START\\_` - Display index of the first record on the current page\n\t\t\t * * `\\_END\\_` - Display index of the last record on the current page\n\t\t\t * * `\\_TOTAL\\_` - Number of records in the table after filtering\n\t\t\t * * `\\_MAX\\_` - Number of records in the table without filtering\n\t\t\t * * `\\_PAGE\\_` - Current page number\n\t\t\t * * `\\_PAGES\\_` - Total number of pages of data in the table\n\t\t\t *\n\t\t\t *  @type string\n\t\t\t *  @default Showing _START_ to _END_ of _TOTAL_ entries\n\t\t\t *\n\t\t\t *  @dtopt Language\n\t\t\t *  @name DataTable.defaults.language.info\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    $(document).ready( function() {\n\t\t\t *      $('#example').dataTable( {\n\t\t\t *        \"language\": {\n\t\t\t *          \"info\": \"Showing page _PAGE_ of _PAGES_\"\n\t\t\t *        }\n\t\t\t *      } );\n\t\t\t *    } );\n\t\t\t */\n\t\t\t\"sInfo\": \"Showing _START_ to _END_ of _TOTAL_ entries\",\n\t\n\t\n\t\t\t/**\n\t\t\t * Display information string for when the table is empty. Typically the\n\t\t\t * format of this string should match `info`.\n\t\t\t *  @type string\n\t\t\t *  @default Showing 0 to 0 of 0 entries\n\t\t\t *\n\t\t\t *  @dtopt Language\n\t\t\t *  @name DataTable.defaults.language.infoEmpty\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    $(document).ready( function() {\n\t\t\t *      $('#example').dataTable( {\n\t\t\t *        \"language\": {\n\t\t\t *          \"infoEmpty\": \"No entries to show\"\n\t\t\t *        }\n\t\t\t *      } );\n\t\t\t *    } );\n\t\t\t */\n\t\t\t\"sInfoEmpty\": \"Showing 0 to 0 of 0 entries\",\n\t\n\t\n\t\t\t/**\n\t\t\t * When a user filters the information in a table, this string is appended\n\t\t\t * to the information (`info`) to give an idea of how strong the filtering\n\t\t\t * is. The variable _MAX_ is dynamically updated.\n\t\t\t *  @type string\n\t\t\t *  @default (filtered from _MAX_ total entries)\n\t\t\t *\n\t\t\t *  @dtopt Language\n\t\t\t *  @name DataTable.defaults.language.infoFiltered\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    $(document).ready( function() {\n\t\t\t *      $('#example').dataTable( {\n\t\t\t *        \"language\": {\n\t\t\t *          \"infoFiltered\": \" - filtering from _MAX_ records\"\n\t\t\t *        }\n\t\t\t *      } );\n\t\t\t *    } );\n\t\t\t */\n\t\t\t\"sInfoFiltered\": \"(filtered from _MAX_ total entries)\",\n\t\n\t\n\t\t\t/**\n\t\t\t * If can be useful to append extra information to the info string at times,\n\t\t\t * and this variable does exactly that. This information will be appended to\n\t\t\t * the `info` (`infoEmpty` and `infoFiltered` in whatever combination they are\n\t\t\t * being used) at all times.\n\t\t\t *  @type string\n\t\t\t *  @default <i>Empty string</i>\n\t\t\t *\n\t\t\t *  @dtopt Language\n\t\t\t *  @name DataTable.defaults.language.infoPostFix\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    $(document).ready( function() {\n\t\t\t *      $('#example').dataTable( {\n\t\t\t *        \"language\": {\n\t\t\t *          \"infoPostFix\": \"All records shown are derived from real information.\"\n\t\t\t *        }\n\t\t\t *      } );\n\t\t\t *    } );\n\t\t\t */\n\t\t\t\"sInfoPostFix\": \"\",\n\t\n\t\n\t\t\t/**\n\t\t\t * This decimal place operator is a little different from the other\n\t\t\t * language options since DataTables doesn't output floating point\n\t\t\t * numbers, so it won't ever use this for display of a number. Rather,\n\t\t\t * what this parameter does is modify the sort methods of the table so\n\t\t\t * that numbers which are in a format which has a character other than\n\t\t\t * a period (`.`) as a decimal place will be sorted numerically.\n\t\t\t *\n\t\t\t * Note that numbers with different decimal places cannot be shown in\n\t\t\t * the same table and still be sortable, the table must be consistent.\n\t\t\t * However, multiple different tables on the page can use different\n\t\t\t * decimal place characters.\n\t\t\t *  @type string\n\t\t\t *  @default \n\t\t\t *\n\t\t\t *  @dtopt Language\n\t\t\t *  @name DataTable.defaults.language.decimal\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    $(document).ready( function() {\n\t\t\t *      $('#example').dataTable( {\n\t\t\t *        \"language\": {\n\t\t\t *          \"decimal\": \",\"\n\t\t\t *          \"thousands\": \".\"\n\t\t\t *        }\n\t\t\t *      } );\n\t\t\t *    } );\n\t\t\t */\n\t\t\t\"sDecimal\": \"\",\n\t\n\t\n\t\t\t/**\n\t\t\t * DataTables has a build in number formatter (`formatNumber`) which is\n\t\t\t * used to format large numbers that are used in the table information.\n\t\t\t * By default a comma is used, but this can be trivially changed to any\n\t\t\t * character you wish with this parameter.\n\t\t\t *  @type string\n\t\t\t *  @default ,\n\t\t\t *\n\t\t\t *  @dtopt Language\n\t\t\t *  @name DataTable.defaults.language.thousands\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    $(document).ready( function() {\n\t\t\t *      $('#example').dataTable( {\n\t\t\t *        \"language\": {\n\t\t\t *          \"thousands\": \"'\"\n\t\t\t *        }\n\t\t\t *      } );\n\t\t\t *    } );\n\t\t\t */\n\t\t\t\"sThousands\": \",\",\n\t\n\t\n\t\t\t/**\n\t\t\t * Detail the action that will be taken when the drop down menu for the\n\t\t\t * pagination length option is changed. The '_MENU_' variable is replaced\n\t\t\t * with a default select list of 10, 25, 50 and 100, and can be replaced\n\t\t\t * with a custom select box if required.\n\t\t\t *  @type string\n\t\t\t *  @default Show _MENU_ entries\n\t\t\t *\n\t\t\t *  @dtopt Language\n\t\t\t *  @name DataTable.defaults.language.lengthMenu\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    // Language change only\n\t\t\t *    $(document).ready( function() {\n\t\t\t *      $('#example').dataTable( {\n\t\t\t *        \"language\": {\n\t\t\t *          \"lengthMenu\": \"Display _MENU_ records\"\n\t\t\t *        }\n\t\t\t *      } );\n\t\t\t *    } );\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    // Language and options change\n\t\t\t *    $(document).ready( function() {\n\t\t\t *      $('#example').dataTable( {\n\t\t\t *        \"language\": {\n\t\t\t *          \"lengthMenu\": 'Display <select>'+\n\t\t\t *            '<option value=\"10\">10</option>'+\n\t\t\t *            '<option value=\"20\">20</option>'+\n\t\t\t *            '<option value=\"30\">30</option>'+\n\t\t\t *            '<option value=\"40\">40</option>'+\n\t\t\t *            '<option value=\"50\">50</option>'+\n\t\t\t *            '<option value=\"-1\">All</option>'+\n\t\t\t *            '</select> records'\n\t\t\t *        }\n\t\t\t *      } );\n\t\t\t *    } );\n\t\t\t */\n\t\t\t\"sLengthMenu\": \"Show _MENU_ entries\",\n\t\n\t\n\t\t\t/**\n\t\t\t * When using Ajax sourced data and during the first draw when DataTables is\n\t\t\t * gathering the data, this message is shown in an empty row in the table to\n\t\t\t * indicate to the end user the the data is being loaded. Note that this\n\t\t\t * parameter is not used when loading data by server-side processing, just\n\t\t\t * Ajax sourced data with client-side processing.\n\t\t\t *  @type string\n\t\t\t *  @default Loading...\n\t\t\t *\n\t\t\t *  @dtopt Language\n\t\t\t *  @name DataTable.defaults.language.loadingRecords\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    $(document).ready( function() {\n\t\t\t *      $('#example').dataTable( {\n\t\t\t *        \"language\": {\n\t\t\t *          \"loadingRecords\": \"Please wait - loading...\"\n\t\t\t *        }\n\t\t\t *      } );\n\t\t\t *    } );\n\t\t\t */\n\t\t\t\"sLoadingRecords\": \"Loading...\",\n\t\n\t\n\t\t\t/**\n\t\t\t * Text which is displayed when the table is processing a user action\n\t\t\t * (usually a sort command or similar).\n\t\t\t *  @type string\n\t\t\t *  @default Processing...\n\t\t\t *\n\t\t\t *  @dtopt Language\n\t\t\t *  @name DataTable.defaults.language.processing\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    $(document).ready( function() {\n\t\t\t *      $('#example').dataTable( {\n\t\t\t *        \"language\": {\n\t\t\t *          \"processing\": \"DataTables is currently busy\"\n\t\t\t *        }\n\t\t\t *      } );\n\t\t\t *    } );\n\t\t\t */\n\t\t\t\"sProcessing\": \"Processing...\",\n\t\n\t\n\t\t\t/**\n\t\t\t * Details the actions that will be taken when the user types into the\n\t\t\t * filtering input text box. The variable \"_INPUT_\", if used in the string,\n\t\t\t * is replaced with the HTML text box for the filtering input allowing\n\t\t\t * control over where it appears in the string. If \"_INPUT_\" is not given\n\t\t\t * then the input box is appended to the string automatically.\n\t\t\t *  @type string\n\t\t\t *  @default Search:\n\t\t\t *\n\t\t\t *  @dtopt Language\n\t\t\t *  @name DataTable.defaults.language.search\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    // Input text box will be appended at the end automatically\n\t\t\t *    $(document).ready( function() {\n\t\t\t *      $('#example').dataTable( {\n\t\t\t *        \"language\": {\n\t\t\t *          \"search\": \"Filter records:\"\n\t\t\t *        }\n\t\t\t *      } );\n\t\t\t *    } );\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    // Specify where the filter should appear\n\t\t\t *    $(document).ready( function() {\n\t\t\t *      $('#example').dataTable( {\n\t\t\t *        \"language\": {\n\t\t\t *          \"search\": \"Apply filter _INPUT_ to table\"\n\t\t\t *        }\n\t\t\t *      } );\n\t\t\t *    } );\n\t\t\t */\n\t\t\t\"sSearch\": \"Search:\",\n\t\n\t\n\t\t\t/**\n\t\t\t * Assign a `placeholder` attribute to the search `input` element\n\t\t\t *  @type string\n\t\t\t *  @default \n\t\t\t *\n\t\t\t *  @dtopt Language\n\t\t\t *  @name DataTable.defaults.language.searchPlaceholder\n\t\t\t */\n\t\t\t\"sSearchPlaceholder\": \"\",\n\t\n\t\n\t\t\t/**\n\t\t\t * All of the language information can be stored in a file on the\n\t\t\t * server-side, which DataTables will look up if this parameter is passed.\n\t\t\t * It must store the URL of the language file, which is in a JSON format,\n\t\t\t * and the object has the same properties as the oLanguage object in the\n\t\t\t * initialiser object (i.e. the above parameters). Please refer to one of\n\t\t\t * the example language files to see how this works in action.\n\t\t\t *  @type string\n\t\t\t *  @default <i>Empty string - i.e. disabled</i>\n\t\t\t *\n\t\t\t *  @dtopt Language\n\t\t\t *  @name DataTable.defaults.language.url\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    $(document).ready( function() {\n\t\t\t *      $('#example').dataTable( {\n\t\t\t *        \"language\": {\n\t\t\t *          \"url\": \"http://www.sprymedia.co.uk/dataTables/lang.txt\"\n\t\t\t *        }\n\t\t\t *      } );\n\t\t\t *    } );\n\t\t\t */\n\t\t\t\"sUrl\": \"\",\n\t\n\t\n\t\t\t/**\n\t\t\t * Text shown inside the table records when the is no information to be\n\t\t\t * displayed after filtering. `emptyTable` is shown when there is simply no\n\t\t\t * information in the table at all (regardless of filtering).\n\t\t\t *  @type string\n\t\t\t *  @default No matching records found\n\t\t\t *\n\t\t\t *  @dtopt Language\n\t\t\t *  @name DataTable.defaults.language.zeroRecords\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    $(document).ready( function() {\n\t\t\t *      $('#example').dataTable( {\n\t\t\t *        \"language\": {\n\t\t\t *          \"zeroRecords\": \"No records to display\"\n\t\t\t *        }\n\t\t\t *      } );\n\t\t\t *    } );\n\t\t\t */\n\t\t\t\"sZeroRecords\": \"No matching records found\"\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * This parameter allows you to have define the global filtering state at\n\t\t * initialisation time. As an object the `search` parameter must be\n\t\t * defined, but all other parameters are optional. When `regex` is true,\n\t\t * the search string will be treated as a regular expression, when false\n\t\t * (default) it will be treated as a straight string. When `smart`\n\t\t * DataTables will use it's smart filtering methods (to word match at\n\t\t * any point in the data), when false this will not be done.\n\t\t *  @namespace\n\t\t *  @extends DataTable.models.oSearch\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @name DataTable.defaults.search\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"search\": {\"search\": \"Initial search\"}\n\t\t *      } );\n\t\t *    } )\n\t\t */\n\t\t\"oSearch\": $.extend( {}, DataTable.models.oSearch ),\n\t\n\t\n\t\t/**\n\t\t * __Deprecated__ The functionality provided by this parameter has now been\n\t\t * superseded by that provided through `ajax`, which should be used instead.\n\t\t *\n\t\t * By default DataTables will look for the property `data` (or `aaData` for\n\t\t * compatibility with DataTables 1.9-) when obtaining data from an Ajax\n\t\t * source or for server-side processing - this parameter allows that\n\t\t * property to be changed. You can use Javascript dotted object notation to\n\t\t * get a data source for multiple levels of nesting.\n\t\t *  @type string\n\t\t *  @default data\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @dtopt Server-side\n\t\t *  @name DataTable.defaults.ajaxDataProp\n\t\t *\n\t\t *  @deprecated 1.10. Please use `ajax` for this functionality now.\n\t\t */\n\t\t\"sAjaxDataProp\": \"data\",\n\t\n\t\n\t\t/**\n\t\t * __Deprecated__ The functionality provided by this parameter has now been\n\t\t * superseded by that provided through `ajax`, which should be used instead.\n\t\t *\n\t\t * You can instruct DataTables to load data from an external\n\t\t * source using this parameter (use aData if you want to pass data in you\n\t\t * already have). Simply provide a url a JSON object can be obtained from.\n\t\t *  @type string\n\t\t *  @default null\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @dtopt Server-side\n\t\t *  @name DataTable.defaults.ajaxSource\n\t\t *\n\t\t *  @deprecated 1.10. Please use `ajax` for this functionality now.\n\t\t */\n\t\t\"sAjaxSource\": null,\n\t\n\t\n\t\t/**\n\t\t * This initialisation variable allows you to specify exactly where in the\n\t\t * DOM you want DataTables to inject the various controls it adds to the page\n\t\t * (for example you might want the pagination controls at the top of the\n\t\t * table). DIV elements (with or without a custom class) can also be added to\n\t\t * aid styling. The follow syntax is used:\n\t\t *   <ul>\n\t\t *     <li>The following options are allowed:\n\t\t *       <ul>\n\t\t *         <li>'l' - Length changing</li>\n\t\t *         <li>'f' - Filtering input</li>\n\t\t *         <li>'t' - The table!</li>\n\t\t *         <li>'i' - Information</li>\n\t\t *         <li>'p' - Pagination</li>\n\t\t *         <li>'r' - pRocessing</li>\n\t\t *       </ul>\n\t\t *     </li>\n\t\t *     <li>The following constants are allowed:\n\t\t *       <ul>\n\t\t *         <li>'H' - jQueryUI theme \"header\" classes ('fg-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix')</li>\n\t\t *         <li>'F' - jQueryUI theme \"footer\" classes ('fg-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix')</li>\n\t\t *       </ul>\n\t\t *     </li>\n\t\t *     <li>The following syntax is expected:\n\t\t *       <ul>\n\t\t *         <li>'&lt;' and '&gt;' - div elements</li>\n\t\t *         <li>'&lt;\"class\" and '&gt;' - div with a class</li>\n\t\t *         <li>'&lt;\"#id\" and '&gt;' - div with an ID</li>\n\t\t *       </ul>\n\t\t *     </li>\n\t\t *     <li>Examples:\n\t\t *       <ul>\n\t\t *         <li>'&lt;\"wrapper\"flipt&gt;'</li>\n\t\t *         <li>'&lt;lf&lt;t&gt;ip&gt;'</li>\n\t\t *       </ul>\n\t\t *     </li>\n\t\t *   </ul>\n\t\t *  @type string\n\t\t *  @default lfrtip <i>(when `jQueryUI` is false)</i> <b>or</b>\n\t\t *    <\"H\"lfr>t<\"F\"ip> <i>(when `jQueryUI` is true)</i>\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @name DataTable.defaults.dom\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"dom\": '&lt;\"top\"i&gt;rt&lt;\"bottom\"flp&gt;&lt;\"clear\"&gt;'\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"sDom\": \"lfrtip\",\n\t\n\t\n\t\t/**\n\t\t * Search delay option. This will throttle full table searches that use the\n\t\t * DataTables provided search input element (it does not effect calls to\n\t\t * `dt-api search()`, providing a delay before the search is made.\n\t\t *  @type integer\n\t\t *  @default 0\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @name DataTable.defaults.searchDelay\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"searchDelay\": 200\n\t\t *      } );\n\t\t *    } )\n\t\t */\n\t\t\"searchDelay\": null,\n\t\n\t\n\t\t/**\n\t\t * DataTables features six different built-in options for the buttons to\n\t\t * display for pagination control:\n\t\t *\n\t\t * * `numbers` - Page number buttons only\n\t\t * * `simple` - 'Previous' and 'Next' buttons only\n\t\t * * 'simple_numbers` - 'Previous' and 'Next' buttons, plus page numbers\n\t\t * * `full` - 'First', 'Previous', 'Next' and 'Last' buttons\n\t\t * * `full_numbers` - 'First', 'Previous', 'Next' and 'Last' buttons, plus page numbers\n\t\t * * `first_last_numbers` - 'First' and 'Last' buttons, plus page numbers\n\t\t *  \n\t\t * Further methods can be added using {@link DataTable.ext.oPagination}.\n\t\t *  @type string\n\t\t *  @default simple_numbers\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @name DataTable.defaults.pagingType\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"pagingType\": \"full_numbers\"\n\t\t *      } );\n\t\t *    } )\n\t\t */\n\t\t\"sPaginationType\": \"simple_numbers\",\n\t\n\t\n\t\t/**\n\t\t * Enable horizontal scrolling. When a table is too wide to fit into a\n\t\t * certain layout, or you have a large number of columns in the table, you\n\t\t * can enable x-scrolling to show the table in a viewport, which can be\n\t\t * scrolled. This property can be `true` which will allow the table to\n\t\t * scroll horizontally when needed, or any CSS unit, or a number (in which\n\t\t * case it will be treated as a pixel measurement). Setting as simply `true`\n\t\t * is recommended.\n\t\t *  @type boolean|string\n\t\t *  @default <i>blank string - i.e. disabled</i>\n\t\t *\n\t\t *  @dtopt Features\n\t\t *  @name DataTable.defaults.scrollX\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"scrollX\": true,\n\t\t *        \"scrollCollapse\": true\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"sScrollX\": \"\",\n\t\n\t\n\t\t/**\n\t\t * This property can be used to force a DataTable to use more width than it\n\t\t * might otherwise do when x-scrolling is enabled. For example if you have a\n\t\t * table which requires to be well spaced, this parameter is useful for\n\t\t * \"over-sizing\" the table, and thus forcing scrolling. This property can by\n\t\t * any CSS unit, or a number (in which case it will be treated as a pixel\n\t\t * measurement).\n\t\t *  @type string\n\t\t *  @default <i>blank string - i.e. disabled</i>\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @name DataTable.defaults.scrollXInner\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"scrollX\": \"100%\",\n\t\t *        \"scrollXInner\": \"110%\"\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"sScrollXInner\": \"\",\n\t\n\t\n\t\t/**\n\t\t * Enable vertical scrolling. Vertical scrolling will constrain the DataTable\n\t\t * to the given height, and enable scrolling for any data which overflows the\n\t\t * current viewport. This can be used as an alternative to paging to display\n\t\t * a lot of data in a small area (although paging and scrolling can both be\n\t\t * enabled at the same time). This property can be any CSS unit, or a number\n\t\t * (in which case it will be treated as a pixel measurement).\n\t\t *  @type string\n\t\t *  @default <i>blank string - i.e. disabled</i>\n\t\t *\n\t\t *  @dtopt Features\n\t\t *  @name DataTable.defaults.scrollY\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"scrollY\": \"200px\",\n\t\t *        \"paginate\": false\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"sScrollY\": \"\",\n\t\n\t\n\t\t/**\n\t\t * __Deprecated__ The functionality provided by this parameter has now been\n\t\t * superseded by that provided through `ajax`, which should be used instead.\n\t\t *\n\t\t * Set the HTTP method that is used to make the Ajax call for server-side\n\t\t * processing or Ajax sourced data.\n\t\t *  @type string\n\t\t *  @default GET\n\t\t *\n\t\t *  @dtopt Options\n\t\t *  @dtopt Server-side\n\t\t *  @name DataTable.defaults.serverMethod\n\t\t *\n\t\t *  @deprecated 1.10. Please use `ajax` for this functionality now.\n\t\t */\n\t\t\"sServerMethod\": \"GET\",\n\t\n\t\n\t\t/**\n\t\t * DataTables makes use of renderers when displaying HTML elements for\n\t\t * a table. These renderers can be added or modified by plug-ins to\n\t\t * generate suitable mark-up for a site. For example the Bootstrap\n\t\t * integration plug-in for DataTables uses a paging button renderer to\n\t\t * display pagination buttons in the mark-up required by Bootstrap.\n\t\t *\n\t\t * For further information about the renderers available see\n\t\t * DataTable.ext.renderer\n\t\t *  @type string|object\n\t\t *  @default null\n\t\t *\n\t\t *  @name DataTable.defaults.renderer\n\t\t *\n\t\t */\n\t\t\"renderer\": null,\n\t\n\t\n\t\t/**\n\t\t * Set the data property name that DataTables should use to get a row's id\n\t\t * to set as the `id` property in the node.\n\t\t *  @type string\n\t\t *  @default DT_RowId\n\t\t *\n\t\t *  @name DataTable.defaults.rowId\n\t\t */\n\t\t\"rowId\": \"DT_RowId\"\n\t};\n\t\n\t_fnHungarianMap( DataTable.defaults );\n\t\n\t\n\t\n\t/*\n\t * Developer note - See note in model.defaults.js about the use of Hungarian\n\t * notation and camel case.\n\t */\n\t\n\t/**\n\t * Column options that can be given to DataTables at initialisation time.\n\t *  @namespace\n\t */\n\tDataTable.defaults.column = {\n\t\t/**\n\t\t * Define which column(s) an order will occur on for this column. This\n\t\t * allows a column's ordering to take multiple columns into account when\n\t\t * doing a sort or use the data from a different column. For example first\n\t\t * name / last name columns make sense to do a multi-column sort over the\n\t\t * two columns.\n\t\t *  @type array|int\n\t\t *  @default null <i>Takes the value of the column index automatically</i>\n\t\t *\n\t\t *  @name DataTable.defaults.column.orderData\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columnDefs`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [\n\t\t *          { \"orderData\": [ 0, 1 ], \"targets\": [ 0 ] },\n\t\t *          { \"orderData\": [ 1, 0 ], \"targets\": [ 1 ] },\n\t\t *          { \"orderData\": 2, \"targets\": [ 2 ] }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columns`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columns\": [\n\t\t *          { \"orderData\": [ 0, 1 ] },\n\t\t *          { \"orderData\": [ 1, 0 ] },\n\t\t *          { \"orderData\": 2 },\n\t\t *          null,\n\t\t *          null\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"aDataSort\": null,\n\t\t\"iDataSort\": -1,\n\t\n\t\n\t\t/**\n\t\t * You can control the default ordering direction, and even alter the\n\t\t * behaviour of the sort handler (i.e. only allow ascending ordering etc)\n\t\t * using this parameter.\n\t\t *  @type array\n\t\t *  @default [ 'asc', 'desc' ]\n\t\t *\n\t\t *  @name DataTable.defaults.column.orderSequence\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columnDefs`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [\n\t\t *          { \"orderSequence\": [ \"asc\" ], \"targets\": [ 1 ] },\n\t\t *          { \"orderSequence\": [ \"desc\", \"asc\", \"asc\" ], \"targets\": [ 2 ] },\n\t\t *          { \"orderSequence\": [ \"desc\" ], \"targets\": [ 3 ] }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columns`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columns\": [\n\t\t *          null,\n\t\t *          { \"orderSequence\": [ \"asc\" ] },\n\t\t *          { \"orderSequence\": [ \"desc\", \"asc\", \"asc\" ] },\n\t\t *          { \"orderSequence\": [ \"desc\" ] },\n\t\t *          null\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"asSorting\": [ 'asc', 'desc' ],\n\t\n\t\n\t\t/**\n\t\t * Enable or disable filtering on the data in this column.\n\t\t *  @type boolean\n\t\t *  @default true\n\t\t *\n\t\t *  @name DataTable.defaults.column.searchable\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columnDefs`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [\n\t\t *          { \"searchable\": false, \"targets\": [ 0 ] }\n\t\t *        ] } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columns`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columns\": [\n\t\t *          { \"searchable\": false },\n\t\t *          null,\n\t\t *          null,\n\t\t *          null,\n\t\t *          null\n\t\t *        ] } );\n\t\t *    } );\n\t\t */\n\t\t\"bSearchable\": true,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable ordering on this column.\n\t\t *  @type boolean\n\t\t *  @default true\n\t\t *\n\t\t *  @name DataTable.defaults.column.orderable\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columnDefs`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [\n\t\t *          { \"orderable\": false, \"targets\": [ 0 ] }\n\t\t *        ] } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columns`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columns\": [\n\t\t *          { \"orderable\": false },\n\t\t *          null,\n\t\t *          null,\n\t\t *          null,\n\t\t *          null\n\t\t *        ] } );\n\t\t *    } );\n\t\t */\n\t\t\"bSortable\": true,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable the display of this column.\n\t\t *  @type boolean\n\t\t *  @default true\n\t\t *\n\t\t *  @name DataTable.defaults.column.visible\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columnDefs`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [\n\t\t *          { \"visible\": false, \"targets\": [ 0 ] }\n\t\t *        ] } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columns`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columns\": [\n\t\t *          { \"visible\": false },\n\t\t *          null,\n\t\t *          null,\n\t\t *          null,\n\t\t *          null\n\t\t *        ] } );\n\t\t *    } );\n\t\t */\n\t\t\"bVisible\": true,\n\t\n\t\n\t\t/**\n\t\t * Developer definable function that is called whenever a cell is created (Ajax source,\n\t\t * etc) or processed for input (DOM source). This can be used as a compliment to mRender\n\t\t * allowing you to modify the DOM element (add background colour for example) when the\n\t\t * element is available.\n\t\t *  @type function\n\t\t *  @param {element} td The TD node that has been created\n\t\t *  @param {*} cellData The Data for the cell\n\t\t *  @param {array|object} rowData The data for the whole row\n\t\t *  @param {int} row The row index for the aoData data store\n\t\t *  @param {int} col The column index for aoColumns\n\t\t *\n\t\t *  @name DataTable.defaults.column.createdCell\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [ {\n\t\t *          \"targets\": [3],\n\t\t *          \"createdCell\": function (td, cellData, rowData, row, col) {\n\t\t *            if ( cellData == \"1.7\" ) {\n\t\t *              $(td).css('color', 'blue')\n\t\t *            }\n\t\t *          }\n\t\t *        } ]\n\t\t *      });\n\t\t *    } );\n\t\t */\n\t\t\"fnCreatedCell\": null,\n\t\n\t\n\t\t/**\n\t\t * This parameter has been replaced by `data` in DataTables to ensure naming\n\t\t * consistency. `dataProp` can still be used, as there is backwards\n\t\t * compatibility in DataTables for this option, but it is strongly\n\t\t * recommended that you use `data` in preference to `dataProp`.\n\t\t *  @name DataTable.defaults.column.dataProp\n\t\t */\n\t\n\t\n\t\t/**\n\t\t * This property can be used to read data from any data source property,\n\t\t * including deeply nested objects / properties. `data` can be given in a\n\t\t * number of different ways which effect its behaviour:\n\t\t *\n\t\t * * `integer` - treated as an array index for the data source. This is the\n\t\t *   default that DataTables uses (incrementally increased for each column).\n\t\t * * `string` - read an object property from the data source. There are\n\t\t *   three 'special' options that can be used in the string to alter how\n\t\t *   DataTables reads the data from the source object:\n\t\t *    * `.` - Dotted Javascript notation. Just as you use a `.` in\n\t\t *      Javascript to read from nested objects, so to can the options\n\t\t *      specified in `data`. For example: `browser.version` or\n\t\t *      `browser.name`. If your object parameter name contains a period, use\n\t\t *      `\\\\` to escape it - i.e. `first\\\\.name`.\n\t\t *    * `[]` - Array notation. DataTables can automatically combine data\n\t\t *      from and array source, joining the data with the characters provided\n\t\t *      between the two brackets. For example: `name[, ]` would provide a\n\t\t *      comma-space separated list from the source array. If no characters\n\t\t *      are provided between the brackets, the original array source is\n\t\t *      returned.\n\t\t *    * `()` - Function notation. Adding `()` to the end of a parameter will\n\t\t *      execute a function of the name given. For example: `browser()` for a\n\t\t *      simple function on the data source, `browser.version()` for a\n\t\t *      function in a nested property or even `browser().version` to get an\n\t\t *      object property if the function called returns an object. Note that\n\t\t *      function notation is recommended for use in `render` rather than\n\t\t *      `data` as it is much simpler to use as a renderer.\n\t\t * * `null` - use the original data source for the row rather than plucking\n\t\t *   data directly from it. This action has effects on two other\n\t\t *   initialisation options:\n\t\t *    * `defaultContent` - When null is given as the `data` option and\n\t\t *      `defaultContent` is specified for the column, the value defined by\n\t\t *      `defaultContent` will be used for the cell.\n\t\t *    * `render` - When null is used for the `data` option and the `render`\n\t\t *      option is specified for the column, the whole data source for the\n\t\t *      row is used for the renderer.\n\t\t * * `function` - the function given will be executed whenever DataTables\n\t\t *   needs to set or get the data for a cell in the column. The function\n\t\t *   takes three parameters:\n\t\t *    * Parameters:\n\t\t *      * `{array|object}` The data source for the row\n\t\t *      * `{string}` The type call data requested - this will be 'set' when\n\t\t *        setting data or 'filter', 'display', 'type', 'sort' or undefined\n\t\t *        when gathering data. Note that when `undefined` is given for the\n\t\t *        type DataTables expects to get the raw data for the object back<\n\t\t *      * `{*}` Data to set when the second parameter is 'set'.\n\t\t *    * Return:\n\t\t *      * The return value from the function is not required when 'set' is\n\t\t *        the type of call, but otherwise the return is what will be used\n\t\t *        for the data requested.\n\t\t *\n\t\t * Note that `data` is a getter and setter option. If you just require\n\t\t * formatting of data for output, you will likely want to use `render` which\n\t\t * is simply a getter and thus simpler to use.\n\t\t *\n\t\t * Note that prior to DataTables 1.9.2 `data` was called `mDataProp`. The\n\t\t * name change reflects the flexibility of this property and is consistent\n\t\t * with the naming of mRender. If 'mDataProp' is given, then it will still\n\t\t * be used by DataTables, as it automatically maps the old name to the new\n\t\t * if required.\n\t\t *\n\t\t *  @type string|int|function|null\n\t\t *  @default null <i>Use automatically calculated column index</i>\n\t\t *\n\t\t *  @name DataTable.defaults.column.data\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    // Read table data from objects\n\t\t *    // JSON structure for each row:\n\t\t *    //   {\n\t\t *    //      \"engine\": {value},\n\t\t *    //      \"browser\": {value},\n\t\t *    //      \"platform\": {value},\n\t\t *    //      \"version\": {value},\n\t\t *    //      \"grade\": {value}\n\t\t *    //   }\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"ajaxSource\": \"sources/objects.txt\",\n\t\t *        \"columns\": [\n\t\t *          { \"data\": \"engine\" },\n\t\t *          { \"data\": \"browser\" },\n\t\t *          { \"data\": \"platform\" },\n\t\t *          { \"data\": \"version\" },\n\t\t *          { \"data\": \"grade\" }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Read information from deeply nested objects\n\t\t *    // JSON structure for each row:\n\t\t *    //   {\n\t\t *    //      \"engine\": {value},\n\t\t *    //      \"browser\": {value},\n\t\t *    //      \"platform\": {\n\t\t *    //         \"inner\": {value}\n\t\t *    //      },\n\t\t *    //      \"details\": [\n\t\t *    //         {value}, {value}\n\t\t *    //      ]\n\t\t *    //   }\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"ajaxSource\": \"sources/deep.txt\",\n\t\t *        \"columns\": [\n\t\t *          { \"data\": \"engine\" },\n\t\t *          { \"data\": \"browser\" },\n\t\t *          { \"data\": \"platform.inner\" },\n\t\t *          { \"data\": \"details.0\" },\n\t\t *          { \"data\": \"details.1\" }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Using `data` as a function to provide different information for\n\t\t *    // sorting, filtering and display. In this case, currency (price)\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [ {\n\t\t *          \"targets\": [ 0 ],\n\t\t *          \"data\": function ( source, type, val ) {\n\t\t *            if (type === 'set') {\n\t\t *              source.price = val;\n\t\t *              // Store the computed dislay and filter values for efficiency\n\t\t *              source.price_display = val==\"\" ? \"\" : \"$\"+numberFormat(val);\n\t\t *              source.price_filter  = val==\"\" ? \"\" : \"$\"+numberFormat(val)+\" \"+val;\n\t\t *              return;\n\t\t *            }\n\t\t *            else if (type === 'display') {\n\t\t *              return source.price_display;\n\t\t *            }\n\t\t *            else if (type === 'filter') {\n\t\t *              return source.price_filter;\n\t\t *            }\n\t\t *            // 'sort', 'type' and undefined all just use the integer\n\t\t *            return source.price;\n\t\t *          }\n\t\t *        } ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Using default content\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [ {\n\t\t *          \"targets\": [ 0 ],\n\t\t *          \"data\": null,\n\t\t *          \"defaultContent\": \"Click to edit\"\n\t\t *        } ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Using array notation - outputting a list from an array\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [ {\n\t\t *          \"targets\": [ 0 ],\n\t\t *          \"data\": \"name[, ]\"\n\t\t *        } ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t */\n\t\t\"mData\": null,\n\t\n\t\n\t\t/**\n\t\t * This property is the rendering partner to `data` and it is suggested that\n\t\t * when you want to manipulate data for display (including filtering,\n\t\t * sorting etc) without altering the underlying data for the table, use this\n\t\t * property. `render` can be considered to be the the read only companion to\n\t\t * `data` which is read / write (then as such more complex). Like `data`\n\t\t * this option can be given in a number of different ways to effect its\n\t\t * behaviour:\n\t\t *\n\t\t * * `integer` - treated as an array index for the data source. This is the\n\t\t *   default that DataTables uses (incrementally increased for each column).\n\t\t * * `string` - read an object property from the data source. There are\n\t\t *   three 'special' options that can be used in the string to alter how\n\t\t *   DataTables reads the data from the source object:\n\t\t *    * `.` - Dotted Javascript notation. Just as you use a `.` in\n\t\t *      Javascript to read from nested objects, so to can the options\n\t\t *      specified in `data`. For example: `browser.version` or\n\t\t *      `browser.name`. If your object parameter name contains a period, use\n\t\t *      `\\\\` to escape it - i.e. `first\\\\.name`.\n\t\t *    * `[]` - Array notation. DataTables can automatically combine data\n\t\t *      from and array source, joining the data with the characters provided\n\t\t *      between the two brackets. For example: `name[, ]` would provide a\n\t\t *      comma-space separated list from the source array. If no characters\n\t\t *      are provided between the brackets, the original array source is\n\t\t *      returned.\n\t\t *    * `()` - Function notation. Adding `()` to the end of a parameter will\n\t\t *      execute a function of the name given. For example: `browser()` for a\n\t\t *      simple function on the data source, `browser.version()` for a\n\t\t *      function in a nested property or even `browser().version` to get an\n\t\t *      object property if the function called returns an object.\n\t\t * * `object` - use different data for the different data types requested by\n\t\t *   DataTables ('filter', 'display', 'type' or 'sort'). The property names\n\t\t *   of the object is the data type the property refers to and the value can\n\t\t *   defined using an integer, string or function using the same rules as\n\t\t *   `render` normally does. Note that an `_` option _must_ be specified.\n\t\t *   This is the default value to use if you haven't specified a value for\n\t\t *   the data type requested by DataTables.\n\t\t * * `function` - the function given will be executed whenever DataTables\n\t\t *   needs to set or get the data for a cell in the column. The function\n\t\t *   takes three parameters:\n\t\t *    * Parameters:\n\t\t *      * {array|object} The data source for the row (based on `data`)\n\t\t *      * {string} The type call data requested - this will be 'filter',\n\t\t *        'display', 'type' or 'sort'.\n\t\t *      * {array|object} The full data source for the row (not based on\n\t\t *        `data`)\n\t\t *    * Return:\n\t\t *      * The return value from the function is what will be used for the\n\t\t *        data requested.\n\t\t *\n\t\t *  @type string|int|function|object|null\n\t\t *  @default null Use the data source value.\n\t\t *\n\t\t *  @name DataTable.defaults.column.render\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    // Create a comma separated list from an array of objects\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"ajaxSource\": \"sources/deep.txt\",\n\t\t *        \"columns\": [\n\t\t *          { \"data\": \"engine\" },\n\t\t *          { \"data\": \"browser\" },\n\t\t *          {\n\t\t *            \"data\": \"platform\",\n\t\t *            \"render\": \"[, ].name\"\n\t\t *          }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Execute a function to obtain data\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [ {\n\t\t *          \"targets\": [ 0 ],\n\t\t *          \"data\": null, // Use the full data source object for the renderer's source\n\t\t *          \"render\": \"browserName()\"\n\t\t *        } ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // As an object, extracting different data for the different types\n\t\t *    // This would be used with a data source such as:\n\t\t *    //   { \"phone\": 5552368, \"phone_filter\": \"5552368 555-2368\", \"phone_display\": \"555-2368\" }\n\t\t *    // Here the `phone` integer is used for sorting and type detection, while `phone_filter`\n\t\t *    // (which has both forms) is used for filtering for if a user inputs either format, while\n\t\t *    // the formatted phone number is the one that is shown in the table.\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [ {\n\t\t *          \"targets\": [ 0 ],\n\t\t *          \"data\": null, // Use the full data source object for the renderer's source\n\t\t *          \"render\": {\n\t\t *            \"_\": \"phone\",\n\t\t *            \"filter\": \"phone_filter\",\n\t\t *            \"display\": \"phone_display\"\n\t\t *          }\n\t\t *        } ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Use as a function to create a link from the data source\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [ {\n\t\t *          \"targets\": [ 0 ],\n\t\t *          \"data\": \"download_link\",\n\t\t *          \"render\": function ( data, type, full ) {\n\t\t *            return '<a href=\"'+data+'\">Download</a>';\n\t\t *          }\n\t\t *        } ]\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"mRender\": null,\n\t\n\t\n\t\t/**\n\t\t * Change the cell type created for the column - either TD cells or TH cells. This\n\t\t * can be useful as TH cells have semantic meaning in the table body, allowing them\n\t\t * to act as a header for a row (you may wish to add scope='row' to the TH elements).\n\t\t *  @type string\n\t\t *  @default td\n\t\t *\n\t\t *  @name DataTable.defaults.column.cellType\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    // Make the first column use TH cells\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [ {\n\t\t *          \"targets\": [ 0 ],\n\t\t *          \"cellType\": \"th\"\n\t\t *        } ]\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"sCellType\": \"td\",\n\t\n\t\n\t\t/**\n\t\t * Class to give to each cell in this column.\n\t\t *  @type string\n\t\t *  @default <i>Empty string</i>\n\t\t *\n\t\t *  @name DataTable.defaults.column.class\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columnDefs`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [\n\t\t *          { \"class\": \"my_class\", \"targets\": [ 0 ] }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columns`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columns\": [\n\t\t *          { \"class\": \"my_class\" },\n\t\t *          null,\n\t\t *          null,\n\t\t *          null,\n\t\t *          null\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"sClass\": \"\",\n\t\n\t\t/**\n\t\t * When DataTables calculates the column widths to assign to each column,\n\t\t * it finds the longest string in each column and then constructs a\n\t\t * temporary table and reads the widths from that. The problem with this\n\t\t * is that \"mmm\" is much wider then \"iiii\", but the latter is a longer\n\t\t * string - thus the calculation can go wrong (doing it properly and putting\n\t\t * it into an DOM object and measuring that is horribly(!) slow). Thus as\n\t\t * a \"work around\" we provide this option. It will append its value to the\n\t\t * text that is found to be the longest string for the column - i.e. padding.\n\t\t * Generally you shouldn't need this!\n\t\t *  @type string\n\t\t *  @default <i>Empty string<i>\n\t\t *\n\t\t *  @name DataTable.defaults.column.contentPadding\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columns`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columns\": [\n\t\t *          null,\n\t\t *          null,\n\t\t *          null,\n\t\t *          {\n\t\t *            \"contentPadding\": \"mmm\"\n\t\t *          }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"sContentPadding\": \"\",\n\t\n\t\n\t\t/**\n\t\t * Allows a default value to be given for a column's data, and will be used\n\t\t * whenever a null data source is encountered (this can be because `data`\n\t\t * is set to null, or because the data source itself is null).\n\t\t *  @type string\n\t\t *  @default null\n\t\t *\n\t\t *  @name DataTable.defaults.column.defaultContent\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columnDefs`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [\n\t\t *          {\n\t\t *            \"data\": null,\n\t\t *            \"defaultContent\": \"Edit\",\n\t\t *            \"targets\": [ -1 ]\n\t\t *          }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columns`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columns\": [\n\t\t *          null,\n\t\t *          null,\n\t\t *          null,\n\t\t *          {\n\t\t *            \"data\": null,\n\t\t *            \"defaultContent\": \"Edit\"\n\t\t *          }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"sDefaultContent\": null,\n\t\n\t\n\t\t/**\n\t\t * This parameter is only used in DataTables' server-side processing. It can\n\t\t * be exceptionally useful to know what columns are being displayed on the\n\t\t * client side, and to map these to database fields. When defined, the names\n\t\t * also allow DataTables to reorder information from the server if it comes\n\t\t * back in an unexpected order (i.e. if you switch your columns around on the\n\t\t * client-side, your server-side code does not also need updating).\n\t\t *  @type string\n\t\t *  @default <i>Empty string</i>\n\t\t *\n\t\t *  @name DataTable.defaults.column.name\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columnDefs`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [\n\t\t *          { \"name\": \"engine\", \"targets\": [ 0 ] },\n\t\t *          { \"name\": \"browser\", \"targets\": [ 1 ] },\n\t\t *          { \"name\": \"platform\", \"targets\": [ 2 ] },\n\t\t *          { \"name\": \"version\", \"targets\": [ 3 ] },\n\t\t *          { \"name\": \"grade\", \"targets\": [ 4 ] }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columns`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columns\": [\n\t\t *          { \"name\": \"engine\" },\n\t\t *          { \"name\": \"browser\" },\n\t\t *          { \"name\": \"platform\" },\n\t\t *          { \"name\": \"version\" },\n\t\t *          { \"name\": \"grade\" }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"sName\": \"\",\n\t\n\t\n\t\t/**\n\t\t * Defines a data source type for the ordering which can be used to read\n\t\t * real-time information from the table (updating the internally cached\n\t\t * version) prior to ordering. This allows ordering to occur on user\n\t\t * editable elements such as form inputs.\n\t\t *  @type string\n\t\t *  @default std\n\t\t *\n\t\t *  @name DataTable.defaults.column.orderDataType\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columnDefs`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [\n\t\t *          { \"orderDataType\": \"dom-text\", \"targets\": [ 2, 3 ] },\n\t\t *          { \"type\": \"numeric\", \"targets\": [ 3 ] },\n\t\t *          { \"orderDataType\": \"dom-select\", \"targets\": [ 4 ] },\n\t\t *          { \"orderDataType\": \"dom-checkbox\", \"targets\": [ 5 ] }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columns`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columns\": [\n\t\t *          null,\n\t\t *          null,\n\t\t *          { \"orderDataType\": \"dom-text\" },\n\t\t *          { \"orderDataType\": \"dom-text\", \"type\": \"numeric\" },\n\t\t *          { \"orderDataType\": \"dom-select\" },\n\t\t *          { \"orderDataType\": \"dom-checkbox\" }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"sSortDataType\": \"std\",\n\t\n\t\n\t\t/**\n\t\t * The title of this column.\n\t\t *  @type string\n\t\t *  @default null <i>Derived from the 'TH' value for this column in the\n\t\t *    original HTML table.</i>\n\t\t *\n\t\t *  @name DataTable.defaults.column.title\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columnDefs`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [\n\t\t *          { \"title\": \"My column title\", \"targets\": [ 0 ] }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columns`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columns\": [\n\t\t *          { \"title\": \"My column title\" },\n\t\t *          null,\n\t\t *          null,\n\t\t *          null,\n\t\t *          null\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"sTitle\": null,\n\t\n\t\n\t\t/**\n\t\t * The type allows you to specify how the data for this column will be\n\t\t * ordered. Four types (string, numeric, date and html (which will strip\n\t\t * HTML tags before ordering)) are currently available. Note that only date\n\t\t * formats understood by Javascript's Date() object will be accepted as type\n\t\t * date. For example: \"Mar 26, 2008 5:03 PM\". May take the values: 'string',\n\t\t * 'numeric', 'date' or 'html' (by default). Further types can be adding\n\t\t * through plug-ins.\n\t\t *  @type string\n\t\t *  @default null <i>Auto-detected from raw data</i>\n\t\t *\n\t\t *  @name DataTable.defaults.column.type\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columnDefs`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [\n\t\t *          { \"type\": \"html\", \"targets\": [ 0 ] }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columns`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columns\": [\n\t\t *          { \"type\": \"html\" },\n\t\t *          null,\n\t\t *          null,\n\t\t *          null,\n\t\t *          null\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"sType\": null,\n\t\n\t\n\t\t/**\n\t\t * Defining the width of the column, this parameter may take any CSS value\n\t\t * (3em, 20px etc). DataTables applies 'smart' widths to columns which have not\n\t\t * been given a specific width through this interface ensuring that the table\n\t\t * remains readable.\n\t\t *  @type string\n\t\t *  @default null <i>Automatic</i>\n\t\t *\n\t\t *  @name DataTable.defaults.column.width\n\t\t *  @dtopt Columns\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columnDefs`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columnDefs\": [\n\t\t *          { \"width\": \"20%\", \"targets\": [ 0 ] }\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t *\n\t\t *  @example\n\t\t *    // Using `columns`\n\t\t *    $(document).ready( function() {\n\t\t *      $('#example').dataTable( {\n\t\t *        \"columns\": [\n\t\t *          { \"width\": \"20%\" },\n\t\t *          null,\n\t\t *          null,\n\t\t *          null,\n\t\t *          null\n\t\t *        ]\n\t\t *      } );\n\t\t *    } );\n\t\t */\n\t\t\"sWidth\": null\n\t};\n\t\n\t_fnHungarianMap( DataTable.defaults.column );\n\t\n\t\n\t\n\t/**\n\t * DataTables settings object - this holds all the information needed for a\n\t * given table, including configuration, data and current application of the\n\t * table options. DataTables does not have a single instance for each DataTable\n\t * with the settings attached to that instance, but rather instances of the\n\t * DataTable \"class\" are created on-the-fly as needed (typically by a\n\t * $().dataTable() call) and the settings object is then applied to that\n\t * instance.\n\t *\n\t * Note that this object is related to {@link DataTable.defaults} but this\n\t * one is the internal data store for DataTables's cache of columns. It should\n\t * NOT be manipulated outside of DataTables. Any configuration should be done\n\t * through the initialisation options.\n\t *  @namespace\n\t *  @todo Really should attach the settings object to individual instances so we\n\t *    don't need to create new instances on each $().dataTable() call (if the\n\t *    table already exists). It would also save passing oSettings around and\n\t *    into every single function. However, this is a very significant\n\t *    architecture change for DataTables and will almost certainly break\n\t *    backwards compatibility with older installations. This is something that\n\t *    will be done in 2.0.\n\t */\n\tDataTable.models.oSettings = {\n\t\t/**\n\t\t * Primary features of DataTables and their enablement state.\n\t\t *  @namespace\n\t\t */\n\t\t\"oFeatures\": {\n\t\n\t\t\t/**\n\t\t\t * Flag to say if DataTables should automatically try to calculate the\n\t\t\t * optimum table and columns widths (true) or not (false).\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @type boolean\n\t\t\t */\n\t\t\t\"bAutoWidth\": null,\n\t\n\t\t\t/**\n\t\t\t * Delay the creation of TR and TD elements until they are actually\n\t\t\t * needed by a driven page draw. This can give a significant speed\n\t\t\t * increase for Ajax source and Javascript source data, but makes no\n\t\t\t * difference at all fro DOM and server-side processing tables.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @type boolean\n\t\t\t */\n\t\t\t\"bDeferRender\": null,\n\t\n\t\t\t/**\n\t\t\t * Enable filtering on the table or not. Note that if this is disabled\n\t\t\t * then there is no filtering at all on the table, including fnFilter.\n\t\t\t * To just remove the filtering input use sDom and remove the 'f' option.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @type boolean\n\t\t\t */\n\t\t\t\"bFilter\": null,\n\t\n\t\t\t/**\n\t\t\t * Table information element (the 'Showing x of y records' div) enable\n\t\t\t * flag.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @type boolean\n\t\t\t */\n\t\t\t\"bInfo\": null,\n\t\n\t\t\t/**\n\t\t\t * Present a user control allowing the end user to change the page size\n\t\t\t * when pagination is enabled.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @type boolean\n\t\t\t */\n\t\t\t\"bLengthChange\": null,\n\t\n\t\t\t/**\n\t\t\t * Pagination enabled or not. Note that if this is disabled then length\n\t\t\t * changing must also be disabled.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @type boolean\n\t\t\t */\n\t\t\t\"bPaginate\": null,\n\t\n\t\t\t/**\n\t\t\t * Processing indicator enable flag whenever DataTables is enacting a\n\t\t\t * user request - typically an Ajax request for server-side processing.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @type boolean\n\t\t\t */\n\t\t\t\"bProcessing\": null,\n\t\n\t\t\t/**\n\t\t\t * Server-side processing enabled flag - when enabled DataTables will\n\t\t\t * get all data from the server for every draw - there is no filtering,\n\t\t\t * sorting or paging done on the client-side.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @type boolean\n\t\t\t */\n\t\t\t\"bServerSide\": null,\n\t\n\t\t\t/**\n\t\t\t * Sorting enablement flag.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @type boolean\n\t\t\t */\n\t\t\t\"bSort\": null,\n\t\n\t\t\t/**\n\t\t\t * Multi-column sorting\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @type boolean\n\t\t\t */\n\t\t\t\"bSortMulti\": null,\n\t\n\t\t\t/**\n\t\t\t * Apply a class to the columns which are being sorted to provide a\n\t\t\t * visual highlight or not. This can slow things down when enabled since\n\t\t\t * there is a lot of DOM interaction.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @type boolean\n\t\t\t */\n\t\t\t\"bSortClasses\": null,\n\t\n\t\t\t/**\n\t\t\t * State saving enablement flag.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @type boolean\n\t\t\t */\n\t\t\t\"bStateSave\": null\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * Scrolling settings for a table.\n\t\t *  @namespace\n\t\t */\n\t\t\"oScroll\": {\n\t\t\t/**\n\t\t\t * When the table is shorter in height than sScrollY, collapse the\n\t\t\t * table container down to the height of the table (when true).\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @type boolean\n\t\t\t */\n\t\t\t\"bCollapse\": null,\n\t\n\t\t\t/**\n\t\t\t * Width of the scrollbar for the web-browser's platform. Calculated\n\t\t\t * during table initialisation.\n\t\t\t *  @type int\n\t\t\t *  @default 0\n\t\t\t */\n\t\t\t\"iBarWidth\": 0,\n\t\n\t\t\t/**\n\t\t\t * Viewport width for horizontal scrolling. Horizontal scrolling is\n\t\t\t * disabled if an empty string.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @type string\n\t\t\t */\n\t\t\t\"sX\": null,\n\t\n\t\t\t/**\n\t\t\t * Width to expand the table to when using x-scrolling. Typically you\n\t\t\t * should not need to use this.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @type string\n\t\t\t *  @deprecated\n\t\t\t */\n\t\t\t\"sXInner\": null,\n\t\n\t\t\t/**\n\t\t\t * Viewport height for vertical scrolling. Vertical scrolling is disabled\n\t\t\t * if an empty string.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @type string\n\t\t\t */\n\t\t\t\"sY\": null\n\t\t},\n\t\n\t\t/**\n\t\t * Language information for the table.\n\t\t *  @namespace\n\t\t *  @extends DataTable.defaults.oLanguage\n\t\t */\n\t\t\"oLanguage\": {\n\t\t\t/**\n\t\t\t * Information callback function. See\n\t\t\t * {@link DataTable.defaults.fnInfoCallback}\n\t\t\t *  @type function\n\t\t\t *  @default null\n\t\t\t */\n\t\t\t\"fnInfoCallback\": null\n\t\t},\n\t\n\t\t/**\n\t\t * Browser support parameters\n\t\t *  @namespace\n\t\t */\n\t\t\"oBrowser\": {\n\t\t\t/**\n\t\t\t * Indicate if the browser incorrectly calculates width:100% inside a\n\t\t\t * scrolling element (IE6/7)\n\t\t\t *  @type boolean\n\t\t\t *  @default false\n\t\t\t */\n\t\t\t\"bScrollOversize\": false,\n\t\n\t\t\t/**\n\t\t\t * Determine if the vertical scrollbar is on the right or left of the\n\t\t\t * scrolling container - needed for rtl language layout, although not\n\t\t\t * all browsers move the scrollbar (Safari).\n\t\t\t *  @type boolean\n\t\t\t *  @default false\n\t\t\t */\n\t\t\t\"bScrollbarLeft\": false,\n\t\n\t\t\t/**\n\t\t\t * Flag for if `getBoundingClientRect` is fully supported or not\n\t\t\t *  @type boolean\n\t\t\t *  @default false\n\t\t\t */\n\t\t\t\"bBounding\": false,\n\t\n\t\t\t/**\n\t\t\t * Browser scrollbar width\n\t\t\t *  @type integer\n\t\t\t *  @default 0\n\t\t\t */\n\t\t\t\"barWidth\": 0\n\t\t},\n\t\n\t\n\t\t\"ajax\": null,\n\t\n\t\n\t\t/**\n\t\t * Array referencing the nodes which are used for the features. The\n\t\t * parameters of this object match what is allowed by sDom - i.e.\n\t\t *   <ul>\n\t\t *     <li>'l' - Length changing</li>\n\t\t *     <li>'f' - Filtering input</li>\n\t\t *     <li>'t' - The table!</li>\n\t\t *     <li>'i' - Information</li>\n\t\t *     <li>'p' - Pagination</li>\n\t\t *     <li>'r' - pRocessing</li>\n\t\t *   </ul>\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aanFeatures\": [],\n\t\n\t\t/**\n\t\t * Store data information - see {@link DataTable.models.oRow} for detailed\n\t\t * information.\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoData\": [],\n\t\n\t\t/**\n\t\t * Array of indexes which are in the current display (after filtering etc)\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aiDisplay\": [],\n\t\n\t\t/**\n\t\t * Array of indexes for display - no filtering\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aiDisplayMaster\": [],\n\t\n\t\t/**\n\t\t * Map of row ids to data indexes\n\t\t *  @type object\n\t\t *  @default {}\n\t\t */\n\t\t\"aIds\": {},\n\t\n\t\t/**\n\t\t * Store information about each column that is in use\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoColumns\": [],\n\t\n\t\t/**\n\t\t * Store information about the table's header\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoHeader\": [],\n\t\n\t\t/**\n\t\t * Store information about the table's footer\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoFooter\": [],\n\t\n\t\t/**\n\t\t * Store the applied global search information in case we want to force a\n\t\t * research or compare the old search to a new one.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t *  @namespace\n\t\t *  @extends DataTable.models.oSearch\n\t\t */\n\t\t\"oPreviousSearch\": {},\n\t\n\t\t/**\n\t\t * Store the applied search for each column - see\n\t\t * {@link DataTable.models.oSearch} for the format that is used for the\n\t\t * filtering information for each column.\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoPreSearchCols\": [],\n\t\n\t\t/**\n\t\t * Sorting that is applied to the table. Note that the inner arrays are\n\t\t * used in the following manner:\n\t\t * <ul>\n\t\t *   <li>Index 0 - column number</li>\n\t\t *   <li>Index 1 - current sorting direction</li>\n\t\t * </ul>\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t *  @type array\n\t\t *  @todo These inner arrays should really be objects\n\t\t */\n\t\t\"aaSorting\": null,\n\t\n\t\t/**\n\t\t * Sorting that is always applied to the table (i.e. prefixed in front of\n\t\t * aaSorting).\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aaSortingFixed\": [],\n\t\n\t\t/**\n\t\t * Classes to use for the striping of a table.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"asStripeClasses\": null,\n\t\n\t\t/**\n\t\t * If restoring a table - we should restore its striping classes as well\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"asDestroyStripes\": [],\n\t\n\t\t/**\n\t\t * If restoring a table - we should restore its width\n\t\t *  @type int\n\t\t *  @default 0\n\t\t */\n\t\t\"sDestroyWidth\": 0,\n\t\n\t\t/**\n\t\t * Callback functions array for every time a row is inserted (i.e. on a draw).\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoRowCallback\": [],\n\t\n\t\t/**\n\t\t * Callback functions for the header on each draw.\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoHeaderCallback\": [],\n\t\n\t\t/**\n\t\t * Callback function for the footer on each draw.\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoFooterCallback\": [],\n\t\n\t\t/**\n\t\t * Array of callback functions for draw callback functions\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoDrawCallback\": [],\n\t\n\t\t/**\n\t\t * Array of callback functions for row created function\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoRowCreatedCallback\": [],\n\t\n\t\t/**\n\t\t * Callback functions for just before the table is redrawn. A return of\n\t\t * false will be used to cancel the draw.\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoPreDrawCallback\": [],\n\t\n\t\t/**\n\t\t * Callback functions for when the table has been initialised.\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoInitComplete\": [],\n\t\n\t\n\t\t/**\n\t\t * Callbacks for modifying the settings to be stored for state saving, prior to\n\t\t * saving state.\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoStateSaveParams\": [],\n\t\n\t\t/**\n\t\t * Callbacks for modifying the settings that have been stored for state saving\n\t\t * prior to using the stored values to restore the state.\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoStateLoadParams\": [],\n\t\n\t\t/**\n\t\t * Callbacks for operating on the settings object once the saved state has been\n\t\t * loaded\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoStateLoaded\": [],\n\t\n\t\t/**\n\t\t * Cache the table ID for quick access\n\t\t *  @type string\n\t\t *  @default <i>Empty string</i>\n\t\t */\n\t\t\"sTableId\": \"\",\n\t\n\t\t/**\n\t\t * The TABLE node for the main table\n\t\t *  @type node\n\t\t *  @default null\n\t\t */\n\t\t\"nTable\": null,\n\t\n\t\t/**\n\t\t * Permanent ref to the thead element\n\t\t *  @type node\n\t\t *  @default null\n\t\t */\n\t\t\"nTHead\": null,\n\t\n\t\t/**\n\t\t * Permanent ref to the tfoot element - if it exists\n\t\t *  @type node\n\t\t *  @default null\n\t\t */\n\t\t\"nTFoot\": null,\n\t\n\t\t/**\n\t\t * Permanent ref to the tbody element\n\t\t *  @type node\n\t\t *  @default null\n\t\t */\n\t\t\"nTBody\": null,\n\t\n\t\t/**\n\t\t * Cache the wrapper node (contains all DataTables controlled elements)\n\t\t *  @type node\n\t\t *  @default null\n\t\t */\n\t\t\"nTableWrapper\": null,\n\t\n\t\t/**\n\t\t * Indicate if when using server-side processing the loading of data\n\t\t * should be deferred until the second draw.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t *  @type boolean\n\t\t *  @default false\n\t\t */\n\t\t\"bDeferLoading\": false,\n\t\n\t\t/**\n\t\t * Indicate if all required information has been read in\n\t\t *  @type boolean\n\t\t *  @default false\n\t\t */\n\t\t\"bInitialised\": false,\n\t\n\t\t/**\n\t\t * Information about open rows. Each object in the array has the parameters\n\t\t * 'nTr' and 'nParent'\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoOpenRows\": [],\n\t\n\t\t/**\n\t\t * Dictate the positioning of DataTables' control elements - see\n\t\t * {@link DataTable.model.oInit.sDom}.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t *  @type string\n\t\t *  @default null\n\t\t */\n\t\t\"sDom\": null,\n\t\n\t\t/**\n\t\t * Search delay (in mS)\n\t\t *  @type integer\n\t\t *  @default null\n\t\t */\n\t\t\"searchDelay\": null,\n\t\n\t\t/**\n\t\t * Which type of pagination should be used.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t *  @type string\n\t\t *  @default two_button\n\t\t */\n\t\t\"sPaginationType\": \"two_button\",\n\t\n\t\t/**\n\t\t * The state duration (for `stateSave`) in seconds.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t *  @type int\n\t\t *  @default 0\n\t\t */\n\t\t\"iStateDuration\": 0,\n\t\n\t\t/**\n\t\t * Array of callback functions for state saving. Each array element is an\n\t\t * object with the following parameters:\n\t\t *   <ul>\n\t\t *     <li>function:fn - function to call. Takes two parameters, oSettings\n\t\t *       and the JSON string to save that has been thus far created. Returns\n\t\t *       a JSON string to be inserted into a json object\n\t\t *       (i.e. '\"param\": [ 0, 1, 2]')</li>\n\t\t *     <li>string:sName - name of callback</li>\n\t\t *   </ul>\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoStateSave\": [],\n\t\n\t\t/**\n\t\t * Array of callback functions for state loading. Each array element is an\n\t\t * object with the following parameters:\n\t\t *   <ul>\n\t\t *     <li>function:fn - function to call. Takes two parameters, oSettings\n\t\t *       and the object stored. May return false to cancel state loading</li>\n\t\t *     <li>string:sName - name of callback</li>\n\t\t *   </ul>\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoStateLoad\": [],\n\t\n\t\t/**\n\t\t * State that was saved. Useful for back reference\n\t\t *  @type object\n\t\t *  @default null\n\t\t */\n\t\t\"oSavedState\": null,\n\t\n\t\t/**\n\t\t * State that was loaded. Useful for back reference\n\t\t *  @type object\n\t\t *  @default null\n\t\t */\n\t\t\"oLoadedState\": null,\n\t\n\t\t/**\n\t\t * Source url for AJAX data for the table.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t *  @type string\n\t\t *  @default null\n\t\t */\n\t\t\"sAjaxSource\": null,\n\t\n\t\t/**\n\t\t * Property from a given object from which to read the table data from. This\n\t\t * can be an empty string (when not server-side processing), in which case\n\t\t * it is  assumed an an array is given directly.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t *  @type string\n\t\t */\n\t\t\"sAjaxDataProp\": null,\n\t\n\t\t/**\n\t\t * Note if draw should be blocked while getting data\n\t\t *  @type boolean\n\t\t *  @default true\n\t\t */\n\t\t\"bAjaxDataGet\": true,\n\t\n\t\t/**\n\t\t * The last jQuery XHR object that was used for server-side data gathering.\n\t\t * This can be used for working with the XHR information in one of the\n\t\t * callbacks\n\t\t *  @type object\n\t\t *  @default null\n\t\t */\n\t\t\"jqXHR\": null,\n\t\n\t\t/**\n\t\t * JSON returned from the server in the last Ajax request\n\t\t *  @type object\n\t\t *  @default undefined\n\t\t */\n\t\t\"json\": undefined,\n\t\n\t\t/**\n\t\t * Data submitted as part of the last Ajax request\n\t\t *  @type object\n\t\t *  @default undefined\n\t\t */\n\t\t\"oAjaxData\": undefined,\n\t\n\t\t/**\n\t\t * Function to get the server-side data.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t *  @type function\n\t\t */\n\t\t\"fnServerData\": null,\n\t\n\t\t/**\n\t\t * Functions which are called prior to sending an Ajax request so extra\n\t\t * parameters can easily be sent to the server\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoServerParams\": [],\n\t\n\t\t/**\n\t\t * Send the XHR HTTP method - GET or POST (could be PUT or DELETE if\n\t\t * required).\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t *  @type string\n\t\t */\n\t\t\"sServerMethod\": null,\n\t\n\t\t/**\n\t\t * Format numbers for display.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t *  @type function\n\t\t */\n\t\t\"fnFormatNumber\": null,\n\t\n\t\t/**\n\t\t * List of options that can be used for the user selectable length menu.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aLengthMenu\": null,\n\t\n\t\t/**\n\t\t * Counter for the draws that the table does. Also used as a tracker for\n\t\t * server-side processing\n\t\t *  @type int\n\t\t *  @default 0\n\t\t */\n\t\t\"iDraw\": 0,\n\t\n\t\t/**\n\t\t * Indicate if a redraw is being done - useful for Ajax\n\t\t *  @type boolean\n\t\t *  @default false\n\t\t */\n\t\t\"bDrawing\": false,\n\t\n\t\t/**\n\t\t * Draw index (iDraw) of the last error when parsing the returned data\n\t\t *  @type int\n\t\t *  @default -1\n\t\t */\n\t\t\"iDrawError\": -1,\n\t\n\t\t/**\n\t\t * Paging display length\n\t\t *  @type int\n\t\t *  @default 10\n\t\t */\n\t\t\"_iDisplayLength\": 10,\n\t\n\t\t/**\n\t\t * Paging start point - aiDisplay index\n\t\t *  @type int\n\t\t *  @default 0\n\t\t */\n\t\t\"_iDisplayStart\": 0,\n\t\n\t\t/**\n\t\t * Server-side processing - number of records in the result set\n\t\t * (i.e. before filtering), Use fnRecordsTotal rather than\n\t\t * this property to get the value of the number of records, regardless of\n\t\t * the server-side processing setting.\n\t\t *  @type int\n\t\t *  @default 0\n\t\t *  @private\n\t\t */\n\t\t\"_iRecordsTotal\": 0,\n\t\n\t\t/**\n\t\t * Server-side processing - number of records in the current display set\n\t\t * (i.e. after filtering). Use fnRecordsDisplay rather than\n\t\t * this property to get the value of the number of records, regardless of\n\t\t * the server-side processing setting.\n\t\t *  @type boolean\n\t\t *  @default 0\n\t\t *  @private\n\t\t */\n\t\t\"_iRecordsDisplay\": 0,\n\t\n\t\t/**\n\t\t * The classes to use for the table\n\t\t *  @type object\n\t\t *  @default {}\n\t\t */\n\t\t\"oClasses\": {},\n\t\n\t\t/**\n\t\t * Flag attached to the settings object so you can check in the draw\n\t\t * callback if filtering has been done in the draw. Deprecated in favour of\n\t\t * events.\n\t\t *  @type boolean\n\t\t *  @default false\n\t\t *  @deprecated\n\t\t */\n\t\t\"bFiltered\": false,\n\t\n\t\t/**\n\t\t * Flag attached to the settings object so you can check in the draw\n\t\t * callback if sorting has been done in the draw. Deprecated in favour of\n\t\t * events.\n\t\t *  @type boolean\n\t\t *  @default false\n\t\t *  @deprecated\n\t\t */\n\t\t\"bSorted\": false,\n\t\n\t\t/**\n\t\t * Indicate that if multiple rows are in the header and there is more than\n\t\t * one unique cell per column, if the top one (true) or bottom one (false)\n\t\t * should be used for sorting / title by DataTables.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t *  @type boolean\n\t\t */\n\t\t\"bSortCellsTop\": null,\n\t\n\t\t/**\n\t\t * Initialisation object that is used for the table\n\t\t *  @type object\n\t\t *  @default null\n\t\t */\n\t\t\"oInit\": null,\n\t\n\t\t/**\n\t\t * Destroy callback functions - for plug-ins to attach themselves to the\n\t\t * destroy so they can clean up markup and events.\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aoDestroyCallback\": [],\n\t\n\t\n\t\t/**\n\t\t * Get the number of records in the current record set, before filtering\n\t\t *  @type function\n\t\t */\n\t\t\"fnRecordsTotal\": function ()\n\t\t{\n\t\t\treturn _fnDataSource( this ) == 'ssp' ?\n\t\t\t\tthis._iRecordsTotal * 1 :\n\t\t\t\tthis.aiDisplayMaster.length;\n\t\t},\n\t\n\t\t/**\n\t\t * Get the number of records in the current record set, after filtering\n\t\t *  @type function\n\t\t */\n\t\t\"fnRecordsDisplay\": function ()\n\t\t{\n\t\t\treturn _fnDataSource( this ) == 'ssp' ?\n\t\t\t\tthis._iRecordsDisplay * 1 :\n\t\t\t\tthis.aiDisplay.length;\n\t\t},\n\t\n\t\t/**\n\t\t * Get the display end point - aiDisplay index\n\t\t *  @type function\n\t\t */\n\t\t\"fnDisplayEnd\": function ()\n\t\t{\n\t\t\tvar\n\t\t\t\tlen      = this._iDisplayLength,\n\t\t\t\tstart    = this._iDisplayStart,\n\t\t\t\tcalc     = start + len,\n\t\t\t\trecords  = this.aiDisplay.length,\n\t\t\t\tfeatures = this.oFeatures,\n\t\t\t\tpaginate = features.bPaginate;\n\t\n\t\t\tif ( features.bServerSide ) {\n\t\t\t\treturn paginate === false || len === -1 ?\n\t\t\t\t\tstart + records :\n\t\t\t\t\tMath.min( start+len, this._iRecordsDisplay );\n\t\t\t}\n\t\t\telse {\n\t\t\t\treturn ! paginate || calc>records || len===-1 ?\n\t\t\t\t\trecords :\n\t\t\t\t\tcalc;\n\t\t\t}\n\t\t},\n\t\n\t\t/**\n\t\t * The DataTables object for this table\n\t\t *  @type object\n\t\t *  @default null\n\t\t */\n\t\t\"oInstance\": null,\n\t\n\t\t/**\n\t\t * Unique identifier for each instance of the DataTables object. If there\n\t\t * is an ID on the table node, then it takes that value, otherwise an\n\t\t * incrementing internal counter is used.\n\t\t *  @type string\n\t\t *  @default null\n\t\t */\n\t\t\"sInstance\": null,\n\t\n\t\t/**\n\t\t * tabindex attribute value that is added to DataTables control elements, allowing\n\t\t * keyboard navigation of the table and its controls.\n\t\t */\n\t\t\"iTabIndex\": 0,\n\t\n\t\t/**\n\t\t * DIV container for the footer scrolling table if scrolling\n\t\t */\n\t\t\"nScrollHead\": null,\n\t\n\t\t/**\n\t\t * DIV container for the footer scrolling table if scrolling\n\t\t */\n\t\t\"nScrollFoot\": null,\n\t\n\t\t/**\n\t\t * Last applied sort\n\t\t *  @type array\n\t\t *  @default []\n\t\t */\n\t\t\"aLastSort\": [],\n\t\n\t\t/**\n\t\t * Stored plug-in instances\n\t\t *  @type object\n\t\t *  @default {}\n\t\t */\n\t\t\"oPlugins\": {},\n\t\n\t\t/**\n\t\t * Function used to get a row's id from the row's data\n\t\t *  @type function\n\t\t *  @default null\n\t\t */\n\t\t\"rowIdFn\": null,\n\t\n\t\t/**\n\t\t * Data location where to store a row's id\n\t\t *  @type string\n\t\t *  @default null\n\t\t */\n\t\t\"rowId\": null\n\t};\n\n\t/**\n\t * Extension object for DataTables that is used to provide all extension\n\t * options.\n\t *\n\t * Note that the `DataTable.ext` object is available through\n\t * `jQuery.fn.dataTable.ext` where it may be accessed and manipulated. It is\n\t * also aliased to `jQuery.fn.dataTableExt` for historic reasons.\n\t *  @namespace\n\t *  @extends DataTable.models.ext\n\t */\n\t\n\t\n\t/**\n\t * DataTables extensions\n\t * \n\t * This namespace acts as a collection area for plug-ins that can be used to\n\t * extend DataTables capabilities. Indeed many of the build in methods\n\t * use this method to provide their own capabilities (sorting methods for\n\t * example).\n\t *\n\t * Note that this namespace is aliased to `jQuery.fn.dataTableExt` for legacy\n\t * reasons\n\t *\n\t *  @namespace\n\t */\n\tDataTable.ext = _ext = {\n\t\t/**\n\t\t * Buttons. For use with the Buttons extension for DataTables. This is\n\t\t * defined here so other extensions can define buttons regardless of load\n\t\t * order. It is _not_ used by DataTables core.\n\t\t *\n\t\t *  @type object\n\t\t *  @default {}\n\t\t */\n\t\tbuttons: {},\n\t\n\t\n\t\t/**\n\t\t * Element class names\n\t\t *\n\t\t *  @type object\n\t\t *  @default {}\n\t\t */\n\t\tclasses: {},\n\t\n\t\n\t\t/**\n\t\t * DataTables build type (expanded by the download builder)\n\t\t *\n\t\t *  @type string\n\t\t */\n\t\tbuild:\"bs/dt-1.10.21\",\n\t\n\t\n\t\t/**\n\t\t * Error reporting.\n\t\t * \n\t\t * How should DataTables report an error. Can take the value 'alert',\n\t\t * 'throw', 'none' or a function.\n\t\t *\n\t\t *  @type string|function\n\t\t *  @default alert\n\t\t */\n\t\terrMode: \"alert\",\n\t\n\t\n\t\t/**\n\t\t * Feature plug-ins.\n\t\t * \n\t\t * This is an array of objects which describe the feature plug-ins that are\n\t\t * available to DataTables. These feature plug-ins are then available for\n\t\t * use through the `dom` initialisation option.\n\t\t * \n\t\t * Each feature plug-in is described by an object which must have the\n\t\t * following properties:\n\t\t * \n\t\t * * `fnInit` - function that is used to initialise the plug-in,\n\t\t * * `cFeature` - a character so the feature can be enabled by the `dom`\n\t\t *   instillation option. This is case sensitive.\n\t\t *\n\t\t * The `fnInit` function has the following input parameters:\n\t\t *\n\t\t * 1. `{object}` DataTables settings object: see\n\t\t *    {@link DataTable.models.oSettings}\n\t\t *\n\t\t * And the following return is expected:\n\t\t * \n\t\t * * {node|null} The element which contains your feature. Note that the\n\t\t *   return may also be void if your plug-in does not require to inject any\n\t\t *   DOM elements into DataTables control (`dom`) - for example this might\n\t\t *   be useful when developing a plug-in which allows table control via\n\t\t *   keyboard entry\n\t\t *\n\t\t *  @type array\n\t\t *\n\t\t *  @example\n\t\t *    $.fn.dataTable.ext.features.push( {\n\t\t *      \"fnInit\": function( oSettings ) {\n\t\t *        return new TableTools( { \"oDTSettings\": oSettings } );\n\t\t *      },\n\t\t *      \"cFeature\": \"T\"\n\t\t *    } );\n\t\t */\n\t\tfeature: [],\n\t\n\t\n\t\t/**\n\t\t * Row searching.\n\t\t * \n\t\t * This method of searching is complimentary to the default type based\n\t\t * searching, and a lot more comprehensive as it allows you complete control\n\t\t * over the searching logic. Each element in this array is a function\n\t\t * (parameters described below) that is called for every row in the table,\n\t\t * and your logic decides if it should be included in the searching data set\n\t\t * or not.\n\t\t *\n\t\t * Searching functions have the following input parameters:\n\t\t *\n\t\t * 1. `{object}` DataTables settings object: see\n\t\t *    {@link DataTable.models.oSettings}\n\t\t * 2. `{array|object}` Data for the row to be processed (same as the\n\t\t *    original format that was passed in as the data source, or an array\n\t\t *    from a DOM data source\n\t\t * 3. `{int}` Row index ({@link DataTable.models.oSettings.aoData}), which\n\t\t *    can be useful to retrieve the `TR` element if you need DOM interaction.\n\t\t *\n\t\t * And the following return is expected:\n\t\t *\n\t\t * * {boolean} Include the row in the searched result set (true) or not\n\t\t *   (false)\n\t\t *\n\t\t * Note that as with the main search ability in DataTables, technically this\n\t\t * is \"filtering\", since it is subtractive. However, for consistency in\n\t\t * naming we call it searching here.\n\t\t *\n\t\t *  @type array\n\t\t *  @default []\n\t\t *\n\t\t *  @example\n\t\t *    // The following example shows custom search being applied to the\n\t\t *    // fourth column (i.e. the data[3] index) based on two input values\n\t\t *    // from the end-user, matching the data in a certain range.\n\t\t *    $.fn.dataTable.ext.search.push(\n\t\t *      function( settings, data, dataIndex ) {\n\t\t *        var min = document.getElementById('min').value * 1;\n\t\t *        var max = document.getElementById('max').value * 1;\n\t\t *        var version = data[3] == \"-\" ? 0 : data[3]*1;\n\t\t *\n\t\t *        if ( min == \"\" && max == \"\" ) {\n\t\t *          return true;\n\t\t *        }\n\t\t *        else if ( min == \"\" && version < max ) {\n\t\t *          return true;\n\t\t *        }\n\t\t *        else if ( min < version && \"\" == max ) {\n\t\t *          return true;\n\t\t *        }\n\t\t *        else if ( min < version && version < max ) {\n\t\t *          return true;\n\t\t *        }\n\t\t *        return false;\n\t\t *      }\n\t\t *    );\n\t\t */\n\t\tsearch: [],\n\t\n\t\n\t\t/**\n\t\t * Selector extensions\n\t\t *\n\t\t * The `selector` option can be used to extend the options available for the\n\t\t * selector modifier options (`selector-modifier` object data type) that\n\t\t * each of the three built in selector types offer (row, column and cell +\n\t\t * their plural counterparts). For example the Select extension uses this\n\t\t * mechanism to provide an option to select only rows, columns and cells\n\t\t * that have been marked as selected by the end user (`{selected: true}`),\n\t\t * which can be used in conjunction with the existing built in selector\n\t\t * options.\n\t\t *\n\t\t * Each property is an array to which functions can be pushed. The functions\n\t\t * take three attributes:\n\t\t *\n\t\t * * Settings object for the host table\n\t\t * * Options object (`selector-modifier` object type)\n\t\t * * Array of selected item indexes\n\t\t *\n\t\t * The return is an array of the resulting item indexes after the custom\n\t\t * selector has been applied.\n\t\t *\n\t\t *  @type object\n\t\t */\n\t\tselector: {\n\t\t\tcell: [],\n\t\t\tcolumn: [],\n\t\t\trow: []\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * Internal functions, exposed for used in plug-ins.\n\t\t * \n\t\t * Please note that you should not need to use the internal methods for\n\t\t * anything other than a plug-in (and even then, try to avoid if possible).\n\t\t * The internal function may change between releases.\n\t\t *\n\t\t *  @type object\n\t\t *  @default {}\n\t\t */\n\t\tinternal: {},\n\t\n\t\n\t\t/**\n\t\t * Legacy configuration options. Enable and disable legacy options that\n\t\t * are available in DataTables.\n\t\t *\n\t\t *  @type object\n\t\t */\n\t\tlegacy: {\n\t\t\t/**\n\t\t\t * Enable / disable DataTables 1.9 compatible server-side processing\n\t\t\t * requests\n\t\t\t *\n\t\t\t *  @type boolean\n\t\t\t *  @default null\n\t\t\t */\n\t\t\tajax: null\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * Pagination plug-in methods.\n\t\t * \n\t\t * Each entry in this object is a function and defines which buttons should\n\t\t * be shown by the pagination rendering method that is used for the table:\n\t\t * {@link DataTable.ext.renderer.pageButton}. The renderer addresses how the\n\t\t * buttons are displayed in the document, while the functions here tell it\n\t\t * what buttons to display. This is done by returning an array of button\n\t\t * descriptions (what each button will do).\n\t\t *\n\t\t * Pagination types (the four built in options and any additional plug-in\n\t\t * options defined here) can be used through the `paginationType`\n\t\t * initialisation parameter.\n\t\t *\n\t\t * The functions defined take two parameters:\n\t\t *\n\t\t * 1. `{int} page` The current page index\n\t\t * 2. `{int} pages` The number of pages in the table\n\t\t *\n\t\t * Each function is expected to return an array where each element of the\n\t\t * array can be one of:\n\t\t *\n\t\t * * `first` - Jump to first page when activated\n\t\t * * `last` - Jump to last page when activated\n\t\t * * `previous` - Show previous page when activated\n\t\t * * `next` - Show next page when activated\n\t\t * * `{int}` - Show page of the index given\n\t\t * * `{array}` - A nested array containing the above elements to add a\n\t\t *   containing 'DIV' element (might be useful for styling).\n\t\t *\n\t\t * Note that DataTables v1.9- used this object slightly differently whereby\n\t\t * an object with two functions would be defined for each plug-in. That\n\t\t * ability is still supported by DataTables 1.10+ to provide backwards\n\t\t * compatibility, but this option of use is now decremented and no longer\n\t\t * documented in DataTables 1.10+.\n\t\t *\n\t\t *  @type object\n\t\t *  @default {}\n\t\t *\n\t\t *  @example\n\t\t *    // Show previous, next and current page buttons only\n\t\t *    $.fn.dataTableExt.oPagination.current = function ( page, pages ) {\n\t\t *      return [ 'previous', page, 'next' ];\n\t\t *    };\n\t\t */\n\t\tpager: {},\n\t\n\t\n\t\trenderer: {\n\t\t\tpageButton: {},\n\t\t\theader: {}\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * Ordering plug-ins - custom data source\n\t\t * \n\t\t * The extension options for ordering of data available here is complimentary\n\t\t * to the default type based ordering that DataTables typically uses. It\n\t\t * allows much greater control over the the data that is being used to\n\t\t * order a column, but is necessarily therefore more complex.\n\t\t * \n\t\t * This type of ordering is useful if you want to do ordering based on data\n\t\t * live from the DOM (for example the contents of an 'input' element) rather\n\t\t * than just the static string that DataTables knows of.\n\t\t * \n\t\t * The way these plug-ins work is that you create an array of the values you\n\t\t * wish to be ordering for the column in question and then return that\n\t\t * array. The data in the array much be in the index order of the rows in\n\t\t * the table (not the currently ordering order!). Which order data gathering\n\t\t * function is run here depends on the `dt-init columns.orderDataType`\n\t\t * parameter that is used for the column (if any).\n\t\t *\n\t\t * The functions defined take two parameters:\n\t\t *\n\t\t * 1. `{object}` DataTables settings object: see\n\t\t *    {@link DataTable.models.oSettings}\n\t\t * 2. `{int}` Target column index\n\t\t *\n\t\t * Each function is expected to return an array:\n\t\t *\n\t\t * * `{array}` Data for the column to be ordering upon\n\t\t *\n\t\t *  @type array\n\t\t *\n\t\t *  @example\n\t\t *    // Ordering using `input` node values\n\t\t *    $.fn.dataTable.ext.order['dom-text'] = function  ( settings, col )\n\t\t *    {\n\t\t *      return this.api().column( col, {order:'index'} ).nodes().map( function ( td, i ) {\n\t\t *        return $('input', td).val();\n\t\t *      } );\n\t\t *    }\n\t\t */\n\t\torder: {},\n\t\n\t\n\t\t/**\n\t\t * Type based plug-ins.\n\t\t *\n\t\t * Each column in DataTables has a type assigned to it, either by automatic\n\t\t * detection or by direct assignment using the `type` option for the column.\n\t\t * The type of a column will effect how it is ordering and search (plug-ins\n\t\t * can also make use of the column type if required).\n\t\t *\n\t\t * @namespace\n\t\t */\n\t\ttype: {\n\t\t\t/**\n\t\t\t * Type detection functions.\n\t\t\t *\n\t\t\t * The functions defined in this object are used to automatically detect\n\t\t\t * a column's type, making initialisation of DataTables super easy, even\n\t\t\t * when complex data is in the table.\n\t\t\t *\n\t\t\t * The functions defined take two parameters:\n\t\t\t *\n\t\t     *  1. `{*}` Data from the column cell to be analysed\n\t\t     *  2. `{settings}` DataTables settings object. This can be used to\n\t\t     *     perform context specific type detection - for example detection\n\t\t     *     based on language settings such as using a comma for a decimal\n\t\t     *     place. Generally speaking the options from the settings will not\n\t\t     *     be required\n\t\t\t *\n\t\t\t * Each function is expected to return:\n\t\t\t *\n\t\t\t * * `{string|null}` Data type detected, or null if unknown (and thus\n\t\t\t *   pass it on to the other type detection functions.\n\t\t\t *\n\t\t\t *  @type array\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    // Currency type detection plug-in:\n\t\t\t *    $.fn.dataTable.ext.type.detect.push(\n\t\t\t *      function ( data, settings ) {\n\t\t\t *        // Check the numeric part\n\t\t\t *        if ( ! data.substring(1).match(/[0-9]/) ) {\n\t\t\t *          return null;\n\t\t\t *        }\n\t\t\t *\n\t\t\t *        // Check prefixed by currency\n\t\t\t *        if ( data.charAt(0) == '$' || data.charAt(0) == '&pound;' ) {\n\t\t\t *          return 'currency';\n\t\t\t *        }\n\t\t\t *        return null;\n\t\t\t *      }\n\t\t\t *    );\n\t\t\t */\n\t\t\tdetect: [],\n\t\n\t\n\t\t\t/**\n\t\t\t * Type based search formatting.\n\t\t\t *\n\t\t\t * The type based searching functions can be used to pre-format the\n\t\t\t * data to be search on. For example, it can be used to strip HTML\n\t\t\t * tags or to de-format telephone numbers for numeric only searching.\n\t\t\t *\n\t\t\t * Note that is a search is not defined for a column of a given type,\n\t\t\t * no search formatting will be performed.\n\t\t\t * \n\t\t\t * Pre-processing of searching data plug-ins - When you assign the sType\n\t\t\t * for a column (or have it automatically detected for you by DataTables\n\t\t\t * or a type detection plug-in), you will typically be using this for\n\t\t\t * custom sorting, but it can also be used to provide custom searching\n\t\t\t * by allowing you to pre-processing the data and returning the data in\n\t\t\t * the format that should be searched upon. This is done by adding\n\t\t\t * functions this object with a parameter name which matches the sType\n\t\t\t * for that target column. This is the corollary of <i>afnSortData</i>\n\t\t\t * for searching data.\n\t\t\t *\n\t\t\t * The functions defined take a single parameter:\n\t\t\t *\n\t\t     *  1. `{*}` Data from the column cell to be prepared for searching\n\t\t\t *\n\t\t\t * Each function is expected to return:\n\t\t\t *\n\t\t\t * * `{string|null}` Formatted string that will be used for the searching.\n\t\t\t *\n\t\t\t *  @type object\n\t\t\t *  @default {}\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    $.fn.dataTable.ext.type.search['title-numeric'] = function ( d ) {\n\t\t\t *      return d.replace(/\\n/g,\" \").replace( /<.*?>/g, \"\" );\n\t\t\t *    }\n\t\t\t */\n\t\t\tsearch: {},\n\t\n\t\n\t\t\t/**\n\t\t\t * Type based ordering.\n\t\t\t *\n\t\t\t * The column type tells DataTables what ordering to apply to the table\n\t\t\t * when a column is sorted upon. The order for each type that is defined,\n\t\t\t * is defined by the functions available in this object.\n\t\t\t *\n\t\t\t * Each ordering option can be described by three properties added to\n\t\t\t * this object:\n\t\t\t *\n\t\t\t * * `{type}-pre` - Pre-formatting function\n\t\t\t * * `{type}-asc` - Ascending order function\n\t\t\t * * `{type}-desc` - Descending order function\n\t\t\t *\n\t\t\t * All three can be used together, only `{type}-pre` or only\n\t\t\t * `{type}-asc` and `{type}-desc` together. It is generally recommended\n\t\t\t * that only `{type}-pre` is used, as this provides the optimal\n\t\t\t * implementation in terms of speed, although the others are provided\n\t\t\t * for compatibility with existing Javascript sort functions.\n\t\t\t *\n\t\t\t * `{type}-pre`: Functions defined take a single parameter:\n\t\t\t *\n\t\t     *  1. `{*}` Data from the column cell to be prepared for ordering\n\t\t\t *\n\t\t\t * And return:\n\t\t\t *\n\t\t\t * * `{*}` Data to be sorted upon\n\t\t\t *\n\t\t\t * `{type}-asc` and `{type}-desc`: Functions are typical Javascript sort\n\t\t\t * functions, taking two parameters:\n\t\t\t *\n\t\t     *  1. `{*}` Data to compare to the second parameter\n\t\t     *  2. `{*}` Data to compare to the first parameter\n\t\t\t *\n\t\t\t * And returning:\n\t\t\t *\n\t\t\t * * `{*}` Ordering match: <0 if first parameter should be sorted lower\n\t\t\t *   than the second parameter, ===0 if the two parameters are equal and\n\t\t\t *   >0 if the first parameter should be sorted height than the second\n\t\t\t *   parameter.\n\t\t\t * \n\t\t\t *  @type object\n\t\t\t *  @default {}\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    // Numeric ordering of formatted numbers with a pre-formatter\n\t\t\t *    $.extend( $.fn.dataTable.ext.type.order, {\n\t\t\t *      \"string-pre\": function(x) {\n\t\t\t *        a = (a === \"-\" || a === \"\") ? 0 : a.replace( /[^\\d\\-\\.]/g, \"\" );\n\t\t\t *        return parseFloat( a );\n\t\t\t *      }\n\t\t\t *    } );\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    // Case-sensitive string ordering, with no pre-formatting method\n\t\t\t *    $.extend( $.fn.dataTable.ext.order, {\n\t\t\t *      \"string-case-asc\": function(x,y) {\n\t\t\t *        return ((x < y) ? -1 : ((x > y) ? 1 : 0));\n\t\t\t *      },\n\t\t\t *      \"string-case-desc\": function(x,y) {\n\t\t\t *        return ((x < y) ? 1 : ((x > y) ? -1 : 0));\n\t\t\t *      }\n\t\t\t *    } );\n\t\t\t */\n\t\t\torder: {}\n\t\t},\n\t\n\t\t/**\n\t\t * Unique DataTables instance counter\n\t\t *\n\t\t * @type int\n\t\t * @private\n\t\t */\n\t\t_unique: 0,\n\t\n\t\n\t\t//\n\t\t// Depreciated\n\t\t// The following properties are retained for backwards compatiblity only.\n\t\t// The should not be used in new projects and will be removed in a future\n\t\t// version\n\t\t//\n\t\n\t\t/**\n\t\t * Version check function.\n\t\t *  @type function\n\t\t *  @depreciated Since 1.10\n\t\t */\n\t\tfnVersionCheck: DataTable.fnVersionCheck,\n\t\n\t\n\t\t/**\n\t\t * Index for what 'this' index API functions should use\n\t\t *  @type int\n\t\t *  @deprecated Since v1.10\n\t\t */\n\t\tiApiIndex: 0,\n\t\n\t\n\t\t/**\n\t\t * jQuery UI class container\n\t\t *  @type object\n\t\t *  @deprecated Since v1.10\n\t\t */\n\t\toJUIClasses: {},\n\t\n\t\n\t\t/**\n\t\t * Software version\n\t\t *  @type string\n\t\t *  @deprecated Since v1.10\n\t\t */\n\t\tsVersion: DataTable.version\n\t};\n\t\n\t\n\t//\n\t// Backwards compatibility. Alias to pre 1.10 Hungarian notation counter parts\n\t//\n\t$.extend( _ext, {\n\t\tafnFiltering: _ext.search,\n\t\taTypes:       _ext.type.detect,\n\t\tofnSearch:    _ext.type.search,\n\t\toSort:        _ext.type.order,\n\t\tafnSortData:  _ext.order,\n\t\taoFeatures:   _ext.feature,\n\t\toApi:         _ext.internal,\n\t\toStdClasses:  _ext.classes,\n\t\toPagination:  _ext.pager\n\t} );\n\t\n\t\n\t$.extend( DataTable.ext.classes, {\n\t\t\"sTable\": \"dataTable\",\n\t\t\"sNoFooter\": \"no-footer\",\n\t\n\t\t/* Paging buttons */\n\t\t\"sPageButton\": \"paginate_button\",\n\t\t\"sPageButtonActive\": \"current\",\n\t\t\"sPageButtonDisabled\": \"disabled\",\n\t\n\t\t/* Striping classes */\n\t\t\"sStripeOdd\": \"odd\",\n\t\t\"sStripeEven\": \"even\",\n\t\n\t\t/* Empty row */\n\t\t\"sRowEmpty\": \"dataTables_empty\",\n\t\n\t\t/* Features */\n\t\t\"sWrapper\": \"dataTables_wrapper\",\n\t\t\"sFilter\": \"dataTables_filter\",\n\t\t\"sInfo\": \"dataTables_info\",\n\t\t\"sPaging\": \"dataTables_paginate paging_\", /* Note that the type is postfixed */\n\t\t\"sLength\": \"dataTables_length\",\n\t\t\"sProcessing\": \"dataTables_processing\",\n\t\n\t\t/* Sorting */\n\t\t\"sSortAsc\": \"sorting_asc\",\n\t\t\"sSortDesc\": \"sorting_desc\",\n\t\t\"sSortable\": \"sorting\", /* Sortable in both directions */\n\t\t\"sSortableAsc\": \"sorting_asc_disabled\",\n\t\t\"sSortableDesc\": \"sorting_desc_disabled\",\n\t\t\"sSortableNone\": \"sorting_disabled\",\n\t\t\"sSortColumn\": \"sorting_\", /* Note that an int is postfixed for the sorting order */\n\t\n\t\t/* Filtering */\n\t\t\"sFilterInput\": \"\",\n\t\n\t\t/* Page length */\n\t\t\"sLengthSelect\": \"\",\n\t\n\t\t/* Scrolling */\n\t\t\"sScrollWrapper\": \"dataTables_scroll\",\n\t\t\"sScrollHead\": \"dataTables_scrollHead\",\n\t\t\"sScrollHeadInner\": \"dataTables_scrollHeadInner\",\n\t\t\"sScrollBody\": \"dataTables_scrollBody\",\n\t\t\"sScrollFoot\": \"dataTables_scrollFoot\",\n\t\t\"sScrollFootInner\": \"dataTables_scrollFootInner\",\n\t\n\t\t/* Misc */\n\t\t\"sHeaderTH\": \"\",\n\t\t\"sFooterTH\": \"\",\n\t\n\t\t// Deprecated\n\t\t\"sSortJUIAsc\": \"\",\n\t\t\"sSortJUIDesc\": \"\",\n\t\t\"sSortJUI\": \"\",\n\t\t\"sSortJUIAscAllowed\": \"\",\n\t\t\"sSortJUIDescAllowed\": \"\",\n\t\t\"sSortJUIWrapper\": \"\",\n\t\t\"sSortIcon\": \"\",\n\t\t\"sJUIHeader\": \"\",\n\t\t\"sJUIFooter\": \"\"\n\t} );\n\t\n\t\n\tvar extPagination = DataTable.ext.pager;\n\t\n\tfunction _numbers ( page, pages ) {\n\t\tvar\n\t\t\tnumbers = [],\n\t\t\tbuttons = extPagination.numbers_length,\n\t\t\thalf = Math.floor( buttons / 2 ),\n\t\t\ti = 1;\n\t\n\t\tif ( pages <= buttons ) {\n\t\t\tnumbers = _range( 0, pages );\n\t\t}\n\t\telse if ( page <= half ) {\n\t\t\tnumbers = _range( 0, buttons-2 );\n\t\t\tnumbers.push( 'ellipsis' );\n\t\t\tnumbers.push( pages-1 );\n\t\t}\n\t\telse if ( page >= pages - 1 - half ) {\n\t\t\tnumbers = _range( pages-(buttons-2), pages );\n\t\t\tnumbers.splice( 0, 0, 'ellipsis' ); // no unshift in ie6\n\t\t\tnumbers.splice( 0, 0, 0 );\n\t\t}\n\t\telse {\n\t\t\tnumbers = _range( page-half+2, page+half-1 );\n\t\t\tnumbers.push( 'ellipsis' );\n\t\t\tnumbers.push( pages-1 );\n\t\t\tnumbers.splice( 0, 0, 'ellipsis' );\n\t\t\tnumbers.splice( 0, 0, 0 );\n\t\t}\n\t\n\t\tnumbers.DT_el = 'span';\n\t\treturn numbers;\n\t}\n\t\n\t\n\t$.extend( extPagination, {\n\t\tsimple: function ( page, pages ) {\n\t\t\treturn [ 'previous', 'next' ];\n\t\t},\n\t\n\t\tfull: function ( page, pages ) {\n\t\t\treturn [  'first', 'previous', 'next', 'last' ];\n\t\t},\n\t\n\t\tnumbers: function ( page, pages ) {\n\t\t\treturn [ _numbers(page, pages) ];\n\t\t},\n\t\n\t\tsimple_numbers: function ( page, pages ) {\n\t\t\treturn [ 'previous', _numbers(page, pages), 'next' ];\n\t\t},\n\t\n\t\tfull_numbers: function ( page, pages ) {\n\t\t\treturn [ 'first', 'previous', _numbers(page, pages), 'next', 'last' ];\n\t\t},\n\t\t\n\t\tfirst_last_numbers: function (page, pages) {\n\t \t\treturn ['first', _numbers(page, pages), 'last'];\n\t \t},\n\t\n\t\t// For testing and plug-ins to use\n\t\t_numbers: _numbers,\n\t\n\t\t// Number of number buttons (including ellipsis) to show. _Must be odd!_\n\t\tnumbers_length: 7\n\t} );\n\t\n\t\n\t$.extend( true, DataTable.ext.renderer, {\n\t\tpageButton: {\n\t\t\t_: function ( settings, host, idx, buttons, page, pages ) {\n\t\t\t\tvar classes = settings.oClasses;\n\t\t\t\tvar lang = settings.oLanguage.oPaginate;\n\t\t\t\tvar aria = settings.oLanguage.oAria.paginate || {};\n\t\t\t\tvar btnDisplay, btnClass, counter=0;\n\t\n\t\t\t\tvar attach = function( container, buttons ) {\n\t\t\t\t\tvar i, ien, node, button, tabIndex;\n\t\t\t\t\tvar disabledClass = classes.sPageButtonDisabled;\n\t\t\t\t\tvar clickHandler = function ( e ) {\n\t\t\t\t\t\t_fnPageChange( settings, e.data.action, true );\n\t\t\t\t\t};\n\t\n\t\t\t\t\tfor ( i=0, ien=buttons.length ; i<ien ; i++ ) {\n\t\t\t\t\t\tbutton = buttons[i];\n\t\n\t\t\t\t\t\tif ( $.isArray( button ) ) {\n\t\t\t\t\t\t\tvar inner = $( '<'+(button.DT_el || 'div')+'/>' )\n\t\t\t\t\t\t\t\t.appendTo( container );\n\t\t\t\t\t\t\tattach( inner, button );\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tbtnDisplay = null;\n\t\t\t\t\t\t\tbtnClass = button;\n\t\t\t\t\t\t\ttabIndex = settings.iTabIndex;\n\t\n\t\t\t\t\t\t\tswitch ( button ) {\n\t\t\t\t\t\t\t\tcase 'ellipsis':\n\t\t\t\t\t\t\t\t\tcontainer.append('<span class=\"ellipsis\">&#x2026;</span>');\n\t\t\t\t\t\t\t\t\tbreak;\n\t\n\t\t\t\t\t\t\t\tcase 'first':\n\t\t\t\t\t\t\t\t\tbtnDisplay = lang.sFirst;\n\t\n\t\t\t\t\t\t\t\t\tif ( page === 0 ) {\n\t\t\t\t\t\t\t\t\t\ttabIndex = -1;\n\t\t\t\t\t\t\t\t\t\tbtnClass += ' ' + disabledClass;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tbreak;\n\t\n\t\t\t\t\t\t\t\tcase 'previous':\n\t\t\t\t\t\t\t\t\tbtnDisplay = lang.sPrevious;\n\t\n\t\t\t\t\t\t\t\t\tif ( page === 0 ) {\n\t\t\t\t\t\t\t\t\t\ttabIndex = -1;\n\t\t\t\t\t\t\t\t\t\tbtnClass += ' ' + disabledClass;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tbreak;\n\t\n\t\t\t\t\t\t\t\tcase 'next':\n\t\t\t\t\t\t\t\t\tbtnDisplay = lang.sNext;\n\t\n\t\t\t\t\t\t\t\t\tif ( pages === 0 || page === pages-1 ) {\n\t\t\t\t\t\t\t\t\t\ttabIndex = -1;\n\t\t\t\t\t\t\t\t\t\tbtnClass += ' ' + disabledClass;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tbreak;\n\t\n\t\t\t\t\t\t\t\tcase 'last':\n\t\t\t\t\t\t\t\t\tbtnDisplay = lang.sLast;\n\t\n\t\t\t\t\t\t\t\t\tif ( page === pages-1 ) {\n\t\t\t\t\t\t\t\t\t\ttabIndex = -1;\n\t\t\t\t\t\t\t\t\t\tbtnClass += ' ' + disabledClass;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tbreak;\n\t\n\t\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t\tbtnDisplay = button + 1;\n\t\t\t\t\t\t\t\t\tbtnClass = page === button ?\n\t\t\t\t\t\t\t\t\t\tclasses.sPageButtonActive : '';\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t\tif ( btnDisplay !== null ) {\n\t\t\t\t\t\t\t\tnode = $('<a>', {\n\t\t\t\t\t\t\t\t\t\t'class': classes.sPageButton+' '+btnClass,\n\t\t\t\t\t\t\t\t\t\t'aria-controls': settings.sTableId,\n\t\t\t\t\t\t\t\t\t\t'aria-label': aria[ button ],\n\t\t\t\t\t\t\t\t\t\t'data-dt-idx': counter,\n\t\t\t\t\t\t\t\t\t\t'tabindex': tabIndex,\n\t\t\t\t\t\t\t\t\t\t'id': idx === 0 && typeof button === 'string' ?\n\t\t\t\t\t\t\t\t\t\t\tsettings.sTableId +'_'+ button :\n\t\t\t\t\t\t\t\t\t\t\tnull\n\t\t\t\t\t\t\t\t\t} )\n\t\t\t\t\t\t\t\t\t.html( btnDisplay )\n\t\t\t\t\t\t\t\t\t.appendTo( container );\n\t\n\t\t\t\t\t\t\t\t_fnBindAction(\n\t\t\t\t\t\t\t\t\tnode, {action: button}, clickHandler\n\t\t\t\t\t\t\t\t);\n\t\n\t\t\t\t\t\t\t\tcounter++;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t};\n\t\n\t\t\t\t// IE9 throws an 'unknown error' if document.activeElement is used\n\t\t\t\t// inside an iframe or frame. Try / catch the error. Not good for\n\t\t\t\t// accessibility, but neither are frames.\n\t\t\t\tvar activeEl;\n\t\n\t\t\t\ttry {\n\t\t\t\t\t// Because this approach is destroying and recreating the paging\n\t\t\t\t\t// elements, focus is lost on the select button which is bad for\n\t\t\t\t\t// accessibility. So we want to restore focus once the draw has\n\t\t\t\t\t// completed\n\t\t\t\t\tactiveEl = $(host).find(document.activeElement).data('dt-idx');\n\t\t\t\t}\n\t\t\t\tcatch (e) {}\n\t\n\t\t\t\tattach( $(host).empty(), buttons );\n\t\n\t\t\t\tif ( activeEl !== undefined ) {\n\t\t\t\t\t$(host).find( '[data-dt-idx='+activeEl+']' ).trigger('focus');\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} );\n\t\n\t\n\t\n\t// Built in type detection. See model.ext.aTypes for information about\n\t// what is required from this methods.\n\t$.extend( DataTable.ext.type.detect, [\n\t\t// Plain numbers - first since V8 detects some plain numbers as dates\n\t\t// e.g. Date.parse('55') (but not all, e.g. Date.parse('22')...).\n\t\tfunction ( d, settings )\n\t\t{\n\t\t\tvar decimal = settings.oLanguage.sDecimal;\n\t\t\treturn _isNumber( d, decimal ) ? 'num'+decimal : null;\n\t\t},\n\t\n\t\t// Dates (only those recognised by the browser's Date.parse)\n\t\tfunction ( d, settings )\n\t\t{\n\t\t\t// V8 tries _very_ hard to make a string passed into `Date.parse()`\n\t\t\t// valid, so we need to use a regex to restrict date formats. Use a\n\t\t\t// plug-in for anything other than ISO8601 style strings\n\t\t\tif ( d && !(d instanceof Date) && ! _re_date.test(d) ) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tvar parsed = Date.parse(d);\n\t\t\treturn (parsed !== null && !isNaN(parsed)) || _empty(d) ? 'date' : null;\n\t\t},\n\t\n\t\t// Formatted numbers\n\t\tfunction ( d, settings )\n\t\t{\n\t\t\tvar decimal = settings.oLanguage.sDecimal;\n\t\t\treturn _isNumber( d, decimal, true ) ? 'num-fmt'+decimal : null;\n\t\t},\n\t\n\t\t// HTML numeric\n\t\tfunction ( d, settings )\n\t\t{\n\t\t\tvar decimal = settings.oLanguage.sDecimal;\n\t\t\treturn _htmlNumeric( d, decimal ) ? 'html-num'+decimal : null;\n\t\t},\n\t\n\t\t// HTML numeric, formatted\n\t\tfunction ( d, settings )\n\t\t{\n\t\t\tvar decimal = settings.oLanguage.sDecimal;\n\t\t\treturn _htmlNumeric( d, decimal, true ) ? 'html-num-fmt'+decimal : null;\n\t\t},\n\t\n\t\t// HTML (this is strict checking - there must be html)\n\t\tfunction ( d, settings )\n\t\t{\n\t\t\treturn _empty( d ) || (typeof d === 'string' && d.indexOf('<') !== -1) ?\n\t\t\t\t'html' : null;\n\t\t}\n\t] );\n\t\n\t\n\t\n\t// Filter formatting functions. See model.ext.ofnSearch for information about\n\t// what is required from these methods.\n\t// \n\t// Note that additional search methods are added for the html numbers and\n\t// html formatted numbers by `_addNumericSort()` when we know what the decimal\n\t// place is\n\t\n\t\n\t$.extend( DataTable.ext.type.search, {\n\t\thtml: function ( data ) {\n\t\t\treturn _empty(data) ?\n\t\t\t\tdata :\n\t\t\t\ttypeof data === 'string' ?\n\t\t\t\t\tdata\n\t\t\t\t\t\t.replace( _re_new_lines, \" \" )\n\t\t\t\t\t\t.replace( _re_html, \"\" ) :\n\t\t\t\t\t'';\n\t\t},\n\t\n\t\tstring: function ( data ) {\n\t\t\treturn _empty(data) ?\n\t\t\t\tdata :\n\t\t\t\ttypeof data === 'string' ?\n\t\t\t\t\tdata.replace( _re_new_lines, \" \" ) :\n\t\t\t\t\tdata;\n\t\t}\n\t} );\n\t\n\t\n\t\n\tvar __numericReplace = function ( d, decimalPlace, re1, re2 ) {\n\t\tif ( d !== 0 && (!d || d === '-') ) {\n\t\t\treturn -Infinity;\n\t\t}\n\t\n\t\t// If a decimal place other than `.` is used, it needs to be given to the\n\t\t// function so we can detect it and replace with a `.` which is the only\n\t\t// decimal place Javascript recognises - it is not locale aware.\n\t\tif ( decimalPlace ) {\n\t\t\td = _numToDecimal( d, decimalPlace );\n\t\t}\n\t\n\t\tif ( d.replace ) {\n\t\t\tif ( re1 ) {\n\t\t\t\td = d.replace( re1, '' );\n\t\t\t}\n\t\n\t\t\tif ( re2 ) {\n\t\t\t\td = d.replace( re2, '' );\n\t\t\t}\n\t\t}\n\t\n\t\treturn d * 1;\n\t};\n\t\n\t\n\t// Add the numeric 'deformatting' functions for sorting and search. This is done\n\t// in a function to provide an easy ability for the language options to add\n\t// additional methods if a non-period decimal place is used.\n\tfunction _addNumericSort ( decimalPlace ) {\n\t\t$.each(\n\t\t\t{\n\t\t\t\t// Plain numbers\n\t\t\t\t\"num\": function ( d ) {\n\t\t\t\t\treturn __numericReplace( d, decimalPlace );\n\t\t\t\t},\n\t\n\t\t\t\t// Formatted numbers\n\t\t\t\t\"num-fmt\": function ( d ) {\n\t\t\t\t\treturn __numericReplace( d, decimalPlace, _re_formatted_numeric );\n\t\t\t\t},\n\t\n\t\t\t\t// HTML numeric\n\t\t\t\t\"html-num\": function ( d ) {\n\t\t\t\t\treturn __numericReplace( d, decimalPlace, _re_html );\n\t\t\t\t},\n\t\n\t\t\t\t// HTML numeric, formatted\n\t\t\t\t\"html-num-fmt\": function ( d ) {\n\t\t\t\t\treturn __numericReplace( d, decimalPlace, _re_html, _re_formatted_numeric );\n\t\t\t\t}\n\t\t\t},\n\t\t\tfunction ( key, fn ) {\n\t\t\t\t// Add the ordering method\n\t\t\t\t_ext.type.order[ key+decimalPlace+'-pre' ] = fn;\n\t\n\t\t\t\t// For HTML types add a search formatter that will strip the HTML\n\t\t\t\tif ( key.match(/^html\\-/) ) {\n\t\t\t\t\t_ext.type.search[ key+decimalPlace ] = _ext.type.search.html;\n\t\t\t\t}\n\t\t\t}\n\t\t);\n\t}\n\t\n\t\n\t// Default sort methods\n\t$.extend( _ext.type.order, {\n\t\t// Dates\n\t\t\"date-pre\": function ( d ) {\n\t\t\tvar ts = Date.parse( d );\n\t\t\treturn isNaN(ts) ? -Infinity : ts;\n\t\t},\n\t\n\t\t// html\n\t\t\"html-pre\": function ( a ) {\n\t\t\treturn _empty(a) ?\n\t\t\t\t'' :\n\t\t\t\ta.replace ?\n\t\t\t\t\ta.replace( /<.*?>/g, \"\" ).toLowerCase() :\n\t\t\t\t\ta+'';\n\t\t},\n\t\n\t\t// string\n\t\t\"string-pre\": function ( a ) {\n\t\t\t// This is a little complex, but faster than always calling toString,\n\t\t\t// http://jsperf.com/tostring-v-check\n\t\t\treturn _empty(a) ?\n\t\t\t\t'' :\n\t\t\t\ttypeof a === 'string' ?\n\t\t\t\t\ta.toLowerCase() :\n\t\t\t\t\t! a.toString ?\n\t\t\t\t\t\t'' :\n\t\t\t\t\t\ta.toString();\n\t\t},\n\t\n\t\t// string-asc and -desc are retained only for compatibility with the old\n\t\t// sort methods\n\t\t\"string-asc\": function ( x, y ) {\n\t\t\treturn ((x < y) ? -1 : ((x > y) ? 1 : 0));\n\t\t},\n\t\n\t\t\"string-desc\": function ( x, y ) {\n\t\t\treturn ((x < y) ? 1 : ((x > y) ? -1 : 0));\n\t\t}\n\t} );\n\t\n\t\n\t// Numeric sorting types - order doesn't matter here\n\t_addNumericSort( '' );\n\t\n\t\n\t$.extend( true, DataTable.ext.renderer, {\n\t\theader: {\n\t\t\t_: function ( settings, cell, column, classes ) {\n\t\t\t\t// No additional mark-up required\n\t\t\t\t// Attach a sort listener to update on sort - note that using the\n\t\t\t\t// `DT` namespace will allow the event to be removed automatically\n\t\t\t\t// on destroy, while the `dt` namespaced event is the one we are\n\t\t\t\t// listening for\n\t\t\t\t$(settings.nTable).on( 'order.dt.DT', function ( e, ctx, sorting, columns ) {\n\t\t\t\t\tif ( settings !== ctx ) { // need to check this this is the host\n\t\t\t\t\t\treturn;               // table, not a nested one\n\t\t\t\t\t}\n\t\n\t\t\t\t\tvar colIdx = column.idx;\n\t\n\t\t\t\t\tcell\n\t\t\t\t\t\t.removeClass(\n\t\t\t\t\t\t\tcolumn.sSortingClass +' '+\n\t\t\t\t\t\t\tclasses.sSortAsc +' '+\n\t\t\t\t\t\t\tclasses.sSortDesc\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.addClass( columns[ colIdx ] == 'asc' ?\n\t\t\t\t\t\t\tclasses.sSortAsc : columns[ colIdx ] == 'desc' ?\n\t\t\t\t\t\t\t\tclasses.sSortDesc :\n\t\t\t\t\t\t\t\tcolumn.sSortingClass\n\t\t\t\t\t\t);\n\t\t\t\t} );\n\t\t\t},\n\t\n\t\t\tjqueryui: function ( settings, cell, column, classes ) {\n\t\t\t\t$('<div/>')\n\t\t\t\t\t.addClass( classes.sSortJUIWrapper )\n\t\t\t\t\t.append( cell.contents() )\n\t\t\t\t\t.append( $('<span/>')\n\t\t\t\t\t\t.addClass( classes.sSortIcon+' '+column.sSortingClassJUI )\n\t\t\t\t\t)\n\t\t\t\t\t.appendTo( cell );\n\t\n\t\t\t\t// Attach a sort listener to update on sort\n\t\t\t\t$(settings.nTable).on( 'order.dt.DT', function ( e, ctx, sorting, columns ) {\n\t\t\t\t\tif ( settings !== ctx ) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\n\t\t\t\t\tvar colIdx = column.idx;\n\t\n\t\t\t\t\tcell\n\t\t\t\t\t\t.removeClass( classes.sSortAsc +\" \"+classes.sSortDesc )\n\t\t\t\t\t\t.addClass( columns[ colIdx ] == 'asc' ?\n\t\t\t\t\t\t\tclasses.sSortAsc : columns[ colIdx ] == 'desc' ?\n\t\t\t\t\t\t\t\tclasses.sSortDesc :\n\t\t\t\t\t\t\t\tcolumn.sSortingClass\n\t\t\t\t\t\t);\n\t\n\t\t\t\t\tcell\n\t\t\t\t\t\t.find( 'span.'+classes.sSortIcon )\n\t\t\t\t\t\t.removeClass(\n\t\t\t\t\t\t\tclasses.sSortJUIAsc +\" \"+\n\t\t\t\t\t\t\tclasses.sSortJUIDesc +\" \"+\n\t\t\t\t\t\t\tclasses.sSortJUI +\" \"+\n\t\t\t\t\t\t\tclasses.sSortJUIAscAllowed +\" \"+\n\t\t\t\t\t\t\tclasses.sSortJUIDescAllowed\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.addClass( columns[ colIdx ] == 'asc' ?\n\t\t\t\t\t\t\tclasses.sSortJUIAsc : columns[ colIdx ] == 'desc' ?\n\t\t\t\t\t\t\t\tclasses.sSortJUIDesc :\n\t\t\t\t\t\t\t\tcolumn.sSortingClassJUI\n\t\t\t\t\t\t);\n\t\t\t\t} );\n\t\t\t}\n\t\t}\n\t} );\n\t\n\t/*\n\t * Public helper functions. These aren't used internally by DataTables, or\n\t * called by any of the options passed into DataTables, but they can be used\n\t * externally by developers working with DataTables. They are helper functions\n\t * to make working with DataTables a little bit easier.\n\t */\n\t\n\tvar __htmlEscapeEntities = function ( d ) {\n\t\treturn typeof d === 'string' ?\n\t\t\td\n\t\t\t\t.replace(/&/g, '&amp;')\n\t\t\t\t.replace(/</g, '&lt;')\n\t\t\t\t.replace(/>/g, '&gt;')\n\t\t\t\t.replace(/\"/g, '&quot;') :\n\t\t\td;\n\t};\n\t\n\t/**\n\t * Helpers for `columns.render`.\n\t *\n\t * The options defined here can be used with the `columns.render` initialisation\n\t * option to provide a display renderer. The following functions are defined:\n\t *\n\t * * `number` - Will format numeric data (defined by `columns.data`) for\n\t *   display, retaining the original unformatted data for sorting and filtering.\n\t *   It takes 5 parameters:\n\t *   * `string` - Thousands grouping separator\n\t *   * `string` - Decimal point indicator\n\t *   * `integer` - Number of decimal points to show\n\t *   * `string` (optional) - Prefix.\n\t *   * `string` (optional) - Postfix (/suffix).\n\t * * `text` - Escape HTML to help prevent XSS attacks. It has no optional\n\t *   parameters.\n\t *\n\t * @example\n\t *   // Column definition using the number renderer\n\t *   {\n\t *     data: \"salary\",\n\t *     render: $.fn.dataTable.render.number( '\\'', '.', 0, '$' )\n\t *   }\n\t *\n\t * @namespace\n\t */\n\tDataTable.render = {\n\t\tnumber: function ( thousands, decimal, precision, prefix, postfix ) {\n\t\t\treturn {\n\t\t\t\tdisplay: function ( d ) {\n\t\t\t\t\tif ( typeof d !== 'number' && typeof d !== 'string' ) {\n\t\t\t\t\t\treturn d;\n\t\t\t\t\t}\n\t\n\t\t\t\t\tvar negative = d < 0 ? '-' : '';\n\t\t\t\t\tvar flo = parseFloat( d );\n\t\n\t\t\t\t\t// If NaN then there isn't much formatting that we can do - just\n\t\t\t\t\t// return immediately, escaping any HTML (this was supposed to\n\t\t\t\t\t// be a number after all)\n\t\t\t\t\tif ( isNaN( flo ) ) {\n\t\t\t\t\t\treturn __htmlEscapeEntities( d );\n\t\t\t\t\t}\n\t\n\t\t\t\t\tflo = flo.toFixed( precision );\n\t\t\t\t\td = Math.abs( flo );\n\t\n\t\t\t\t\tvar intPart = parseInt( d, 10 );\n\t\t\t\t\tvar floatPart = precision ?\n\t\t\t\t\t\tdecimal+(d - intPart).toFixed( precision ).substring( 2 ):\n\t\t\t\t\t\t'';\n\t\n\t\t\t\t\treturn negative + (prefix||'') +\n\t\t\t\t\t\tintPart.toString().replace(\n\t\t\t\t\t\t\t/\\B(?=(\\d{3})+(?!\\d))/g, thousands\n\t\t\t\t\t\t) +\n\t\t\t\t\t\tfloatPart +\n\t\t\t\t\t\t(postfix||'');\n\t\t\t\t}\n\t\t\t};\n\t\t},\n\t\n\t\ttext: function () {\n\t\t\treturn {\n\t\t\t\tdisplay: __htmlEscapeEntities,\n\t\t\t\tfilter: __htmlEscapeEntities\n\t\t\t};\n\t\t}\n\t};\n\t\n\t\n\t/*\n\t * This is really a good bit rubbish this method of exposing the internal methods\n\t * publicly... - To be fixed in 2.0 using methods on the prototype\n\t */\n\t\n\t\n\t/**\n\t * Create a wrapper function for exporting an internal functions to an external API.\n\t *  @param {string} fn API function name\n\t *  @returns {function} wrapped function\n\t *  @memberof DataTable#internal\n\t */\n\tfunction _fnExternApiFunc (fn)\n\t{\n\t\treturn function() {\n\t\t\tvar args = [_fnSettingsFromNode( this[DataTable.ext.iApiIndex] )].concat(\n\t\t\t\tArray.prototype.slice.call(arguments)\n\t\t\t);\n\t\t\treturn DataTable.ext.internal[fn].apply( this, args );\n\t\t};\n\t}\n\t\n\t\n\t/**\n\t * Reference to internal functions for use by plug-in developers. Note that\n\t * these methods are references to internal functions and are considered to be\n\t * private. If you use these methods, be aware that they are liable to change\n\t * between versions.\n\t *  @namespace\n\t */\n\t$.extend( DataTable.ext.internal, {\n\t\t_fnExternApiFunc: _fnExternApiFunc,\n\t\t_fnBuildAjax: _fnBuildAjax,\n\t\t_fnAjaxUpdate: _fnAjaxUpdate,\n\t\t_fnAjaxParameters: _fnAjaxParameters,\n\t\t_fnAjaxUpdateDraw: _fnAjaxUpdateDraw,\n\t\t_fnAjaxDataSrc: _fnAjaxDataSrc,\n\t\t_fnAddColumn: _fnAddColumn,\n\t\t_fnColumnOptions: _fnColumnOptions,\n\t\t_fnAdjustColumnSizing: _fnAdjustColumnSizing,\n\t\t_fnVisibleToColumnIndex: _fnVisibleToColumnIndex,\n\t\t_fnColumnIndexToVisible: _fnColumnIndexToVisible,\n\t\t_fnVisbleColumns: _fnVisbleColumns,\n\t\t_fnGetColumns: _fnGetColumns,\n\t\t_fnColumnTypes: _fnColumnTypes,\n\t\t_fnApplyColumnDefs: _fnApplyColumnDefs,\n\t\t_fnHungarianMap: _fnHungarianMap,\n\t\t_fnCamelToHungarian: _fnCamelToHungarian,\n\t\t_fnLanguageCompat: _fnLanguageCompat,\n\t\t_fnBrowserDetect: _fnBrowserDetect,\n\t\t_fnAddData: _fnAddData,\n\t\t_fnAddTr: _fnAddTr,\n\t\t_fnNodeToDataIndex: _fnNodeToDataIndex,\n\t\t_fnNodeToColumnIndex: _fnNodeToColumnIndex,\n\t\t_fnGetCellData: _fnGetCellData,\n\t\t_fnSetCellData: _fnSetCellData,\n\t\t_fnSplitObjNotation: _fnSplitObjNotation,\n\t\t_fnGetObjectDataFn: _fnGetObjectDataFn,\n\t\t_fnSetObjectDataFn: _fnSetObjectDataFn,\n\t\t_fnGetDataMaster: _fnGetDataMaster,\n\t\t_fnClearTable: _fnClearTable,\n\t\t_fnDeleteIndex: _fnDeleteIndex,\n\t\t_fnInvalidate: _fnInvalidate,\n\t\t_fnGetRowElements: _fnGetRowElements,\n\t\t_fnCreateTr: _fnCreateTr,\n\t\t_fnBuildHead: _fnBuildHead,\n\t\t_fnDrawHead: _fnDrawHead,\n\t\t_fnDraw: _fnDraw,\n\t\t_fnReDraw: _fnReDraw,\n\t\t_fnAddOptionsHtml: _fnAddOptionsHtml,\n\t\t_fnDetectHeader: _fnDetectHeader,\n\t\t_fnGetUniqueThs: _fnGetUniqueThs,\n\t\t_fnFeatureHtmlFilter: _fnFeatureHtmlFilter,\n\t\t_fnFilterComplete: _fnFilterComplete,\n\t\t_fnFilterCustom: _fnFilterCustom,\n\t\t_fnFilterColumn: _fnFilterColumn,\n\t\t_fnFilter: _fnFilter,\n\t\t_fnFilterCreateSearch: _fnFilterCreateSearch,\n\t\t_fnEscapeRegex: _fnEscapeRegex,\n\t\t_fnFilterData: _fnFilterData,\n\t\t_fnFeatureHtmlInfo: _fnFeatureHtmlInfo,\n\t\t_fnUpdateInfo: _fnUpdateInfo,\n\t\t_fnInfoMacros: _fnInfoMacros,\n\t\t_fnInitialise: _fnInitialise,\n\t\t_fnInitComplete: _fnInitComplete,\n\t\t_fnLengthChange: _fnLengthChange,\n\t\t_fnFeatureHtmlLength: _fnFeatureHtmlLength,\n\t\t_fnFeatureHtmlPaginate: _fnFeatureHtmlPaginate,\n\t\t_fnPageChange: _fnPageChange,\n\t\t_fnFeatureHtmlProcessing: _fnFeatureHtmlProcessing,\n\t\t_fnProcessingDisplay: _fnProcessingDisplay,\n\t\t_fnFeatureHtmlTable: _fnFeatureHtmlTable,\n\t\t_fnScrollDraw: _fnScrollDraw,\n\t\t_fnApplyToChildren: _fnApplyToChildren,\n\t\t_fnCalculateColumnWidths: _fnCalculateColumnWidths,\n\t\t_fnThrottle: _fnThrottle,\n\t\t_fnConvertToWidth: _fnConvertToWidth,\n\t\t_fnGetWidestNode: _fnGetWidestNode,\n\t\t_fnGetMaxLenString: _fnGetMaxLenString,\n\t\t_fnStringToCss: _fnStringToCss,\n\t\t_fnSortFlatten: _fnSortFlatten,\n\t\t_fnSort: _fnSort,\n\t\t_fnSortAria: _fnSortAria,\n\t\t_fnSortListener: _fnSortListener,\n\t\t_fnSortAttachListener: _fnSortAttachListener,\n\t\t_fnSortingClasses: _fnSortingClasses,\n\t\t_fnSortData: _fnSortData,\n\t\t_fnSaveState: _fnSaveState,\n\t\t_fnLoadState: _fnLoadState,\n\t\t_fnSettingsFromNode: _fnSettingsFromNode,\n\t\t_fnLog: _fnLog,\n\t\t_fnMap: _fnMap,\n\t\t_fnBindAction: _fnBindAction,\n\t\t_fnCallbackReg: _fnCallbackReg,\n\t\t_fnCallbackFire: _fnCallbackFire,\n\t\t_fnLengthOverflow: _fnLengthOverflow,\n\t\t_fnRenderer: _fnRenderer,\n\t\t_fnDataSource: _fnDataSource,\n\t\t_fnRowAttributes: _fnRowAttributes,\n\t\t_fnExtend: _fnExtend,\n\t\t_fnCalculateEnd: function () {} // Used by a lot of plug-ins, but redundant\n\t\t                                // in 1.10, so this dead-end function is\n\t\t                                // added to prevent errors\n\t} );\n\t\n\n\t// jQuery access\n\t$.fn.dataTable = DataTable;\n\n\t// Provide access to the host jQuery object (circular reference)\n\tDataTable.$ = $;\n\n\t// Legacy aliases\n\t$.fn.dataTableSettings = DataTable.settings;\n\t$.fn.dataTableExt = DataTable.ext;\n\n\t// With a capital `D` we return a DataTables API instance rather than a\n\t// jQuery object\n\t$.fn.DataTable = function ( opts ) {\n\t\treturn $(this).dataTable( opts ).api();\n\t};\n\n\t// All properties that are available to $.fn.dataTable should also be\n\t// available on $.fn.DataTable\n\t$.each( DataTable, function ( prop, val ) {\n\t\t$.fn.DataTable[ prop ] = val;\n\t} );\n\n\n\t// Information about events fired by DataTables - for documentation.\n\t/**\n\t * Draw event, fired whenever the table is redrawn on the page, at the same\n\t * point as fnDrawCallback. This may be useful for binding events or\n\t * performing calculations when the table is altered at all.\n\t *  @name DataTable#draw.dt\n\t *  @event\n\t *  @param {event} e jQuery event object\n\t *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}\n\t */\n\n\t/**\n\t * Search event, fired when the searching applied to the table (using the\n\t * built-in global search, or column filters) is altered.\n\t *  @name DataTable#search.dt\n\t *  @event\n\t *  @param {event} e jQuery event object\n\t *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}\n\t */\n\n\t/**\n\t * Page change event, fired when the paging of the table is altered.\n\t *  @name DataTable#page.dt\n\t *  @event\n\t *  @param {event} e jQuery event object\n\t *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}\n\t */\n\n\t/**\n\t * Order event, fired when the ordering applied to the table is altered.\n\t *  @name DataTable#order.dt\n\t *  @event\n\t *  @param {event} e jQuery event object\n\t *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}\n\t */\n\n\t/**\n\t * DataTables initialisation complete event, fired when the table is fully\n\t * drawn, including Ajax data loaded, if Ajax data is required.\n\t *  @name DataTable#init.dt\n\t *  @event\n\t *  @param {event} e jQuery event object\n\t *  @param {object} oSettings DataTables settings object\n\t *  @param {object} json The JSON object request from the server - only\n\t *    present if client-side Ajax sourced data is used</li></ol>\n\t */\n\n\t/**\n\t * State save event, fired when the table has changed state a new state save\n\t * is required. This event allows modification of the state saving object\n\t * prior to actually doing the save, including addition or other state\n\t * properties (for plug-ins) or modification of a DataTables core property.\n\t *  @name DataTable#stateSaveParams.dt\n\t *  @event\n\t *  @param {event} e jQuery event object\n\t *  @param {object} oSettings DataTables settings object\n\t *  @param {object} json The state information to be saved\n\t */\n\n\t/**\n\t * State load event, fired when the table is loading state from the stored\n\t * data, but prior to the settings object being modified by the saved state\n\t * - allowing modification of the saved state is required or loading of\n\t * state for a plug-in.\n\t *  @name DataTable#stateLoadParams.dt\n\t *  @event\n\t *  @param {event} e jQuery event object\n\t *  @param {object} oSettings DataTables settings object\n\t *  @param {object} json The saved state information\n\t */\n\n\t/**\n\t * State loaded event, fired when state has been loaded from stored data and\n\t * the settings object has been modified by the loaded data.\n\t *  @name DataTable#stateLoaded.dt\n\t *  @event\n\t *  @param {event} e jQuery event object\n\t *  @param {object} oSettings DataTables settings object\n\t *  @param {object} json The saved state information\n\t */\n\n\t/**\n\t * Processing event, fired when DataTables is doing some kind of processing\n\t * (be it, order, search or anything else). It can be used to indicate to\n\t * the end user that there is something happening, or that something has\n\t * finished.\n\t *  @name DataTable#processing.dt\n\t *  @event\n\t *  @param {event} e jQuery event object\n\t *  @param {object} oSettings DataTables settings object\n\t *  @param {boolean} bShow Flag for if DataTables is doing processing or not\n\t */\n\n\t/**\n\t * Ajax (XHR) event, fired whenever an Ajax request is completed from a\n\t * request to made to the server for new data. This event is called before\n\t * DataTables processed the returned data, so it can also be used to pre-\n\t * process the data returned from the server, if needed.\n\t *\n\t * Note that this trigger is called in `fnServerData`, if you override\n\t * `fnServerData` and which to use this event, you need to trigger it in you\n\t * success function.\n\t *  @name DataTable#xhr.dt\n\t *  @event\n\t *  @param {event} e jQuery event object\n\t *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}\n\t *  @param {object} json JSON returned from the server\n\t *\n\t *  @example\n\t *     // Use a custom property returned from the server in another DOM element\n\t *     $('#table').dataTable().on('xhr.dt', function (e, settings, json) {\n\t *       $('#status').html( json.status );\n\t *     } );\n\t *\n\t *  @example\n\t *     // Pre-process the data returned from the server\n\t *     $('#table').dataTable().on('xhr.dt', function (e, settings, json) {\n\t *       for ( var i=0, ien=json.aaData.length ; i<ien ; i++ ) {\n\t *         json.aaData[i].sum = json.aaData[i].one + json.aaData[i].two;\n\t *       }\n\t *       // Note no return - manipulate the data directly in the JSON object.\n\t *     } );\n\t */\n\n\t/**\n\t * Destroy event, fired when the DataTable is destroyed by calling fnDestroy\n\t * or passing the bDestroy:true parameter in the initialisation object. This\n\t * can be used to remove bound events, added DOM nodes, etc.\n\t *  @name DataTable#destroy.dt\n\t *  @event\n\t *  @param {event} e jQuery event object\n\t *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}\n\t */\n\n\t/**\n\t * Page length change event, fired when number of records to show on each\n\t * page (the length) is changed.\n\t *  @name DataTable#length.dt\n\t *  @event\n\t *  @param {event} e jQuery event object\n\t *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}\n\t *  @param {integer} len New length\n\t */\n\n\t/**\n\t * Column sizing has changed.\n\t *  @name DataTable#column-sizing.dt\n\t *  @event\n\t *  @param {event} e jQuery event object\n\t *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}\n\t */\n\n\t/**\n\t * Column visibility has changed.\n\t *  @name DataTable#column-visibility.dt\n\t *  @event\n\t *  @param {event} e jQuery event object\n\t *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}\n\t *  @param {int} column Column index\n\t *  @param {bool} vis `false` if column now hidden, or `true` if visible\n\t */\n\n\treturn $.fn.dataTable;\n}));\n\n\n/*! DataTables Bootstrap 3 integration\n * ©2011-2015 SpryMedia Ltd - datatables.net/license\n */\n\n/**\n * DataTables integration for Bootstrap 3. This requires Bootstrap 3 and\n * DataTables 1.10 or newer.\n *\n * This file sets the defaults and adds options to DataTables to style its\n * controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap\n * for further information.\n */\n(function( factory ){\n\tif ( typeof define === 'function' && define.amd ) {\n\t\t// AMD\n\t\tdefine( ['jquery', 'datatables.net'], function ( $ ) {\n\t\t\treturn factory( $, window, document );\n\t\t} );\n\t}\n\telse if ( typeof exports === 'object' ) {\n\t\t// CommonJS\n\t\tmodule.exports = function (root, $) {\n\t\t\tif ( ! root ) {\n\t\t\t\troot = window;\n\t\t\t}\n\n\t\t\tif ( ! $ || ! $.fn.dataTable ) {\n\t\t\t\t// Require DataTables, which attaches to jQuery, including\n\t\t\t\t// jQuery if needed and have a $ property so we can access the\n\t\t\t\t// jQuery object that is used\n\t\t\t\t$ = require('datatables.net')(root, $).$;\n\t\t\t}\n\n\t\t\treturn factory( $, root, root.document );\n\t\t};\n\t}\n\telse {\n\t\t// Browser\n\t\tfactory( jQuery, window, document );\n\t}\n}(function( $, window, document, undefined ) {\n'use strict';\nvar DataTable = $.fn.dataTable;\n\n\n/* Set the defaults for DataTables initialisation */\n$.extend( true, DataTable.defaults, {\n\tdom:\n\t\t\"<'row'<'col-sm-6'l><'col-sm-6'f>>\" +\n\t\t\"<'row'<'col-sm-12'tr>>\" +\n\t\t\"<'row'<'col-sm-5'i><'col-sm-7'p>>\",\n\trenderer: 'bootstrap'\n} );\n\n\n/* Default class modification */\n$.extend( DataTable.ext.classes, {\n\tsWrapper:      \"dataTables_wrapper form-inline dt-bootstrap\",\n\tsFilterInput:  \"form-control input-sm\",\n\tsLengthSelect: \"form-control input-sm\",\n\tsProcessing:   \"dataTables_processing panel panel-default\"\n} );\n\n\n/* Bootstrap paging button renderer */\nDataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, buttons, page, pages ) {\n\tvar api     = new DataTable.Api( settings );\n\tvar classes = settings.oClasses;\n\tvar lang    = settings.oLanguage.oPaginate;\n\tvar aria = settings.oLanguage.oAria.paginate || {};\n\tvar btnDisplay, btnClass, counter=0;\n\n\tvar attach = function( container, buttons ) {\n\t\tvar i, ien, node, button;\n\t\tvar clickHandler = function ( e ) {\n\t\t\te.preventDefault();\n\t\t\tif ( !$(e.currentTarget).hasClass('disabled') && api.page() != e.data.action ) {\n\t\t\t\tapi.page( e.data.action ).draw( 'page' );\n\t\t\t}\n\t\t};\n\n\t\tfor ( i=0, ien=buttons.length ; i<ien ; i++ ) {\n\t\t\tbutton = buttons[i];\n\n\t\t\tif ( $.isArray( button ) ) {\n\t\t\t\tattach( container, button );\n\t\t\t}\n\t\t\telse {\n\t\t\t\tbtnDisplay = '';\n\t\t\t\tbtnClass = '';\n\n\t\t\t\tswitch ( button ) {\n\t\t\t\t\tcase 'ellipsis':\n\t\t\t\t\t\tbtnDisplay = '&#x2026;';\n\t\t\t\t\t\tbtnClass = 'disabled';\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'first':\n\t\t\t\t\t\tbtnDisplay = lang.sFirst;\n\t\t\t\t\t\tbtnClass = button + (page > 0 ?\n\t\t\t\t\t\t\t'' : ' disabled');\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'previous':\n\t\t\t\t\t\tbtnDisplay = lang.sPrevious;\n\t\t\t\t\t\tbtnClass = button + (page > 0 ?\n\t\t\t\t\t\t\t'' : ' disabled');\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'next':\n\t\t\t\t\t\tbtnDisplay = lang.sNext;\n\t\t\t\t\t\tbtnClass = button + (page < pages-1 ?\n\t\t\t\t\t\t\t'' : ' disabled');\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'last':\n\t\t\t\t\t\tbtnDisplay = lang.sLast;\n\t\t\t\t\t\tbtnClass = button + (page < pages-1 ?\n\t\t\t\t\t\t\t'' : ' disabled');\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tbtnDisplay = button + 1;\n\t\t\t\t\t\tbtnClass = page === button ?\n\t\t\t\t\t\t\t'active' : '';\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tif ( btnDisplay ) {\n\t\t\t\t\tnode = $('<li>', {\n\t\t\t\t\t\t\t'class': classes.sPageButton+' '+btnClass,\n\t\t\t\t\t\t\t'id': idx === 0 && typeof button === 'string' ?\n\t\t\t\t\t\t\t\tsettings.sTableId +'_'+ button :\n\t\t\t\t\t\t\t\tnull\n\t\t\t\t\t\t} )\n\t\t\t\t\t\t.append( $('<a>', {\n\t\t\t\t\t\t\t\t'href': '#',\n\t\t\t\t\t\t\t\t'aria-controls': settings.sTableId,\n\t\t\t\t\t\t\t\t'aria-label': aria[ button ],\n\t\t\t\t\t\t\t\t'data-dt-idx': counter,\n\t\t\t\t\t\t\t\t'tabindex': settings.iTabIndex\n\t\t\t\t\t\t\t} )\n\t\t\t\t\t\t\t.html( btnDisplay )\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.appendTo( container );\n\n\t\t\t\t\tsettings.oApi._fnBindAction(\n\t\t\t\t\t\tnode, {action: button}, clickHandler\n\t\t\t\t\t);\n\n\t\t\t\t\tcounter++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\t// IE9 throws an 'unknown error' if document.activeElement is used\n\t// inside an iframe or frame. \n\tvar activeEl;\n\n\ttry {\n\t\t// Because this approach is destroying and recreating the paging\n\t\t// elements, focus is lost on the select button which is bad for\n\t\t// accessibility. So we want to restore focus once the draw has\n\t\t// completed\n\t\tactiveEl = $(host).find(document.activeElement).data('dt-idx');\n\t}\n\tcatch (e) {}\n\n\tattach(\n\t\t$(host).empty().html('<ul class=\"pagination\"/>').children('ul'),\n\t\tbuttons\n\t);\n\n\tif ( activeEl !== undefined ) {\n\t\t$(host).find( '[data-dt-idx='+activeEl+']' ).trigger('focus');\n\t}\n};\n\n\nreturn DataTable;\n}));\n\n\n"
  },
  {
    "path": "gui/static/js/domains.js",
    "content": ";(function(window, document) {\n  'use strict';\n\n  function processDomainType(value) {\n    var modal_body = $('.modal-body');\n\n    if (value == \"msteams-nosub\") {\n      //console.log(\"Clicked on authtype \" + value);\n      modal_body.find(\".domain_name\").attr('disabled', true);\n      modal_body.find(\".pbx_list\").attr('disabled', true);\n      modal_body.find(\".notes\").attr('disabled', true);\n      modal_body.find(\":submit\").attr('disabled', true);\n      //modal_body.find(\":submit\").attr('disabled',true);\n    }\n    else {\n      modal_body.find(\".domain_name\").attr('disabled', false);\n      modal_body.find(\".pbx_list\").attr('disabled', false);\n      modal_body.find(\".notes\").attr('disabled', false);\n      modal_body.find(\":submit\").attr('disabled', false);\n    }\n  }\n\n  /* validate domain form fields before submitting */\n  function validateDomainFormFields(fields) {\n    var domainlist_obj = fields.get('domainlist');\n    // empty string will return [''] here\n    var domains = domainlist_obj.val().split(',');\n    for (var i=0; i<domains.length; i++) {\n      if (domains[i].trim() === '') {\n        return {\n          result: false,\n          err_node: domainlist_obj,\n          err_msg: \"Domain can not be an empty string\"\n        };\n      }\n    }\n\n    return {\n      result: true\n    };\n  }\n\n\n  function updateModals() {\n\n    $(document).ready(function() {\n\n      // Updates the modal with domain to be deleted\n      $('#domains #open-Delete').off('click').on('click', function () {\n        var row_index = $(this).parent().parent().parent().index() + 1;\n        var c = document.getElementById('domains');\n        var domain_id = $(c).find('tr:eq(' + row_index + ') td:eq(1)').text();\n        var domain_name = $(c).find('tr:eq(' + row_index + ') td:eq(2)').text();\n\n        /* update modal fields */\n        var modal_body = $('#delete .modal-body');\n        modal_body.find(\".domain_id\").val(domain_id);\n        modal_body.find(\".domain_name\").val(domain_name);\n      });\n\n      // Updates the modal with the values from the endpointgroup API\n      $('#domains #open-Update').off('click').on('click', function () {\n\n        var row_index = $(this).parent().parent().parent().index() + 1;\n        var c = document.getElementById('domains');\n        var domain_id = $(c).find('tr:eq(' + row_index + ') td:eq(1)').text();\n        var domain_name = $(c).find('tr:eq(' + row_index + ') td:eq(2)').text();\n        var domain_type = $(c).find('tr:eq(' + row_index + ') td:eq(3)').text();\n        var pbx_name = $(c).find('tr:eq(' + row_index + ') td:eq(4)').text();\n        var authtype = $(c).find('tr:eq(' + row_index + ') td:eq(5)').text();\n        var pbx_list = $(c).find('tr:eq(' + row_index + ') td:eq(6)').text();\n        var notes = $(c).find('tr:eq(' + row_index + ') td:eq(7)').text();\n\n\n        /** Clear out and reset the modal */\n        var modal_body = $('#edit .modal-body');\n        modal_body.find(\".domain_name\").attr('disabled', false);\n        modal_body.find(\".pbx_list\").attr('disabled', false);\n        modal_body.find(\".notes\").attr('disabled', false);\n        modal_body.find(\":submit\").attr('disabled', false);\n        modal_body.find(\".domain_id\").val('');\n        modal_body.find(\".domain_name\").val('');\n        modal_body.find(\".domain_type\").val('');\n        modal_body.find(\".pbx_name\").val('');\n        modal_body.find(\".pbx_list\").val('');\n        modal_body.find(\".notes\").val('');\n        modal_body.find('.authtype').val([]);\n        modal_body.find('.authtype').val(\"\");\n\n        /* update modal fields */\n        modal_body.find(\".domain_id\").val(domain_id);\n        modal_body.find(\".domain_name\").val(domain_name);\n        modal_body.find(\".domain_type\").val(domain_type);\n        modal_body.find(\".pbx_name\").val(pbx_name);\n        modal_body.find(\".pbx_list\").val(pbx_list);\n        modal_body.find(\".notes\").val(notes);\n\n        if (authtype !== \"\") {\n          /* Set the radio button if authtype is given */\n          //modal_body.find('.authtype option[value=\"' + authtype + '\"]').attr('selected', 'selected').trigger(\"change\");\n          modal_body.find('.authtype').val(authtype)\n        }\n      });\n\n    });\n  }\n\n\n  $(document).ready(function() {\n    // datatable init\n    $('#domains')\n      .on('page.dt', function() { updateModals(); } )\n      .DataTable({\n      \"columnDefs\": [\n        {\"orderable\": true, \"targets\": [1, 2, 3, 4, 5, 6, 7]},\n        {\"orderable\": false, \"targets\": [0, 8, 9]},\n      ],\n      \"order\": [[1, 'asc']]\n    });\n\n\n    // Update the modals on the first page load\n    updateModals();\n\n    // Resets the Add Modal\n    $('#open-DomainAdd').click(function() {\n      /** Clear out and reset the modal */\n      var modal_body = $('#add .modal-body');\n      modal_body.find(\".domain_name\").attr('disabled', false);\n      modal_body.find(\".pbx_list\").attr('disabled', false);\n      modal_body.find(\".notes\").attr('disabled', false);\n      modal_body.find(\":submit\").attr('disabled', false);\n      modal_body.find(\".domain_id\").val('');\n      modal_body.find(\".domain_name\").val('');\n      modal_body.find(\".domain_type\").val('');\n      modal_body.find(\".pbx_name\").val('');\n      modal_body.find(\".pbx_list\").val('');\n      modal_body.find(\".notes\").val('');\n      modal_body.find('.authtype').val([]);\n      modal_body.find('.authtype').val(\"\");\n\n    });   \n\n    $('#add .authtype').change(function() {\n      var modal_body = $('#add .modal-body');\n      var type = modal_body.find('.authtype').val();\n\n      processDomainType(type);\n    });\n\n    $('#edit .authtype').change(function() {\n      var modal_body = $('#edit .modal-body');\n      var type = modal_body.find('.authtype').val();\n\n      processDomainType(type);\n    });\n\n    $('#addDomainForm').submit(function(ev) {\n      if (!validateFields(this, validateDomainFormFields)) {\n        // prevent form from submitting if it failed\n        ev.preventDefault();\n        // prevent jquery from propagating event\n        return false;\n      }\n    });\n\n    $('#updateDomainForm').submit(function(ev) {\n      if (!validateFields(this, validateDomainFormFields)) {\n        ev.preventDefault();\n        return false;\n      }\n    });\n  });\n})(window, document);\n"
  },
  {
    "path": "gui/static/js/endpointgroups.js",
    "content": ";(function(window, document) {\n  'use strict';\n\n  // throw an error if required functions not defined\n  if (typeof validateFields === \"undefined\") {\n    throw new Error(\"validateFields() is required and is not defined\");\n  }\n  if (typeof showNotification === \"undefined\") {\n    throw new Error(\"showNotification() is required and is not defined\");\n  }\n  if (typeof toggleElemDisabled === \"undefined\") {\n    throw new Error(\"toggleElemDisabled() is required and is not defined\");\n  }\n  if (typeof reloadKamRequired === \"undefined\") {\n    throw new Error(\"reloadKamRequired() is required and is not defined\");\n  }\n\n  // throw an error if required globals not defined\n  if (typeof API_BASE_URL === \"undefined\") {\n    throw new Error(\"API_BASE_URL is required and is not defined\");\n  }\n\n  // global constants for this script\n  const SIGNAL_OPTIONS = {\n    \"proxy\": \"Unaltered\",\n    \"sip_udp\": \"SIP over UDP\",\n    \"sip_tcp\": \"SIP over TCP\",\n    \"sip_sctp\": \"SIP over SCTP\",\n    // \"sip_ws\": \"SIP over WS\",\n    \"sips_tls\": \"SIPS over TLS\",\n    \"sips_sctp\": \"SIPS over SCTP\",\n    // \"sips_wss\": \"SIPS over WSS\"\n  };\n  const SIGNAL_OPTIONS_STR = JSON.stringify(SIGNAL_OPTIONS);\n  // TODO: think of a more user friendly description for these options\n  const MEDIA_OPTIONS = {\n    \"proxy\": \"Proxy Media\",\n    \"direct\": \"Direct Media\",\n    \"rtp_avp\": \"RTP/AVP\",\n    \"rtp_savp\": \"RTP/SAVP\",\n    \"rtp_avpf\": \"RTP/AVPF\",\n    \"rtp_savpf\": \"RTP/SAVPF\",\n    \"rtp_avp_any\": \"UDP/TLS/RTP/SAVP\",\n    \"rtp_avpf_any\": \"UDP/TLS/RTP/SAVPF\",\n    \"udptl\": \"T.38 over UDPTL\",\n    \"osrtp_avp\": \"OSRTP over RTP/AVP\",\n    \"osrtp_avpf\": \"OSRTP over RTP/AVPF\"\n  };\n  const MEDIA_OPTIONS_STR = JSON.stringify(MEDIA_OPTIONS);\n  const KEEPALIVE_OPTIONS = {\n    0: \"disabled\",\n    1: \"enabled\"\n  };\n  const KEEPALIVE_OPTIONS_STR = JSON.stringify(KEEPALIVE_OPTIONS);\n\n  // global variables/constants for this script\n  // TODO: find a way to pass these values around gwgroupid instead of using global\n  var gwgroupid;\n  var endpoint_table1;\n  var endpoint_table2;\n  var gwgroup_table;\n\n  function generateEndpointObject(row) {\n    var jq_row = $(row);\n    return {\n      gwid: parseInt(jq_row.find('input[name=\"gwid\"]').val(), 10),\n      host: jq_row.find('input[name=\"host\"]').val(),\n      port: parseInt(jq_row.find('input[name=\"port\"]').val(), 10),\n      signalling: jq_row.find('select[name=\"signalling\"]').val(),\n      media: jq_row.find('select[name=\"media\"]').val(),\n      description: jq_row.find('input[name=\"description\"]').val(),\n      rweight: parseInt(jq_row.find('input[name=\"rweight\"]').val(), 10),\n      keepalive: parseInt(jq_row.find('select[name=\"keepalive\"]').val(), 10),\n    };\n  }\n\n  /**\n   * Generate the markup for an endpoint wrapped in a query object\n   * @param endpoint\n   * @returns jQuery\n   */\n  function generateEndpointMarkup(endpoint = null) {\n    if (endpoint === null) {\n      return $('<tr class=\"endpoint\">' +\n        '<td name=\"gwid\"></td>' +\n        '<td name=\"host\"></td>' +\n        '<td name=\"port\"></td>' +\n        '<td name=\"signalling\"></td>' +\n        '<td name=\"media\"></td>' +\n        '<td name=\"description\"></td>' +\n        '<td name=\"rweight\">1</td>' +\n        '<td name=\"keepalive\"></td>' +\n        '</tr>');\n    }\n    else {\n      return $('<tr class=\"endpoint\">' +\n        '<td name=\"gwid\">' + endpoint.gwid.toString() + '</td>' +\n        '<td name=\"host\">' + endpoint.host + '</td>' +\n        '<td name=\"port\">' + endpoint.port + '</td>' +\n        '<td name=\"signalling\">' + SIGNAL_OPTIONS[endpoint.signalling] + '</td>' +\n        '<td name=\"media\">' + MEDIA_OPTIONS[endpoint.media] + '</td>' +\n        '<td name=\"description\">' + endpoint.description + '</td>' +\n        '<td name=\"rweight\">' + endpoint.rweight.toString() + '</td>' +\n        '<td name=\"keepalive\">' + KEEPALIVE_OPTIONS[endpoint.keepalive] + '</td>' +\n        '</tr>');\n    }\n  }\n\n  function generateEndpointTable(selector) {\n    var endpoint_table = $(selector);\n\n    endpoint_table.Tabledit({\n      columns: {\n        identifier: [0, 'gwid'],\n        editable: [\n          [1, 'host'],\n          [2, 'port'],\n          [3, 'signalling', SIGNAL_OPTIONS_STR],\n          [4, 'media', MEDIA_OPTIONS_STR],\n          [5, 'description'],\n          [6, 'rweight'],\n          [7, 'keepalive', KEEPALIVE_OPTIONS_STR],\n        ],\n        saveButton: true,\n      },\n      ajaxDisabled: true,\n      restoreButton: false\n    });\n\n    return endpoint_table;\n  }\n\n  // Add EndpointGroup\n  function addEndpointGroup(action) {\n    var selector, modal_body, url, tmp;\n\n    // The default action is a POST (creating a new EndpointGroup)\n    if (typeof action === \"undefined\") {\n      action = \"POST\";\n    }\n\n    if (action === \"POST\") {\n      action = \"POST\";\n      selector = \"#add\";\n      modal_body = $(selector + ' .modal-body');\n      url = API_BASE_URL + \"endpointgroups\";\n    }\n    // Grab the Gateway Group ID if updating using a PUT\n    else if (action === \"PUT\") {\n      selector = \"#edit\";\n      modal_body = $(selector + ' .modal-body');\n      gwgroupid = modal_body.find(\".gwgroupid\").val();\n      url = API_BASE_URL + \"endpointgroups/\" + gwgroupid;\n    }\n    else {\n      throw new Error(\"addEndpointGroup(): action must be either POST or PUT\");\n    }\n\n    var requestPayload = {};\n    requestPayload.name = modal_body.find(\".name\").val();\n\n    var call_settings = {};\n    tmp = modal_body.find(\".call_limit\").val();\n    call_settings.limit = tmp === '' ? null : tmp;\n    tmp = modal_body.find(\".call_timeout\").val();\n    call_settings.timeout = tmp === '' ? null : tmp;\n    requestPayload.call_settings = call_settings;\n\n    var auth = {};\n    if (action === \"POST\") {\n      if ($('input#ip.authtype').is(':checked')) {\n        auth.type = \"ip\";\n      }\n      else {\n        auth.type = \"userpwd\";\n        auth.pass = modal_body.find(\"#auth_password\").val();\n      }\n    }\n    else if (action === \"PUT\") {\n      if ($('input#ip2.authtype').is(':checked')) {\n        auth.type = \"ip\";\n      }\n      else {\n        auth.type = \"userpwd\";\n        auth.pass = modal_body.find(\"#auth_password2\").val();\n      }\n    }\n\n    auth.user = modal_body.find(\".auth_username\").val();\n    auth.domain = modal_body.find(\".auth_domain\").val();\n    requestPayload.auth = auth;\n\n    requestPayload.strip = modal_body.find(\".strip\").val();\n    requestPayload.prefix = modal_body.find(\".prefix\").val();\n\n    var notifications = {};\n    notifications.overmaxcalllimit = modal_body.find(\".email_over_max_calls\").val();\n    notifications.endpointfailure = modal_body.find(\".email_endpoint_failure\").val();\n    requestPayload.notifications = notifications;\n\n    var cdr = {};\n    cdr.cdr_email = modal_body.find(\".cdr_email\").val();\n    cdr.cdr_send_interval = modal_body.find(\".cdr_send_minute\").val() + ' ' +\n      modal_body.find(\".cdr_send_hour\").val() + ' ' +\n      modal_body.find(\".cdr_send_day\").val() + ' ' +\n      modal_body.find(\".cdr_send_month\").val() + ' ' +\n      modal_body.find(\".cdr_send_weekday\").val();\n    requestPayload.cdr = cdr;\n\n    var fusionpbx = {};\n    fusionpbx.enabled = modal_body.find(\".fusionpbx_db_enabled\").val();\n    fusionpbx.dbhost = modal_body.find(\".fusionpbx_db_server\").val();\n    fusionpbx.dbuser = modal_body.find(\".fusionpbx_db_username\").val();\n    fusionpbx.dbpass = modal_body.find(\".fusionpbx_db_password\").val();\n    fusionpbx.clustersupport = modal_body.find(\".fusionpbx_clustersupport\").val();\n    requestPayload.fusionpbx = fusionpbx;\n\n    /* Process endpoints (empty endpoints are ignored) */\n    requestPayload.endpoints = $(\"tr.endpoint\").map(function(idx, row) {\n      return generateEndpointObject(row);\n    }).get();\n\n    // set payload defaults for numbers\n    // doing it here allows us to keep placeholder on the input\n    if (requestPayload.strip.length === 0) {\n      requestPayload.strip = 0;\n    }\n\n    // Put into JSON Message and send over\n    $.ajax({\n      type: action,\n      url: url,\n      dataType: \"json\",\n      contentType: \"application/json; charset=utf-8\",\n      data: JSON.stringify(requestPayload),\n      success: function(response, textStatus, jqXHR) {\n        var btn;\n        var gwgroupid_int = response.data[0].gwgroupid;\n\n        // Update the Add Button and the table\n        if (action === \"POST\") {\n          btn = $('#add .modal-footer').find('#addButton');\n          btn.removeClass(\"btn-primary\");\n        }\n        else {\n          btn = $('#edit .modal-footer').find('#updateButton');\n          btn.removeClass(\"btn-warning\");\n        }\n\n        btn.addClass(\"btn-success\");\n        btn.html(\"<span class='glyphicon glyphicon-check'></span> Saved!\");\n        btn.attr(\"disabled\", true);\n\n        // Update Reload buttons\n        reloadKamRequired(true);\n\n        if (action === \"POST\") {\n          gwgroup_table.row.add({\n            \"name\": requestPayload.name,\n            \"gwgroupid\": gwgroupid_int\n          }).draw();\n        }\n        else {\n          gwgroup_table.row(function(idx, data, node) {\n            return data.gwgroupid === gwgroupid_int;\n          }).data({\n            \"name\": requestPayload.name,\n            \"gwgroupid\": gwgroupid_int\n          }).draw();\n        }\n      }\n    })\n  }\n\n  function updateEndpointGroup() {\n    addEndpointGroup(\"PUT\");\n  }\n\n  function clearEndpointGroupModal(modal_selector) {\n    /** Clear out the modal */\n    var modal_body = $(modal_selector).find('.modal-body');\n    modal_body.find(\".gwgroupid\").val('');\n    modal_body.find(\".name\").val('');\n    modal_body.find(\".ip_addr\").val('');\n    modal_body.find(\".strip\").val('');\n    modal_body.find(\".prefix\").val('');\n    modal_body.find(\".fusionpbx_db_server\").val('');\n    modal_body.find(\".fusionpbx_db_username\").val('fusionpbx');\n    modal_body.find(\".fusionpbx_db_password\").val('');\n    modal_body.find(\".authtype[value='ip']\").trigger('click');\n    modal_body.find(\".auth_username\").val('');\n    modal_body.find(\".auth_password\").val('');\n    modal_body.find(\".auth_domain\").val('');\n    modal_body.find(\".call_limit\").val('');\n    modal_body.find(\".call_timeout\").val('');\n    modal_body.find(\".email_over_max_calls\").val('');\n    modal_body.find(\".email_endpoint_failure\").val('');\n    modal_body.find(\".cdr_email\").val('');\n    modal_body.find(\".cdr_send_minute\").val('*');\n    modal_body.find(\".cdr_send_hour\").val('*');\n    modal_body.find(\".cdr_send_day\").val('1');\n    modal_body.find(\".cdr_send_month\").val('*');\n    modal_body.find(\".cdr_send_weekday\").val('*');\n    modal_body.find('.FusionPBXDomainOptions').addClass(\"hidden\");\n    modal_body.find('.updateButton').attr(\"disabled\", false);\n\n    // Clear out update button in add footer\n    var modal_footer = modal_body.find('.modal-footer');\n    modal_footer.find(\"#addButton\").attr(\"disabled\", false);\n\n    // Clear out update button in add footer\n    modal_footer.find(\"#updateButton\").attr(\"disabled\", false);\n\n    var btn;\n    if (modal_selector == \"#add\") {\n      btn = $('#add .modal-footer').find('#addButton');\n      btn.html(\"<span class='glyphicon glyphicon-ok-sign'></span> Add\");\n      btn.removeClass(\"btn-success\");\n      btn.addClass(\"btn-primary\");\n    }\n    else {\n      btn = $('#edit .modal-footer').find('#updateButton');\n      btn.html(\"<span class='glyphicon glyphicon-ok-sign'></span> Update\");\n      btn.removeClass(\"btn-success\");\n      btn.addClass(\"btn-warning\");\n    }\n    btn.attr('disabled', false);\n\n    // Remove Endpont Rows\n    $(\"tr.endpoint\").each(function(i, row) {\n      $(this).remove();\n    })\n\n    /* start endpoint-nav on first tab */\n    modal_body.find('#endpoint-nav .nav-tabs > li').removeClass(\"active\");\n    modal_body.find('#endpoint-nav > .nav-tabs a').first().trigger('click');\n\n    // make sure userpwd options not shown\n    modal_body.find('.userpwd').addClass('hidden');\n\n    /* make sure ip_addr not disabled */\n    toggleElemDisabled(modal_body.find('.ip_addr'), false);\n  }\n\n  function displayEndpointGroup(gwgroup_data) {\n    var modal_body = $('#edit .modal-body');\n    modal_body.find(\".name\").val(gwgroup_data.name);\n    modal_body.find(\".gwgroupid\").val(gwgroup_data.gwgroupid);\n    modal_body.find(\".call_limit\").val(gwgroup_data.call_settings.limit);\n    modal_body.find(\".call_timeout\").val(gwgroup_data.call_settings.timeout);\n\n    if (gwgroup_data.auth.type == \"ip\") {\n      $('#ip2.authtype').prop('checked', true);\n      $(\"#userpwd_enabled2\").addClass('hidden');\n      $(\"#userpwd_enabled\").addClass('hidden');\n    }\n    else {\n      $('#userpwd2.authtype').prop('checked', true);\n      $(\"#userpwd_enabled2\").removeClass('hidden');\n      $(\"#userpwd_enabled\").removeClass('hidden');\n    }\n\n    // parse the cdr_send_interval\n    var send_interval = gwgroup_data.cdr.cdr_send_interval;\n\n    modal_body.find(\".auth_username\").val(gwgroup_data.auth.user);\n    modal_body.find(\"#auth_password2\").val(gwgroup_data.auth.pass);\n    modal_body.find(\"#auth_password\").val(gwgroup_data.auth.pass);\n    modal_body.find(\".auth_domain\").val(gwgroup_data.auth.domain);\n    modal_body.find(\".strip\").val(gwgroup_data.strip);\n    modal_body.find(\".prefix\").val(gwgroup_data.prefix);\n    modal_body.find(\".email_over_max_calls\").val(gwgroup_data.notifications.overmaxcalllimit);\n    modal_body.find(\".email_endpoint_failure\").val(gwgroup_data.notifications.endpointfailure);\n    modal_body.find(\".cdr_email\").val(gwgroup_data.cdr.cdr_email);\n    if (send_interval) {\n      send_interval = send_interval.split(' ');\n      modal_body.find(\".cdr_send_minute\").val(send_interval[0]);\n      modal_body.find(\".cdr_send_hour\").val(send_interval[1]);\n      modal_body.find(\".cdr_send_day\").val(send_interval[2]);\n      modal_body.find(\".cdr_send_month\").val(send_interval[3]);\n      modal_body.find(\".cdr_send_weekday\").val(send_interval[4]);\n    }\n    modal_body.find(\".fusionpbx_db_enabled\").val(gwgroup_data.fusionpbx.enabled);\n    modal_body.find(\".fusionpbx_db_server\").val(gwgroup_data.fusionpbx.dbhost);\n    modal_body.find(\".fusionpbx_db_username\").val(gwgroup_data.fusionpbx.dbuser);\n    modal_body.find(\".fusionpbx_db_password\").val(gwgroup_data.fusionpbx.dbpass);\n    modal_body.find(\".fusionpbx_clustersupport\").val(gwgroup_data.fusionpbx.clustersupport);\n\n    /* reset the save button*/\n    var updatebtn = $('#edit .modal-footer').find(\"#updateButton\");\n    updatebtn.removeClass(\"btn-success\");\n    updatebtn.addClass(\"btn-warning\");\n    updatebtn.html(\"<span class='glyphicon glyphicon-ok-sign'></span>Update\");\n\n    if (gwgroup_data.endpoints) {\n      for (var i = 0; i < gwgroup_data.endpoints.length; i++) {\n        endpoint_table1.append(generateEndpointMarkup(gwgroup_data.endpoints[i]));\n      }\n      endpoint_table1.data('Tabledit').reload();\n    }\n\n    if (gwgroup_data.fusionpbx.enabled) {\n      modal_body.find(\".toggleFusionPBXDomain\").bootstrapToggle('on');\n    }\n    else {\n      modal_body.find(\".toggleFusionPBXDomain\").bootstrapToggle('off');\n    }\n\n    if (gwgroup_data.auth.type == \"userpwd\") {\n      /* userpwd auth enabled, Set the radio button to true */\n      modal_body.find('.authtype[data-toggle=\"userpwd_enabled\"]').trigger('click');\n    }\n    else {\n      /* ip auth enabled, Set the radio button to true */\n      modal_body.find('.authtype[data-toggle=\"ip_enabled\"]').trigger('click');\n    }\n  }\n\n  function deleteEndpointGroup() {\n    var gwgroupid_int = parseInt(gwgroupid, 10);\n\n    $.ajax({\n      type: \"DELETE\",\n      url: API_BASE_URL + \"endpointgroups/\" + gwgroupid,\n      dataType: \"json\",\n      contentType: \"application/json; charset=utf-8\",\n      success: function(response, textStatus, jqXHR) {\n        $('#delete').modal('hide');\n        $('#edit').modal('hide');\n\n        // Update Reload buttons\n        reloadKamRequired(true);\n\n        gwgroup_table.row(function(idx, data, node) {\n          return data.gwgroupid === gwgroupid_int;\n        }).remove().draw();\n      }\n    });\n  }\n\n  $(document).ready(function() {\n    // datatable init\n    gwgroup_table = $('#endpointgroups').DataTable({\n      \"ajax\": {\n        \"url\": API_BASE_URL + \"endpointgroups\"\n      },\n      \"columns\": [\n        {\"data\": \"name\"},\n        {\"data\": \"gwgroupid\"}\n        //{ \"data\": \"gwlist\", visible: false },\n      ],\n      \"order\": [[1, 'asc']]\n    });\n\n    // table editing by clicking on the row\n    $('#endpointgroups tbody').on('click', 'tr', function() {\n      //Turn off selected on any other rows\n      $('#endpointgroups').find('tr').removeClass('selected');\n\n      if ($(this).hasClass('selected')) {\n        $(this).removeClass('selected');\n      }\n      else {\n        //table.$('tr.selected').removeClass('selected');\n        $(this).addClass('selected');\n        gwgroupid = $(this).find('td').eq(1).text()\n        //console.log(gwgroupid);\n        $('#edit').modal('show');\n      }\n    });\n\n    /* edit modal tabledit init */\n    endpoint_table1 = generateEndpointTable('#endpoint-table');\n\n    /* add modal tabledit init */\n    endpoint_table2 = generateEndpointTable('#endpoint-table2');\n\n    $('#edit').on('show.bs.modal', function() {\n      clearEndpointGroupModal('#edit');\n\n      // Show the auth tab by default when the modal shows\n      var modal_body = $('#edit .modal-body');\n      modal_body.find(\"[name='auth-toggle']\").trigger('click');\n\n      // Put into JSON Message and send over\n      $.ajax({\n        type: \"GET\",\n        url: API_BASE_URL + \"endpointgroups/\" + gwgroupid,\n        dataType: \"json\",\n        contentType: \"application/json; charset=utf-8\",\n        success: function(response, textStatus, jqXHR) {\n          displayEndpointGroup(response.data[0]);\n        }\n      })\n    });\n\n    $('#addEndpointRow').on('click', function() {\n      endpoint_table2.append(generateEndpointMarkup());\n      endpoint_table2.data('Tabledit').reload();\n      endpoint_table2.find(\"tbody tr:last td:last .tabledit-edit-button\").trigger(\"click\");\n    });\n\n    $('#updateEndpointRow').on('click', function() {\n      endpoint_table1.append(generateEndpointMarkup());\n      endpoint_table1.data('Tabledit').reload();\n      endpoint_table1.find(\"tbody tr:last td:last .tabledit-edit-button\").trigger(\"click\");\n    });\n\n    $('.modal-body .fusionpbx_clustersupport').change(function() {\n      var modal = $(this).closest('div.modal');\n      var modal_body = modal.find('.modal-body');\n\n      if ($(this).is(\":checked\") || $(this).prop(\"checked\")) {\n        modal_body.find('.fusionpbx_clustersupport').val(1);\n      }\n      else {\n        modal_body.find('.fusionpbx_clustersupport').val(0);\n      }\n    });\n\n    /* listener for fusionPBX toggle */\n    $('.modal-body .toggleFusionPBXDomain').change(function() {\n      var self = $(this);\n      var modal = self.closest('div.modal');\n      var modal_body = modal.find('.modal-body');\n\n      if (self.is(\":checked\") || self.prop(\"checked\")) {\n        modal_body.find('.FusionPBXDomainOptions').removeClass(\"hidden\");\n        modal_body.find('.fusionpbx_db_enabled').val(1);\n        self.bootstrapToggle('on');\n      }\n      else {\n        modal_body.find('.FusionPBXDomainOptions').addClass(\"hidden\");\n        modal_body.find('.fusionpbx_db_enabled').val(0);\n        self.bootstrapToggle('off');\n      }\n    });\n\n    /* listener for freePBX toggle */\n    $('.modal-body .toggleFreePBXDomain').change(function() {\n      var self = $(this);\n      var modal = self.closest('div.modal');\n      var modal_body = modal.find('.modal-body');\n\n      if (self.is(\":checked\") || self.prop(\"checked\")) {\n        modal_body.find('.FreePBXDomainOptions').removeClass(\"hidden\");\n        modal_body.find('.freepbx_enabled').val(1);\n        self.bootstrapToggle('on');\n      }\n      else {\n        modal_body.find('.FreePBXDomainOptions').addClass(\"hidden\");\n        modal_body.find('.freepbx_enabled').val(0);\n        self.bootstrapToggle('off');\n      }\n    });\n\n    $(\".toggle-password\").on('click', function() {\n      var input = $($(this).attr(\"toggle\"));\n      if (input.attr(\"type\") == \"password\") {\n        input.attr(\"type\", \"text\");\n        $(this).removeClass(\"glyphicon glyphicon-eye-close\");\n        $(this).addClass(\"glyphicon glyphicon-eye-open\");\n      }\n      else {\n        input.attr(\"type\", \"password\");\n        $(this).removeClass(\"glyphicon glyphicon-eye-open\");\n        $(this).addClass(\"glyphicon glyphicon-eye-close\");\n      }\n    });\n\n    $(\"#authoptions :input\").change(function() {\n      var userpwd_div = $('#userpwd_enabled');\n      var authpwd_inp = $(\"#auth_password\");\n      var togglepwd_span = $(\".toggle-password\");\n\n      if ($('#ip').is(':checked')) {\n        userpwd_div.addClass('hidden');\n      }\n      else {\n        $.ajax({\n          type: \"GET\",\n          url: API_BASE_URL + \"sys/generatepassword\",\n          dataType: \"json\",\n          contentType: \"application/json; charset=utf-8\",\n          success: function(response, textStatus, jqXHR) {\n            authpwd_inp.attr(\"type\", \"text\");\n            authpwd_inp.val(response.data[0])\n            togglepwd_span.removeClass(\"glyphicon glyphicon-eye-close\");\n            togglepwd_span.addClass(\"glyphicon glyphicon-eye-open\");\n          }\n        });\n\n        userpwd_div.removeClass('hidden');\n      }\n    });\n\n    $(\"#authoptions2 :input\").change(function() {\n      var userpwd_div = $('#userpwd_enabled2');\n\n      if ($('#ip2').is(':checked')) {\n        userpwd_div.addClass('hidden');\n      }\n      else {\n        userpwd_div.removeClass('hidden');\n      }\n    });\n\n    $('#open-EndpointGroupsAdd').click(function() {\n      clearEndpointGroupModal('#add');\n    });\n\n    /* validate fields before submitting api request */\n    $('#addButton').click(function(ev) {\n      /* prevent form default submit */\n      ev.preventDefault();\n\n      if (validateFields('#add')) {\n        addEndpointGroup();\n        // hide the modal after 1.5 sec\n        setTimeout(function() {\n          var add_modal = $('#add');\n          if (add_modal.is(':visible')) {\n            add_modal.modal('hide');\n          }\n        }, 1500);\n      }\n    });\n\n    /* validate fields before submitting api request */\n    $('#updateButton').click(function(ev) {\n      /* prevent form default submit */\n      ev.preventDefault();\n\n      if (validateFields('#edit')) {\n        updateEndpointGroup();\n        // hide the modal after 1.5 sec\n        setTimeout(function() {\n          var edit_modal = $('#edit');\n          if (edit_modal.is(':visible')) {\n            edit_modal.modal('hide');\n          }\n        }, 1500);\n      }\n\n      /* prevent page reload */\n      return false;\n    });\n\n    /* handler for deleting endpoint group */\n    $('#deleteButton').click(function() {\n      deleteEndpointGroup();\n    });\n\n    /* validate fields before moving to next tab */\n    $('#endpoint-nav > .nav-tabs').click({tab_panes: $('div.tab-content > div.tab-pane')}, function(ev) {\n      var current_tab = ev.data.tab_panes.filter(':not(:hidden)');\n      if (!validateFields(current_tab)) {\n        return false;\n      }\n    });\n  });\n\n})(window, document);\n"
  },
  {
    "path": "gui/static/js/highlight/LICENSE",
    "content": "Copyright (c) 2006, Ivan Sagalaev\nAll rights reserved.\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright\n      notice, this list of conditions and the following disclaimer in the\n      documentation and/or other materials provided with the distribution.\n    * Neither the name of highlight.js nor the names of its contributors \n      may be used to endorse or promote products derived from this software \n      without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY\nEXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "gui/static/js/highlight/highlight.pack.js",
    "content": "/*! highlight.js v9.12.0 | BSD3 License | git.io/hljslicense */\n!function(e){var n=\"object\"==typeof window&&window||\"object\"==typeof self&&self;\"undefined\"!=typeof exports?e(exports):n&&(n.hljs=e({}),\"function\"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+\" \";if(o+=e.parentNode?e.parentNode.className:\"\",t=B.exec(o))return w(t[1])?t[1]:\"no-highlight\";for(o=o.split(/\\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||w(i))return i}function o(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:\"start\",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:\"stop\",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset<r[0].offset?e:r:\"start\"===r[0].event?e:r:e.length?e:r}function o(e){function r(e){return\" \"+e.nodeName+'=\"'+n(e.value).replace('\"',\"&quot;\")+'\"'}s+=\"<\"+t(e)+E.map.call(e.attributes,r).join(\"\")+\">\"}function u(e){s+=\"</\"+t(e)+\">\"}function c(e){(\"start\"===e.event?o:u)(e.node)}for(var l=0,s=\"\",f=[];e.length||r.length;){var g=i();if(s+=n(a.substring(l,g[0].offset)),l=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===l);f.reverse().forEach(o)}else\"start\"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return s+n(a.substr(l))}function l(e){return e.v&&!e.cached_variants&&(e.cached_variants=e.v.map(function(n){return o(e,{v:null},n)})),e.cached_variants||e.eW&&[o(e)]||[e]}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),\"m\"+(e.cI?\"i\":\"\")+(r?\"g\":\"\"))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},u=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(\" \").forEach(function(e){var t=e.split(\"|\");o[t[0]]=[n,t[1]?Number(t[1]):1]})};\"string\"==typeof a.k?u(\"keyword\",a.k):x(a.k).forEach(function(e){u(e,a.k[e])}),a.k=o}a.lR=t(a.l||/\\w+/,!0),i&&(a.bK&&(a.b=\"\\\\b(\"+a.bK.split(\" \").join(\"|\")+\")\\\\b\"),a.b||(a.b=/\\B|\\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\\B|\\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||\"\",a.eW&&i.tE&&(a.tE+=(a.e?\"|\":\"\")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]),a.c=Array.prototype.concat.apply([],a.c.map(function(e){return l(\"self\"===e?a:e)})),a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var c=a.c.map(function(e){return e.bK?\"\\\\.?(\"+e.b+\")\\\\.?\":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=c.length?t(c.join(\"|\"),!0):{exec:function(){return null}}}}r(e)}function f(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function l(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function p(e,n,t,r){var a=r?\"\":I.classPrefix,i='<span class=\"'+a,o=t?\"\":C;return i+=e+'\">',i+n+o}function h(){var e,t,r,a;if(!E.k)return n(k);for(a=\"\",t=0,E.lR.lastIndex=0,r=E.lR.exec(k);r;)a+=n(k.substring(t,r.index)),e=l(E,r),e?(B+=e[1],a+=p(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(k);return a+n(k.substr(t))}function d(){var e=\"string\"==typeof E.sL;if(e&&!y[E.sL])return n(k);var t=e?f(E.sL,k,!0,x[E.sL]):g(k,E.sL.length?E.sL:void 0);return E.r>0&&(B+=t.r),e&&(x[E.sL]=t.top),p(t.language,t.value,!1,!0)}function b(){L+=null!=E.sL?d():h(),k=\"\"}function v(e){L+=e.cN?p(e.cN,\"\",!0):\"\",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(k+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?k+=n:(t.eB&&(k+=n),b(),t.rB||t.eB||(k=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?k+=n:(a.rE||a.eE||(k+=n),b(),a.eE&&(k=n));do E.cN&&(L+=C),E.skip||(B+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,\"\"),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme \"'+n+'\" for mode \"'+(E.cN||\"<unnamed>\")+'\"');return k+=n,n.length||1}var N=w(e);if(!N)throw new Error('Unknown language: \"'+e+'\"');s(N);var R,E=i||N,x={},L=\"\";for(R=E;R!==N;R=R.parent)R.cN&&(L=p(R.cN,\"\",!0)+L);var k=\"\",B=0;try{for(var M,j,O=0;;){if(E.t.lastIndex=O,M=E.t.exec(t),!M)break;j=m(t.substring(O,M.index),M[0]),O=M.index+j}for(m(t.substr(O)),R=E;R.parent;R=R.parent)R.cN&&(L+=C);return{r:B,value:L,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf(\"Illegal\"))return{r:0,value:n(t)};throw T}}function g(e,t){t=t||I.languages||x(y);var r={r:0,value:n(e)},a=r;return t.filter(w).forEach(function(n){var t=f(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function p(e){return I.tabReplace||I.useBR?e.replace(M,function(e,n){return I.useBR&&\"\\n\"===e?\"<br>\":I.tabReplace?n.replace(/\\t/g,I.tabReplace):\"\"}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\\bhljs\\b/)||a.push(\"hljs\"),-1===e.indexOf(r)&&a.push(r),a.join(\" \").trim()}function d(e){var n,t,r,o,l,s=i(e);a(s)||(I.useBR?(n=document.createElementNS(\"http://www.w3.org/1999/xhtml\",\"div\"),n.innerHTML=e.innerHTML.replace(/\\n/g,\"\").replace(/<br[ \\/]*>/g,\"\\n\")):n=e,l=n.textContent,r=s?f(s,l,!0):g(l),t=u(n),t.length&&(o=document.createElementNS(\"http://www.w3.org/1999/xhtml\",\"div\"),o.innerHTML=r.value,r.value=c(t,u(o),l)),r.value=p(r.value),e.innerHTML=r.value,e.className=h(e.className,s,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function b(e){I=o(I,e)}function v(){if(!v.called){v.called=!0;var e=document.querySelectorAll(\"pre code\");E.forEach.call(e,d)}}function m(){addEventListener(\"DOMContentLoaded\",v,!1),addEventListener(\"load\",v,!1)}function N(n,t){var r=y[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function R(){return x(y)}function w(e){return e=(e||\"\").toLowerCase(),y[e]||y[L[e]]}var E=[],x=Object.keys,y={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\\blang(?:uage)?-([\\w-]+)\\b/i,M=/((^(<[^>]+>|\\t|)+|(?:\\n)))/gm,C=\"</span>\",I={classPrefix:\"hljs-\",tabReplace:null,useBR:!1,languages:void 0};return e.highlight=f,e.highlightAuto=g,e.fixMarkup=p,e.highlightBlock=d,e.configure=b,e.initHighlighting=v,e.initHighlightingOnLoad=m,e.registerLanguage=N,e.listLanguages=R,e.getLanguage=w,e.inherit=o,e.IR=\"[a-zA-Z]\\\\w*\",e.UIR=\"[a-zA-Z_]\\\\w*\",e.NR=\"\\\\b\\\\d+(\\\\.\\\\d+)?\",e.CNR=\"(-?)(\\\\b0[xX][a-fA-F0-9]+|(\\\\b\\\\d+(\\\\.\\\\d*)?|\\\\.\\\\d+)([eE][-+]?\\\\d+)?)\",e.BNR=\"\\\\b(0b[01]+)\",e.RSR=\"!|!=|!==|%|%=|&|&&|&=|\\\\*|\\\\*=|\\\\+|\\\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\\\?|\\\\[|\\\\{|\\\\(|\\\\^|\\\\^=|\\\\||\\\\|=|\\\\|\\\\||~\",e.BE={b:\"\\\\\\\\[\\\\s\\\\S]\",r:0},e.ASM={cN:\"string\",b:\"'\",e:\"'\",i:\"\\\\n\",c:[e.BE]},e.QSM={cN:\"string\",b:'\"',e:'\"',i:\"\\\\n\",c:[e.BE]},e.PWM={b:/\\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\\b/},e.C=function(n,t,r){var a=e.inherit({cN:\"comment\",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:\"doctag\",b:\"(?:TODO|FIXME|NOTE|BUG|XXX):\",r:0}),a},e.CLCM=e.C(\"//\",\"$\"),e.CBCM=e.C(\"/\\\\*\",\"\\\\*/\"),e.HCM=e.C(\"#\",\"$\"),e.NM={cN:\"number\",b:e.NR,r:0},e.CNM={cN:\"number\",b:e.CNR,r:0},e.BNM={cN:\"number\",b:e.BNR,r:0},e.CSSNM={cN:\"number\",b:e.NR+\"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?\",r:0},e.RM={cN:\"regexp\",b:/\\//,e:/\\/[gimuy]*/,i:/\\n/,c:[e.BE,{b:/\\[/,e:/\\]/,r:0,c:[e.BE]}]},e.TM={cN:\"title\",b:e.IR,r:0},e.UTM={cN:\"title\",b:e.UIR,r:0},e.METHOD_GUARD={b:\"\\\\.\\\\s*\"+e.UIR,r:0},e});hljs.registerLanguage(\"xml\",function(s){var e=\"[A-Za-z0-9\\\\._:-]+\",t={eW:!0,i:/</,r:0,c:[{cN:\"attr\",b:e,r:0},{b:/=\\s*/,r:0,c:[{cN:\"string\",endsParent:!0,v:[{b:/\"/,e:/\"/},{b:/'/,e:/'/},{b:/[^\\s\"'=<>`]+/}]}]}]};return{aliases:[\"html\",\"xhtml\",\"rss\",\"atom\",\"xjb\",\"xsd\",\"xsl\",\"plist\"],cI:!0,c:[{cN:\"meta\",b:\"<!DOCTYPE\",e:\">\",r:10,c:[{b:\"\\\\[\",e:\"\\\\]\"}]},s.C(\"<!--\",\"-->\",{r:10}),{b:\"<\\\\!\\\\[CDATA\\\\[\",e:\"\\\\]\\\\]>\",r:10},{b:/<\\?(php)?/,e:/\\?>/,sL:\"php\",c:[{b:\"/\\\\*\",e:\"\\\\*/\",skip:!0}]},{cN:\"tag\",b:\"<style(?=\\\\s|>|$)\",e:\">\",k:{name:\"style\"},c:[t],starts:{e:\"</style>\",rE:!0,sL:[\"css\",\"xml\"]}},{cN:\"tag\",b:\"<script(?=\\\\s|>|$)\",e:\">\",k:{name:\"script\"},c:[t],starts:{e:\"</script>\",rE:!0,sL:[\"actionscript\",\"javascript\",\"handlebars\",\"xml\"]}},{cN:\"meta\",v:[{b:/<\\?xml/,e:/\\?>/,r:10},{b:/<\\?\\w+/,e:/\\?>/}]},{cN:\"tag\",b:\"</?\",e:\"/?>\",c:[{cN:\"name\",b:/[^\\/><\\s]+/,r:0},t]}]}});hljs.registerLanguage(\"markdown\",function(e){return{aliases:[\"md\",\"mkdown\",\"mkd\"],c:[{cN:\"section\",v:[{b:\"^#{1,6}\",e:\"$\"},{b:\"^.+?\\\\n[=-]{2,}$\"}]},{b:\"<\",e:\">\",sL:\"xml\",r:0},{cN:\"bullet\",b:\"^([*+-]|(\\\\d+\\\\.))\\\\s+\"},{cN:\"strong\",b:\"[*_]{2}.+?[*_]{2}\"},{cN:\"emphasis\",v:[{b:\"\\\\*.+?\\\\*\"},{b:\"_.+?_\",r:0}]},{cN:\"quote\",b:\"^>\\\\s+\",e:\"$\"},{cN:\"code\",v:[{b:\"^```w*s*$\",e:\"^```s*$\"},{b:\"`.+?`\"},{b:\"^( {4}|\t)\",e:\"$\",r:0}]},{b:\"^[-\\\\*]{3,}\",e:\"$\"},{b:\"\\\\[.+?\\\\][\\\\(\\\\[].*?[\\\\)\\\\]]\",rB:!0,c:[{cN:\"string\",b:\"\\\\[\",e:\"\\\\]\",eB:!0,rE:!0,r:0},{cN:\"link\",b:\"\\\\]\\\\(\",e:\"\\\\)\",eB:!0,eE:!0},{cN:\"symbol\",b:\"\\\\]\\\\[\",e:\"\\\\]\",eB:!0,eE:!0}],r:10},{b:/^\\[[^\\n]+\\]:/,rB:!0,c:[{cN:\"symbol\",b:/\\[/,e:/\\]/,eB:!0,eE:!0},{cN:\"link\",b:/:\\s*/,e:/$/,eB:!0}]}]}});hljs.registerLanguage(\"javascript\",function(e){var r=\"[A-Za-z$_][0-9A-Za-z$_]*\",t={keyword:\"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as\",literal:\"true false null undefined NaN Infinity\",built_in:\"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise\"},a={cN:\"number\",v:[{b:\"\\\\b(0[bB][01]+)\"},{b:\"\\\\b(0[oO][0-7]+)\"},{b:e.CNR}],r:0},n={cN:\"subst\",b:\"\\\\$\\\\{\",e:\"\\\\}\",k:t,c:[]},c={cN:\"string\",b:\"`\",e:\"`\",c:[e.BE,n]};n.c=[e.ASM,e.QSM,c,a,e.RM];var s=n.c.concat([e.CBCM,e.CLCM]);return{aliases:[\"js\",\"jsx\"],k:t,c:[{cN:\"meta\",r:10,b:/^\\s*['\"]use (strict|asm)['\"]/},{cN:\"meta\",b:/^#!/,e:/$/},e.ASM,e.QSM,c,e.CLCM,e.CBCM,a,{b:/[{,]\\s*/,r:0,c:[{b:r+\"\\\\s*:\",rB:!0,r:0,c:[{cN:\"attr\",b:r,r:0}]}]},{b:\"(\"+e.RSR+\"|\\\\b(case|return|throw)\\\\b)\\\\s*\",k:\"return throw case\",c:[e.CLCM,e.CBCM,e.RM,{cN:\"function\",b:\"(\\\\(.*?\\\\)|\"+r+\")\\\\s*=>\",rB:!0,e:\"\\\\s*=>\",c:[{cN:\"params\",v:[{b:r},{b:/\\(\\s*\\)/},{b:/\\(/,e:/\\)/,eB:!0,eE:!0,k:t,c:s}]}]},{b:/</,e:/(\\/\\w+|\\w+\\/)>/,sL:\"xml\",c:[{b:/<\\w+\\s*\\/>/,skip:!0},{b:/<\\w+/,e:/(\\/\\w+|\\w+\\/)>/,skip:!0,c:[{b:/<\\w+\\s*\\/>/,skip:!0},\"self\"]}]}],r:0},{cN:\"function\",bK:\"function\",e:/\\{/,eE:!0,c:[e.inherit(e.TM,{b:r}),{cN:\"params\",b:/\\(/,e:/\\)/,eB:!0,eE:!0,c:s}],i:/\\[|%/},{b:/\\$[(.]/},e.METHOD_GUARD,{cN:\"class\",bK:\"class\",e:/[{;=]/,eE:!0,i:/[:\"\\[\\]]/,c:[{bK:\"extends\"},e.UTM]},{bK:\"constructor\",e:/\\{/,eE:!0}],i:/#(?!!)/}});hljs.registerLanguage(\"go\",function(e){var t={keyword:\"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune\",literal:\"true false iota nil\",built_in:\"append cap close complex copy imag len make new panic print println real recover delete\"};return{aliases:[\"golang\"],k:t,i:\"</\",c:[e.CLCM,e.CBCM,{cN:\"string\",v:[e.QSM,{b:\"'\",e:\"[^\\\\\\\\]'\"},{b:\"`\",e:\"`\"}]},{cN:\"number\",v:[{b:e.CNR+\"[dflsi]\",r:1},e.CNM]},{b:/:=/},{cN:\"function\",bK:\"func\",e:/\\s*\\{/,eE:!0,c:[e.TM,{cN:\"params\",b:/\\(/,e:/\\)/,k:t,i:/[\"']/}]}]}});hljs.registerLanguage(\"lua\",function(e){var t=\"\\\\[=*\\\\[\",a=\"\\\\]=*\\\\]\",r={b:t,e:a,c:[\"self\"]},n=[e.C(\"--(?!\"+t+\")\",\"$\"),e.C(\"--\"+t,a,{c:[r],r:10})];return{l:e.UIR,k:{literal:\"true false nil\",keyword:\"and break do else elseif end for goto if in local not or repeat return then until while\",built_in:\"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstringmodule next pairs pcall print rawequal rawget rawset require select setfenvsetmetatable tonumber tostring type unpack xpcall arg selfcoroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove\"},c:n.concat([{cN:\"function\",bK:\"function\",e:\"\\\\)\",c:[e.inherit(e.TM,{b:\"([_a-zA-Z]\\\\w*\\\\.)*([_a-zA-Z]\\\\w*:)?[_a-zA-Z]\\\\w*\"}),{cN:\"params\",b:\"\\\\(\",eW:!0,c:n}].concat(n)},e.CNM,e.ASM,e.QSM,{cN:\"string\",b:t,e:a,c:[r],r:5}])}});hljs.registerLanguage(\"less\",function(e){var r=\"[\\\\w-]+\",t=\"(\"+r+\"|@{\"+r+\"})\",a=[],c=[],s=function(e){return{cN:\"string\",b:\"~?\"+e+\".*?\"+e}},b=function(e,r,t){return{cN:e,b:r,r:t}},n={b:\"\\\\(\",e:\"\\\\)\",c:c,r:0};c.push(e.CLCM,e.CBCM,s(\"'\"),s('\"'),e.CSSNM,{b:\"(url|data-uri)\\\\(\",starts:{cN:\"string\",e:\"[\\\\)\\\\n]\",eE:!0}},b(\"number\",\"#[0-9A-Fa-f]+\\\\b\"),n,b(\"variable\",\"@@?\"+r,10),b(\"variable\",\"@{\"+r+\"}\"),b(\"built_in\",\"~?`[^`]*?`\"),{cN:\"attribute\",b:r+\"\\\\s*:\",e:\":\",rB:!0,eE:!0},{cN:\"meta\",b:\"!important\"});var i=c.concat({b:\"{\",e:\"}\",c:a}),o={bK:\"when\",eW:!0,c:[{bK:\"and not\"}].concat(c)},u={b:t+\"\\\\s*:\",rB:!0,e:\"[;}]\",r:0,c:[{cN:\"attribute\",b:t,e:\":\",eE:!0,starts:{eW:!0,i:\"[<=$]\",r:0,c:c}}]},l={cN:\"keyword\",b:\"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\\\b\",starts:{e:\"[;{}]\",rE:!0,c:c,r:0}},C={cN:\"variable\",v:[{b:\"@\"+r+\"\\\\s*:\",r:15},{b:\"@\"+r}],starts:{e:\"[;}]\",rE:!0,c:i}},p={v:[{b:\"[\\\\.#:&\\\\[>]\",e:\"[;{}]\"},{b:t,e:\"{\"}],rB:!0,rE:!0,i:\"[<='$\\\"]\",r:0,c:[e.CLCM,e.CBCM,o,b(\"keyword\",\"all\\\\b\"),b(\"variable\",\"@{\"+r+\"}\"),b(\"selector-tag\",t+\"%?\",0),b(\"selector-id\",\"#\"+t),b(\"selector-class\",\"\\\\.\"+t,0),b(\"selector-tag\",\"&\",0),{cN:\"selector-attr\",b:\"\\\\[\",e:\"\\\\]\"},{cN:\"selector-pseudo\",b:/:(:)?[a-zA-Z0-9\\_\\-\\+\\(\\)\"'.]+/},{b:\"\\\\(\",e:\"\\\\)\",c:i},{b:\"!important\"}]};return a.push(e.CLCM,e.CBCM,l,C,u,p),{cI:!0,i:\"[=>'/<($\\\"]\",c:a}});hljs.registerLanguage(\"cmake\",function(e){return{aliases:[\"cmake.in\"],cI:!0,k:{keyword:\"add_custom_command add_custom_target add_definitions add_dependencies add_executable add_library add_subdirectory add_test aux_source_directory break build_command cmake_minimum_required cmake_policy configure_file create_test_sourcelist define_property else elseif enable_language enable_testing endforeach endfunction endif endmacro endwhile execute_process export find_file find_library find_package find_path find_program fltk_wrap_ui foreach function get_cmake_property get_directory_property get_filename_component get_property get_source_file_property get_target_property get_test_property if include include_directories include_external_msproject include_regular_expression install link_directories load_cache load_command macro mark_as_advanced message option output_required_files project qt_wrap_cpp qt_wrap_ui remove_definitions return separate_arguments set set_directory_properties set_property set_source_files_properties set_target_properties set_tests_properties site_name source_group string target_link_libraries try_compile try_run unset variable_watch while build_name exec_program export_library_dependencies install_files install_programs install_targets link_libraries make_directory remove subdir_depends subdirs use_mangled_mesa utility_source variable_requires write_file qt5_use_modules qt5_use_package qt5_wrap_cpp on off true false and or equal less greater strless strgreater strequal matches\"},c:[{cN:\"variable\",b:\"\\\\${\",e:\"}\"},e.HCM,e.QSM,e.NM]}});hljs.registerLanguage(\"diff\",function(e){return{aliases:[\"patch\"],c:[{cN:\"meta\",r:10,v:[{b:/^@@ +\\-\\d+,\\d+ +\\+\\d+,\\d+ +@@$/},{b:/^\\*\\*\\* +\\d+,\\d+ +\\*\\*\\*\\*$/},{b:/^\\-\\-\\- +\\d+,\\d+ +\\-\\-\\-\\-$/}]},{cN:\"comment\",v:[{b:/Index: /,e:/$/},{b:/={3,}/,e:/$/},{b:/^\\-{3}/,e:/$/},{b:/^\\*{3} /,e:/$/},{b:/^\\+{3}/,e:/$/},{b:/\\*{5}/,e:/\\*{5}$/}]},{cN:\"addition\",b:\"^\\\\+\",e:\"$\"},{cN:\"deletion\",b:\"^\\\\-\",e:\"$\"},{cN:\"addition\",b:\"^\\\\!\",e:\"$\"}]}});hljs.registerLanguage(\"nginx\",function(e){var r={cN:\"variable\",v:[{b:/\\$\\d+/},{b:/\\$\\{/,e:/}/},{b:\"[\\\\$\\\\@]\"+e.UIR}]},b={eW:!0,l:\"[a-z/_]+\",k:{literal:\"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll\"},r:0,i:\"=>\",c:[e.HCM,{cN:\"string\",c:[e.BE,r],v:[{b:/\"/,e:/\"/},{b:/'/,e:/'/}]},{b:\"([a-z]+):/\",e:\"\\\\s\",eW:!0,eE:!0,c:[r]},{cN:\"regexp\",c:[e.BE,r],v:[{b:\"\\\\s\\\\^\",e:\"\\\\s|{|;\",rE:!0},{b:\"~\\\\*?\\\\s+\",e:\"\\\\s|{|;\",rE:!0},{b:\"\\\\*(\\\\.[a-z\\\\-]+)+\"},{b:\"([a-z\\\\-]+\\\\.)+\\\\*\"}]},{cN:\"number\",b:\"\\\\b\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}(:\\\\d{1,5})?\\\\b\"},{cN:\"number\",b:\"\\\\b\\\\d+[kKmMgGdshdwy]*\\\\b\",r:0},r]};return{aliases:[\"nginxconf\"],c:[e.HCM,{b:e.UIR+\"\\\\s+{\",rB:!0,e:\"{\",c:[{cN:\"section\",b:e.UIR}],r:0},{b:e.UIR+\"\\\\s\",e:\";|{\",rB:!0,c:[{cN:\"attribute\",b:e.UIR,starts:b}],r:0}],i:\"[^\\\\s\\\\}]\"}});hljs.registerLanguage(\"bash\",function(e){var t={cN:\"variable\",v:[{b:/\\$[\\w\\d#@][\\w\\d_]*/},{b:/\\$\\{(.*?)}/}]},s={cN:\"string\",b:/\"/,e:/\"/,c:[e.BE,t,{cN:\"variable\",b:/\\$\\(/,e:/\\)/,c:[e.BE]}]},a={cN:\"string\",b:/'/,e:/'/};return{aliases:[\"sh\",\"zsh\"],l:/\\b-?[a-z\\._]+\\b/,k:{keyword:\"if then else elif fi for while in do done case esac function\",literal:\"true false\",built_in:\"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp\",_:\"-ne -eq -lt -gt -f -d -e -s -l -a\"},c:[{cN:\"meta\",b:/^#![^\\n]+sh\\s*$/,r:10},{cN:\"function\",b:/\\w[\\w\\d_]*\\s*\\(\\s*\\)\\s*\\{/,rB:!0,c:[e.inherit(e.TM,{b:/\\w[\\w\\d_]*/})],r:0},e.HCM,s,a,t]}});hljs.registerLanguage(\"java\",function(e){var a=\"[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*\",t=a+\"(<\"+a+\"(\\\\s*,\\\\s*\"+a+\")*>)?\",r=\"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do\",s=\"\\\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\\\d]+[\\\\d_]+[\\\\d]+|[\\\\d]+)(\\\\.([\\\\d]+[\\\\d_]+[\\\\d]+|[\\\\d]+))?|\\\\.([\\\\d]+[\\\\d_]+[\\\\d]+|[\\\\d]+))([eE][-+]?\\\\d+)?)[lLfF]?\",c={cN:\"number\",b:s,r:0};return{aliases:[\"jsp\"],k:r,i:/<\\/|#/,c:[e.C(\"/\\\\*\\\\*\",\"\\\\*/\",{r:0,c:[{b:/\\w+@/,r:0},{cN:\"doctag\",b:\"@[A-Za-z]+\"}]}),e.CLCM,e.CBCM,e.ASM,e.QSM,{cN:\"class\",bK:\"class interface\",e:/[{;=]/,eE:!0,k:\"class interface\",i:/[:\"\\[\\]]/,c:[{bK:\"extends implements\"},e.UTM]},{bK:\"new throw return else\",r:0},{cN:\"function\",b:\"(\"+t+\"\\\\s+)+\"+e.UIR+\"\\\\s*\\\\(\",rB:!0,e:/[{;=]/,eE:!0,k:r,c:[{b:e.UIR+\"\\\\s*\\\\(\",rB:!0,r:0,c:[e.UTM]},{cN:\"params\",b:/\\(/,e:/\\)/,k:r,r:0,c:[e.ASM,e.QSM,e.CNM,e.CBCM]},e.CLCM,e.CBCM]},c,{cN:\"meta\",b:\"@[A-Za-z]+\"}]}});hljs.registerLanguage(\"perl\",function(e){var t=\"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when\",r={cN:\"subst\",b:\"[$@]\\\\{\",e:\"\\\\}\",k:t},s={b:\"->{\",e:\"}\"},n={v:[{b:/\\$\\d/},{b:/[\\$%@](\\^\\w\\b|#\\w+(::\\w+)*|{\\w+}|\\w+(::\\w*)*)/},{b:/[\\$%@][^\\s\\w{]/,r:0}]},i=[e.BE,r,n],o=[n,e.HCM,e.C(\"^\\\\=\\\\w\",\"\\\\=cut\",{eW:!0}),s,{cN:\"string\",c:i,v:[{b:\"q[qwxr]?\\\\s*\\\\(\",e:\"\\\\)\",r:5},{b:\"q[qwxr]?\\\\s*\\\\[\",e:\"\\\\]\",r:5},{b:\"q[qwxr]?\\\\s*\\\\{\",e:\"\\\\}\",r:5},{b:\"q[qwxr]?\\\\s*\\\\|\",e:\"\\\\|\",r:5},{b:\"q[qwxr]?\\\\s*\\\\<\",e:\"\\\\>\",r:5},{b:\"qw\\\\s+q\",e:\"q\",r:5},{b:\"'\",e:\"'\",c:[e.BE]},{b:'\"',e:'\"'},{b:\"`\",e:\"`\",c:[e.BE]},{b:\"{\\\\w+}\",c:[],r:0},{b:\"-?\\\\w+\\\\s*\\\\=\\\\>\",c:[],r:0}]},{cN:\"number\",b:\"(\\\\b0[0-7_]+)|(\\\\b0x[0-9a-fA-F_]+)|(\\\\b[1-9][0-9_]*(\\\\.[0-9_]+)?)|[0_]\\\\b\",r:0},{b:\"(\\\\/\\\\/|\"+e.RSR+\"|\\\\b(split|return|print|reverse|grep)\\\\b)\\\\s*\",k:\"split return print reverse grep\",r:0,c:[e.HCM,{cN:\"regexp\",b:\"(s|tr|y)/(\\\\\\\\.|[^/])*/(\\\\\\\\.|[^/])*/[a-z]*\",r:10},{cN:\"regexp\",b:\"(m|qr)?/\",e:\"/[a-z]*\",c:[e.BE],r:0}]},{cN:\"function\",bK:\"sub\",e:\"(\\\\s*\\\\(.*?\\\\))?[;{]\",eE:!0,r:5,c:[e.TM]},{b:\"-\\\\w\\\\b\",r:0},{b:\"^__DATA__$\",e:\"^__END__$\",sL:\"mojolicious\",c:[{b:\"^@@.*\",e:\"$\",cN:\"comment\"}]}];return r.c=o,s.c=o,{aliases:[\"pl\",\"pm\"],l:/[\\w\\.]+/,k:t,c:o}});hljs.registerLanguage(\"coffeescript\",function(e){var c={keyword:\"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super yield import export from as default await then unless until loop of by when and or is isnt not\",literal:\"true false null undefined yes no on off\",built_in:\"npm require console print module global window document\"},n=\"[A-Za-z$_][0-9A-Za-z$_]*\",r={cN:\"subst\",b:/#\\{/,e:/}/,k:c},i=[e.BNM,e.inherit(e.CNM,{starts:{e:\"(\\\\s*/)?\",r:0}}),{cN:\"string\",v:[{b:/'''/,e:/'''/,c:[e.BE]},{b:/'/,e:/'/,c:[e.BE]},{b:/\"\"\"/,e:/\"\"\"/,c:[e.BE,r]},{b:/\"/,e:/\"/,c:[e.BE,r]}]},{cN:\"regexp\",v:[{b:\"///\",e:\"///\",c:[r,e.HCM]},{b:\"//[gim]*\",r:0},{b:/\\/(?![ *])(\\\\\\/|.)*?\\/[gim]*(?=\\W|$)/}]},{b:\"@\"+n},{sL:\"javascript\",eB:!0,eE:!0,v:[{b:\"```\",e:\"```\"},{b:\"`\",e:\"`\"}]}];r.c=i;var s=e.inherit(e.TM,{b:n}),t=\"(\\\\(.*\\\\))?\\\\s*\\\\B[-=]>\",o={cN:\"params\",b:\"\\\\([^\\\\(]\",rB:!0,c:[{b:/\\(/,e:/\\)/,k:c,c:[\"self\"].concat(i)}]};return{aliases:[\"coffee\",\"cson\",\"iced\"],k:c,i:/\\/\\*/,c:i.concat([e.C(\"###\",\"###\"),e.HCM,{cN:\"function\",b:\"^\\\\s*\"+n+\"\\\\s*=\\\\s*\"+t,e:\"[-=]>\",rB:!0,c:[s,o]},{b:/[:\\(,=]\\s*/,r:0,c:[{cN:\"function\",b:t,e:\"[-=]>\",rB:!0,c:[o]}]},{cN:\"class\",bK:\"class\",e:\"$\",i:/[:=\"\\[\\]]/,c:[{bK:\"extends\",eW:!0,i:/[:=\"\\[\\]]/,c:[s]},s]},{b:n+\":\",e:\":\",rB:!0,rE:!0,r:0}])}});hljs.registerLanguage(\"accesslog\",function(T){return{c:[{cN:\"number\",b:\"\\\\b\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}(:\\\\d{1,5})?\\\\b\"},{cN:\"number\",b:\"\\\\b\\\\d+\\\\b\",r:0},{cN:\"string\",b:'\"(GET|POST|HEAD|PUT|DELETE|CONNECT|OPTIONS|PATCH|TRACE)',e:'\"',k:\"GET POST HEAD PUT DELETE CONNECT OPTIONS PATCH TRACE\",i:\"\\\\n\",r:10},{cN:\"string\",b:/\\[/,e:/\\]/,i:\"\\\\n\"},{cN:\"string\",b:'\"',e:'\"',i:\"\\\\n\"}]}});hljs.registerLanguage(\"ruby\",function(e){var b=\"[a-zA-Z_]\\\\w*[!?=]?|[-+~]\\\\@|<<|>>|=~|===?|<=>|[<>]=?|\\\\*\\\\*|[-/+%^&*~`|]|\\\\[\\\\]=?\",r={keyword:\"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor\",literal:\"true false nil\"},c={cN:\"doctag\",b:\"@[A-Za-z]+\"},a={b:\"#<\",e:\">\"},s=[e.C(\"#\",\"$\",{c:[c]}),e.C(\"^\\\\=begin\",\"^\\\\=end\",{c:[c],r:10}),e.C(\"^__END__\",\"\\\\n$\")],n={cN:\"subst\",b:\"#\\\\{\",e:\"}\",k:r},t={cN:\"string\",c:[e.BE,n],v:[{b:/'/,e:/'/},{b:/\"/,e:/\"/},{b:/`/,e:/`/},{b:\"%[qQwWx]?\\\\(\",e:\"\\\\)\"},{b:\"%[qQwWx]?\\\\[\",e:\"\\\\]\"},{b:\"%[qQwWx]?{\",e:\"}\"},{b:\"%[qQwWx]?<\",e:\">\"},{b:\"%[qQwWx]?/\",e:\"/\"},{b:\"%[qQwWx]?%\",e:\"%\"},{b:\"%[qQwWx]?-\",e:\"-\"},{b:\"%[qQwWx]?\\\\|\",e:\"\\\\|\"},{b:/\\B\\?(\\\\\\d{1,3}|\\\\x[A-Fa-f0-9]{1,2}|\\\\u[A-Fa-f0-9]{4}|\\\\?\\S)\\b/},{b:/<<(-?)\\w+$/,e:/^\\s*\\w+$/}]},i={cN:\"params\",b:\"\\\\(\",e:\"\\\\)\",endsParent:!0,k:r},d=[t,a,{cN:\"class\",bK:\"class module\",e:\"$|;\",i:/=/,c:[e.inherit(e.TM,{b:\"[A-Za-z_]\\\\w*(::\\\\w+)*(\\\\?|\\\\!)?\"}),{b:\"<\\\\s*\",c:[{b:\"(\"+e.IR+\"::)?\"+e.IR}]}].concat(s)},{cN:\"function\",bK:\"def\",e:\"$|;\",c:[e.inherit(e.TM,{b:b}),i].concat(s)},{b:e.IR+\"::\"},{cN:\"symbol\",b:e.UIR+\"(\\\\!|\\\\?)?:\",r:0},{cN:\"symbol\",b:\":(?!\\\\s)\",c:[t,{b:b}],r:0},{cN:\"number\",b:\"(\\\\b0[0-7_]+)|(\\\\b0x[0-9a-fA-F_]+)|(\\\\b[1-9][0-9_]*(\\\\.[0-9_]+)?)|[0_]\\\\b\",r:0},{b:\"(\\\\$\\\\W)|((\\\\$|\\\\@\\\\@?)(\\\\w+))\"},{cN:\"params\",b:/\\|/,e:/\\|/,k:r},{b:\"(\"+e.RSR+\"|unless)\\\\s*\",k:\"unless\",c:[a,{cN:\"regexp\",c:[e.BE,n],i:/\\n/,v:[{b:\"/\",e:\"/[a-z]*\"},{b:\"%r{\",e:\"}[a-z]*\"},{b:\"%r\\\\(\",e:\"\\\\)[a-z]*\"},{b:\"%r!\",e:\"![a-z]*\"},{b:\"%r\\\\[\",e:\"\\\\][a-z]*\"}]}].concat(s),r:0}].concat(s);n.c=d,i.c=d;var l=\"[>?]>\",o=\"[\\\\w#]+\\\\(\\\\w+\\\\):\\\\d+:\\\\d+>\",u=\"(\\\\w+-)?\\\\d+\\\\.\\\\d+\\\\.\\\\d(p\\\\d+)?[^>]+>\",w=[{b:/^\\s*=>/,starts:{e:\"$\",c:d}},{cN:\"meta\",b:\"^(\"+l+\"|\"+o+\"|\"+u+\")\",starts:{e:\"$\",c:d}}];return{aliases:[\"rb\",\"gemspec\",\"podspec\",\"thor\",\"irb\"],k:r,i:/\\/\\*/,c:s.concat(w).concat(d)}});hljs.registerLanguage(\"css\",function(e){var c=\"[a-zA-Z-][a-zA-Z0-9_-]*\",t={b:/[A-Z\\_\\.\\-]+\\s*:/,rB:!0,e:\";\",eW:!0,c:[{cN:\"attribute\",b:/\\S/,e:\":\",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\\w-]+\\(/,rB:!0,c:[{cN:\"built_in\",b:/[\\w-]+/},{b:/\\(/,e:/\\)/,c:[e.ASM,e.QSM]}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:\"number\",b:\"#[0-9A-Fa-f]+\"},{cN:\"meta\",b:\"!important\"}]}}]};return{cI:!0,i:/[=\\/|'\\$]/,c:[e.CBCM,{cN:\"selector-id\",b:/#[A-Za-z0-9_-]+/},{cN:\"selector-class\",b:/\\.[A-Za-z0-9_-]+/},{cN:\"selector-attr\",b:/\\[/,e:/\\]/,i:\"$\"},{cN:\"selector-pseudo\",b:/:(:)?[a-zA-Z0-9\\_\\-\\+\\(\\)\"'.]+/},{b:\"@(font-face|page)\",l:\"[a-z-]+\",k:\"font-face page\"},{b:\"@\",e:\"[{;]\",i:/:/,c:[{cN:\"keyword\",b:/\\w+/},{b:/\\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:\"selector-tag\",b:c,r:0},{b:\"{\",e:\"}\",i:/\\S/,c:[e.CBCM,t]}]}});hljs.registerLanguage(\"cpp\",function(t){var e={cN:\"keyword\",b:\"\\\\b[a-z\\\\d_]*_t\\\\b\"},r={cN:\"string\",v:[{b:'(u8?|U)?L?\"',e:'\"',i:\"\\\\n\",c:[t.BE]},{b:'(u8?|U)?R\"',e:'\"',c:[t.BE]},{b:\"'\\\\\\\\?.\",e:\"'\",i:\".\"}]},s={cN:\"number\",v:[{b:\"\\\\b(0b[01']+)\"},{b:\"(-?)\\\\b([\\\\d']+(\\\\.[\\\\d']*)?|\\\\.[\\\\d']+)(u|U|l|L|ul|UL|f|F|b|B)\"},{b:\"(-?)(\\\\b0[xX][a-fA-F0-9']+|(\\\\b[\\\\d']+(\\\\.[\\\\d']*)?|\\\\.[\\\\d']+)([eE][-+]?[\\\\d']+)?)\"}],r:0},i={cN:\"meta\",b:/#\\s*[a-z]+\\b/,e:/$/,k:{\"meta-keyword\":\"if else elif endif define undef warning error line pragma ifdef ifndef include\"},c:[{b:/\\\\\\n/,r:0},t.inherit(r,{cN:\"meta-string\"}),{cN:\"meta-string\",b:/<[^\\n>]*>/,e:/$/,i:\"\\\\n\"},t.CLCM,t.CBCM]},a=t.IR+\"\\\\s*\\\\(\",c={keyword:\"int float while private char catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and or not\",built_in:\"std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr\",literal:\"true false nullptr NULL\"},n=[e,t.CLCM,t.CBCM,s,r];return{aliases:[\"c\",\"cc\",\"h\",\"c++\",\"h++\",\"hpp\"],k:c,i:\"</\",c:n.concat([i,{b:\"\\\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\\\s*<\",e:\">\",k:c,c:[\"self\",e]},{b:t.IR+\"::\",k:c},{v:[{b:/=/,e:/;/},{b:/\\(/,e:/\\)/},{bK:\"new throw return else\",e:/;/}],k:c,c:n.concat([{b:/\\(/,e:/\\)/,k:c,c:n.concat([\"self\"]),r:0}]),r:0},{cN:\"function\",b:\"(\"+t.IR+\"[\\\\*&\\\\s]+)+\"+a,rB:!0,e:/[{;=]/,eE:!0,k:c,i:/[^\\w\\s\\*&]/,c:[{b:a,rB:!0,c:[t.TM],r:0},{cN:\"params\",b:/\\(/,e:/\\)/,k:c,r:0,c:[t.CLCM,t.CBCM,r,s,e]},t.CLCM,t.CBCM,i]},{cN:\"class\",bK:\"class struct\",e:/[{;:]/,c:[{b:/</,e:/>/,c:[\"self\"]},t.TM]}]),exports:{preprocessor:i,strings:r,k:c}}});hljs.registerLanguage(\"awk\",function(e){var r={cN:\"variable\",v:[{b:/\\$[\\w\\d#@][\\w\\d_]*/},{b:/\\$\\{(.*?)}/}]},b=\"BEGIN END if else while do for in break continue delete next nextfile function func exit|10\",n={cN:\"string\",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,r:10},{b:/(u|b)?r?\"\"\"/,e:/\"\"\"/,r:10},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)\"/,e:/\"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)\"/,e:/\"/},e.ASM,e.QSM]};return{k:{keyword:b},c:[r,n,e.RM,e.HCM,e.NM]}});hljs.registerLanguage(\"shell\",function(s){return{aliases:[\"console\"],c:[{cN:\"meta\",b:\"^\\\\s{0,3}[\\\\w\\\\d\\\\[\\\\]()@-]*[>%$#]\",starts:{e:\"$\",sL:\"bash\"}}]}});hljs.registerLanguage(\"objectivec\",function(e){var t={cN:\"built_in\",b:\"\\\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\\\w+\"},_={keyword:\"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN\",literal:\"false true FALSE TRUE nil YES NO NULL\",built_in:\"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once\"},i=/[a-zA-Z@][a-zA-Z0-9_]*/,n=\"@interface @class @protocol @implementation\";return{aliases:[\"mm\",\"objc\",\"obj-c\"],k:_,l:i,i:\"</\",c:[t,e.CLCM,e.CBCM,e.CNM,e.QSM,{cN:\"string\",v:[{b:'@\"',e:'\"',i:\"\\\\n\",c:[e.BE]},{b:\"'\",e:\"[^\\\\\\\\]'\",i:\"[^\\\\\\\\][^']\"}]},{cN:\"meta\",b:\"#\",e:\"$\",c:[{cN:\"meta-string\",v:[{b:'\"',e:'\"'},{b:\"<\",e:\">\"}]}]},{cN:\"class\",b:\"(\"+n.split(\" \").join(\"|\")+\")\\\\b\",e:\"({|$)\",eE:!0,k:n,l:i,c:[e.UTM]},{b:\"\\\\.\"+e.UIR,r:0}]}});hljs.registerLanguage(\"ini\",function(e){var b={cN:\"string\",c:[e.BE],v:[{b:\"'''\",e:\"'''\",r:10},{b:'\"\"\"',e:'\"\"\"',r:10},{b:'\"',e:'\"'},{b:\"'\",e:\"'\"}]};return{aliases:[\"toml\"],cI:!0,i:/\\S/,c:[e.C(\";\",\"$\"),e.HCM,{cN:\"section\",b:/^\\s*\\[+/,e:/\\]+/},{b:/^[a-z0-9\\[\\]_-]+\\s*=\\s*/,e:\"$\",rB:!0,c:[{cN:\"attr\",b:/[a-z0-9\\[\\]_-]+/},{b:/=/,eW:!0,r:0,c:[{cN:\"literal\",b:/\\bon|off|true|false|yes|no\\b/},{cN:\"variable\",v:[{b:/\\$[\\w\\d\"][\\w\\d_]*/},{b:/\\$\\{(.*?)}/}]},b,{cN:\"number\",b:/([\\+\\-]+)?[\\d]+_[\\d_]+/},e.NM]}]}]}});hljs.registerLanguage(\"makefile\",function(e){var i={cN:\"variable\",v:[{b:\"\\\\$\\\\(\"+e.UIR+\"\\\\)\",c:[e.BE]},{b:/\\$[@%<?\\^\\+\\*]/}]},r={cN:\"string\",b:/\"/,e:/\"/,c:[e.BE,i]},a={cN:\"variable\",b:/\\$\\([\\w-]+\\s/,e:/\\)/,k:{built_in:\"subst patsubst strip findstring filter filter-out sort word wordlist firstword lastword dir notdir suffix basename addsuffix addprefix join wildcard realpath abspath error warning shell origin flavor foreach if or and call eval file value\"},c:[i]},n={b:\"^\"+e.UIR+\"\\\\s*[:+?]?=\",i:\"\\\\n\",rB:!0,c:[{b:\"^\"+e.UIR,e:\"[:+?]?=\",eE:!0}]},t={cN:\"meta\",b:/^\\.PHONY:/,e:/$/,k:{\"meta-keyword\":\".PHONY\"},l:/[\\.\\w]+/},l={cN:\"section\",b:/^[^\\s]+:/,e:/$/,c:[i]};return{aliases:[\"mk\",\"mak\"],k:\"define endef undefine ifdef ifndef ifeq ifneq else endif include -include sinclude override export unexport private vpath\",l:/[\\w-]+/,c:[e.HCM,i,r,a,n,t,l]}});hljs.registerLanguage(\"python\",function(e){var r={keyword:\"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False\",built_in:\"Ellipsis NotImplemented\"},b={cN:\"meta\",b:/^(>>>|\\.\\.\\.) /},c={cN:\"subst\",b:/\\{/,e:/\\}/,k:r,i:/#/},a={cN:\"string\",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[b],r:10},{b:/(u|b)?r?\"\"\"/,e:/\"\"\"/,c:[b],r:10},{b:/(fr|rf|f)'''/,e:/'''/,c:[b,c]},{b:/(fr|rf|f)\"\"\"/,e:/\"\"\"/,c:[b,c]},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)\"/,e:/\"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)\"/,e:/\"/},{b:/(fr|rf|f)'/,e:/'/,c:[c]},{b:/(fr|rf|f)\"/,e:/\"/,c:[c]},e.ASM,e.QSM]},s={cN:\"number\",r:0,v:[{b:e.BNR+\"[lLjJ]?\"},{b:\"\\\\b(0o[0-7]+)[lLjJ]?\"},{b:e.CNR+\"[lLjJ]?\"}]},i={cN:\"params\",b:/\\(/,e:/\\)/,c:[\"self\",b,s,a]};return c.c=[a,s,b],{aliases:[\"py\",\"gyp\"],k:r,i:/(<\\/|->|\\?)|=>/,c:[b,s,a,e.HCM,{v:[{cN:\"function\",bK:\"def\"},{cN:\"class\",bK:\"class\"}],e:/:/,i:/[${=;\\n,]/,c:[e.UTM,i,{b:/->/,eW:!0,k:\"None\"}]},{cN:\"meta\",b:/^[\\t ]*@/,e:/$/},{b:/\\b(print|exec)\\(/}]}});hljs.registerLanguage(\"json\",function(e){var i={literal:\"true false null\"},n=[e.QSM,e.CNM],r={e:\",\",eW:!0,eE:!0,c:n,k:i},t={b:\"{\",e:\"}\",c:[{cN:\"attr\",b:/\"/,e:/\"/,c:[e.BE],i:\"\\\\n\"},e.inherit(r,{b:/:/})],i:\"\\\\S\"},c={b:\"\\\\[\",e:\"\\\\]\",c:[e.inherit(r)],i:\"\\\\S\"};return n.splice(n.length,0,t,c),{c:n,k:i,i:\"\\\\S\"}});hljs.registerLanguage(\"apache\",function(e){var r={cN:\"number\",b:\"[\\\\$%]\\\\d+\"};return{aliases:[\"apacheconf\"],cI:!0,c:[e.HCM,{cN:\"section\",b:\"</?\",e:\">\"},{cN:\"attribute\",b:/\\w+/,r:0,k:{nomarkup:\"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername\"},starts:{e:/$/,r:0,k:{literal:\"on off all\"},c:[{cN:\"meta\",b:\"\\\\s\\\\[\",e:\"\\\\]$\"},{cN:\"variable\",b:\"[\\\\$%]\\\\{\",e:\"\\\\}\",c:[\"self\",r]},r,e.QSM]}}],i:/\\S/}});hljs.registerLanguage(\"cs\",function(e){var i={keyword:\"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long nameof object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let on orderby partial remove select set value var where yield\",literal:\"null false true\"},t={cN:\"string\",b:'@\"',e:'\"',c:[{b:'\"\"'}]},r=e.inherit(t,{i:/\\n/}),a={cN:\"subst\",b:\"{\",e:\"}\",k:i},c=e.inherit(a,{i:/\\n/}),n={cN:\"string\",b:/\\$\"/,e:'\"',i:/\\n/,c:[{b:\"{{\"},{b:\"}}\"},e.BE,c]},s={cN:\"string\",b:/\\$@\"/,e:'\"',c:[{b:\"{{\"},{b:\"}}\"},{b:'\"\"'},a]},o=e.inherit(s,{i:/\\n/,c:[{b:\"{{\"},{b:\"}}\"},{b:'\"\"'},c]});a.c=[s,n,t,e.ASM,e.QSM,e.CNM,e.CBCM],c.c=[o,n,r,e.ASM,e.QSM,e.CNM,e.inherit(e.CBCM,{i:/\\n/})];var l={v:[s,n,t,e.ASM,e.QSM]},b=e.IR+\"(<\"+e.IR+\"(\\\\s*,\\\\s*\"+e.IR+\")*>)?(\\\\[\\\\])?\";return{aliases:[\"csharp\"],k:i,i:/::/,c:[e.C(\"///\",\"$\",{rB:!0,c:[{cN:\"doctag\",v:[{b:\"///\",r:0},{b:\"<!--|-->\"},{b:\"</?\",e:\">\"}]}]}),e.CLCM,e.CBCM,{cN:\"meta\",b:\"#\",e:\"$\",k:{\"meta-keyword\":\"if else elif endif define undef warning error line region endregion pragma checksum\"}},l,e.CNM,{bK:\"class interface\",e:/[{;=]/,i:/[^\\s:]/,c:[e.TM,e.CLCM,e.CBCM]},{bK:\"namespace\",e:/[{;=]/,i:/[^\\s:]/,c:[e.inherit(e.TM,{b:\"[a-zA-Z](\\\\.?\\\\w)*\"}),e.CLCM,e.CBCM]},{cN:\"meta\",b:\"^\\\\s*\\\\[\",eB:!0,e:\"\\\\]\",eE:!0,c:[{cN:\"meta-string\",b:/\"/,e:/\"/}]},{bK:\"new return throw await else\",r:0},{cN:\"function\",b:\"(\"+b+\"\\\\s+)+\"+e.IR+\"\\\\s*\\\\(\",rB:!0,e:/[{;=]/,eE:!0,k:i,c:[{b:e.IR+\"\\\\s*\\\\(\",rB:!0,c:[e.TM],r:0},{cN:\"params\",b:/\\(/,e:/\\)/,eB:!0,eE:!0,k:i,r:0,c:[l,e.CNM,e.CBCM]},e.CLCM,e.CBCM]}]}});hljs.registerLanguage(\"sql\",function(e){var t=e.C(\"--\",\"$\");return{cI:!0,i:/[<>{}*#]/,c:[{bK:\"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment\",e:/;/,eW:!0,l:/[\\w\\.]+/,k:{keyword:\"abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek\",literal:\"true false null\",built_in:\"array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text varchar varying void\"},c:[{cN:\"string\",b:\"'\",e:\"'\",c:[e.BE,{b:\"''\"}]},{cN:\"string\",b:'\"',e:'\"',c:[e.BE,{b:'\"\"'}]},{cN:\"string\",b:\"`\",e:\"`\",c:[e.BE]},e.CNM,e.CBCM,t]},e.CBCM,t]}});hljs.registerLanguage(\"scss\",function(e){var t=\"[a-zA-Z-][a-zA-Z0-9_-]*\",i={cN:\"variable\",b:\"(\\\\$\"+t+\")\\\\b\"},r={cN:\"number\",b:\"#[0-9A-Fa-f]+\"};({cN:\"attribute\",b:\"[A-Z\\\\_\\\\.\\\\-]+\",e:\":\",eE:!0,i:\"[^\\\\s]\",starts:{eW:!0,eE:!0,c:[r,e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:\"meta\",b:\"!important\"}]}});return{cI:!0,i:\"[=/|']\",c:[e.CLCM,e.CBCM,{cN:\"selector-id\",b:\"\\\\#[A-Za-z0-9_-]+\",r:0},{cN:\"selector-class\",b:\"\\\\.[A-Za-z0-9_-]+\",r:0},{cN:\"selector-attr\",b:\"\\\\[\",e:\"\\\\]\",i:\"$\"},{cN:\"selector-tag\",b:\"\\\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\\\b\",r:0},{b:\":(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)\"},{b:\"::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)\"},i,{cN:\"attribute\",b:\"\\\\b(z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\\\b\",i:\"[^\\\\s]\"},{b:\"\\\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\\\b\"},{b:\":\",e:\";\",c:[i,r,e.CSSNM,e.QSM,e.ASM,{cN:\"meta\",b:\"!important\"}]},{b:\"@\",e:\"[{;]\",k:\"mixin include extend for if else each while charset import debug media page content font-face namespace warn\",c:[i,e.QSM,e.ASM,r,e.CSSNM,{b:\"\\\\s[A-Za-z0-9_.-]+\",r:0}]}]}});hljs.registerLanguage(\"yaml\",function(e){var b=\"true false yes no null\",a=\"^[ \\\\-]*\",r=\"[a-zA-Z_][\\\\w\\\\-]*\",t={cN:\"attr\",v:[{b:a+r+\":\"},{b:a+'\"'+r+'\":'},{b:a+\"'\"+r+\"':\"}]},c={cN:\"template-variable\",v:[{b:\"{{\",e:\"}}\"},{b:\"%{\",e:\"}\"}]},l={cN:\"string\",r:0,v:[{b:/'/,e:/'/},{b:/\"/,e:/\"/},{b:/\\S+/}],c:[e.BE,c]};return{cI:!0,aliases:[\"yml\",\"YAML\",\"yaml\"],c:[t,{cN:\"meta\",b:\"^---s*$\",r:10},{cN:\"string\",b:\"[\\\\|>] *$\",rE:!0,c:l.c,e:t.v[0].b},{b:\"<%[%=-]?\",e:\"[%-]?%>\",sL:\"ruby\",eB:!0,eE:!0,r:0},{cN:\"type\",b:\"!!\"+e.UIR},{cN:\"meta\",b:\"&\"+e.UIR+\"$\"},{cN:\"meta\",b:\"\\\\*\"+e.UIR+\"$\"},{cN:\"bullet\",b:\"^ *-\",r:0},e.HCM,{bK:b,k:{literal:b}},e.CNM,l]}});hljs.registerLanguage(\"php\",function(e){var c={b:\"\\\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*\"},i={cN:\"meta\",b:/<\\?(php)?|\\?>/},t={cN:\"string\",c:[e.BE,i],v:[{b:'b\"',e:'\"'},{b:\"b'\",e:\"'\"},e.inherit(e.ASM,{i:null}),e.inherit(e.QSM,{i:null})]},a={v:[e.BNM,e.CNM]};return{aliases:[\"php3\",\"php4\",\"php5\",\"php6\"],cI:!0,k:\"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally\",c:[e.HCM,e.C(\"//\",\"$\",{c:[i]}),e.C(\"/\\\\*\",\"\\\\*/\",{c:[{cN:\"doctag\",b:\"@[A-Za-z]+\"}]}),e.C(\"__halt_compiler.+?;\",!1,{eW:!0,k:\"__halt_compiler\",l:e.UIR}),{cN:\"string\",b:/<<<['\"]?\\w+['\"]?$/,e:/^\\w+;?$/,c:[e.BE,{cN:\"subst\",v:[{b:/\\$\\w+/},{b:/\\{\\$/,e:/\\}/}]}]},i,{cN:\"keyword\",b:/\\$this\\b/},c,{b:/(::|->)+[a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*/},{cN:\"function\",bK:\"function\",e:/[;{]/,eE:!0,i:\"\\\\$|\\\\[|%\",c:[e.UTM,{cN:\"params\",b:\"\\\\(\",e:\"\\\\)\",c:[\"self\",c,e.CBCM,t,a]}]},{cN:\"class\",bK:\"class interface\",e:\"{\",eE:!0,i:/[:\\(\\$\"]/,c:[{bK:\"extends implements\"},e.UTM]},{bK:\"namespace\",e:\";\",i:/[\\.']/,c:[e.UTM]},{bK:\"use\",e:\";\",c:[e.UTM]},{b:\"=>\"},t,a]}});hljs.registerLanguage(\"http\",function(e){var t=\"HTTP/[0-9\\\\.]+\";return{aliases:[\"https\"],i:\"\\\\S\",c:[{b:\"^\"+t,e:\"$\",c:[{cN:\"number\",b:\"\\\\b\\\\d{3}\\\\b\"}]},{b:\"^[A-Z]+ (.*?) \"+t+\"$\",rB:!0,e:\"$\",c:[{cN:\"string\",b:\" \",e:\" \",eB:!0,eE:!0},{b:t},{cN:\"keyword\",b:\"[A-Z]+\"}]},{cN:\"attribute\",b:\"^\\\\w\",e:\": \",eE:!0,i:\"\\\\n|\\\\s|=\",starts:{e:\"$\",r:0}},{b:\"\\\\n\\\\n\",starts:{sL:[],eW:!0}}]}});"
  },
  {
    "path": "gui/static/js/inboundmapping.js",
    "content": ";(function(window, document) {\n  'use strict';\n\n  /**\n   * @global window scope\n   * @namespace aria\n   */\n  var aria = aria || {};\n\n  /**\n   * @global script scope\n   * @type {Array}\n   */\n  var DID_LIST = DID_LIST || [];\n\n  /**\n   * Search DID_LIST for search_string\n   * DID_LIST should be globally defined\n   * @param search\n   * @returns {Array}\n   */\n  function searchDIDs(search) {\n    var res = [];\n    var num_dids = DID_LIST.length;\n\n    for (var i = 0; i < num_dids; i++) {\n      if (DID_LIST[i].indexOf(search.toLowerCase()) === 0) {\n        res.push(DID_LIST[i]);\n      }\n    }\n    return res;\n  }\n\n  /**\n   * Wrapper for initializing\n   * @param parent_selector\n   * @returns {aria.ListboxCombobox}\n   */\n  function comboboxInit(parent_selector) {\n    var parent = $(parent_selector);\n    var arrow = parent.find('.did-combobox-arrow > span');\n\n    /* create combobox */\n    new aria.ListboxCombobox(\n        parent.find('.did-combobox').get(0),\n        parent.find('.did-combobox-input').get(0),\n        parent.find('.did-listbox').get(0),\n        searchDIDs,\n        false,\n        function() {\n          arrow.removeClass('icon-circle-down');\n          arrow.addClass('icon-circle-up');\n        },\n        function() {\n          arrow.removeClass('icon-circle-up');\n          arrow.addClass('icon-circle-down');\n        }\n    )\n  }\n\n  /* any handlers depending on DOM elems go here */\n  $(document).ready(function() {\n    /* only created if we have DID's */\n    if (DID_LIST.length > 0) {\n      /* init the combobox's */\n      comboboxInit('#add .modal-body');\n      comboboxInit('#edit .modal-body');\n    }\n\n    /* init datatable */\n    $('#inboundmapping').DataTable({\n      \"columnDefs\": [\n        {\"orderable\": true, \"targets\": [1, 2, 3, 4, 5]},\n        {\"orderable\": false, \"targets\": [0, 6, 7]},\n      ],\n      \"order\": [[1, 'asc']]\n    });\n\n    $('#open-Add').click(function() {\n      /** Clear out the modal */\n      var modal_body = $('#add .modal-body');\n      modal_body.find(\"input.ruleid\").val('');\n      modal_body.find(\"input.prefix\").val('');\n      modal_body.find(\"input.rulename\").val('');\n      modal_body.find(\"input.hf_ruleid\").val('');\n      modal_body.find(\"input.hf_groupid\").val('');\n      modal_body.find(\"input.hf_fwddid\").val('');\n      modal_body.find(\"input.ff_ruleid\").val('');\n      modal_body.find(\"input.ff_groupid\").val('');\n      modal_body.find(\"input.ff_fwddid\").val('');\n\n      /* reset options selected */\n      modal_body.find(\"select\").val('');\n\n      /* reset toggle buttons */\n      modal_body.find(\"input.toggle-hardfwd\").bootstrapToggle('off');\n      modal_body.find(\"input.toggle-failfwd\").bootstrapToggle('off');\n    });\n\n    $('#inboundmapping').on('click', '#open-Update', function() {\n      var row_index = $(this).parent().parent().parent().index() + 1;\n      var c = document.getElementById('inboundmapping');\n      var ruleid = $(c).find('tr:eq(' + row_index + ') td.ruleid').text();\n      var prefix = $(c).find('tr:eq(' + row_index + ') td.prefix').text();\n      var gwgroupid = $(c).find('tr:eq(' + row_index + ') td.gwgroupid').text();\n      var gwgroupname = $(c).find('tr:eq(' + row_index + ') td.gwgroupname').text();\n      var rulename = $(c).find('tr:eq(' + row_index + ') td.rulename').text();\n      var gwlistid = $(c).find('tr:eq(' + row_index + ') td.gwlistid').text().replace('#', '');\n      var lb_enabled = $(c).find('tr:eq(' + row_index + ') td.lb_enabled').text() === '1';\n      var hf_ruleid = $(c).find('tr:eq(' + row_index + ') td.hf_ruleid').text();\n      var hf_groupid = $(c).find('tr:eq(' + row_index + ') td.hf_groupid').text();\n      var hf_gwgroupid = $(c).find('tr:eq(' + row_index + ') td.hf_gwgroupid').text();\n      var hf_fwddid = $(c).find('tr:eq(' + row_index + ') td.hf_fwddid').text();\n      var ff_ruleid = $(c).find('tr:eq(' + row_index + ') td.ff_ruleid').text();\n      var ff_groupid = $(c).find('tr:eq(' + row_index + ') td.ff_groupid').text();\n      var ff_gwgroupid = $(c).find('tr:eq(' + row_index + ') td.ff_gwgroupid').text();\n      var ff_fwddid = $(c).find('tr:eq(' + row_index + ') td.ff_fwddid').text();\n\n      /** Clear out the modal */\n      var modal_body = $('#edit .modal-body');\n      modal_body.find(\"input.ruleid\").val('');\n      modal_body.find(\"input.prefix\").val('');\n      modal_body.find(\"input.rulename\").val('');\n      modal_body.find(\"input.hf_ruleid\").val('');\n      modal_body.find(\"input.hf_groupid\").val('');\n      modal_body.find(\"input.hf_fwddid\").val('');\n      modal_body.find(\"input.ff_ruleid\").val('');\n      modal_body.find(\"input.ff_groupid\").val('');\n      modal_body.find(\"input.ff_fwddid\").val('');\n\n      /* update modal fields */\n      modal_body.find(\"input.ruleid\").val(ruleid);\n      modal_body.find(\"input.prefix\").val(prefix);\n      modal_body.find(\"input.rulename\").val(rulename);\n      modal_body.find(\"input.hf_ruleid\").val(hf_ruleid);\n      modal_body.find(\"input.hf_groupid\").val(hf_groupid);\n      modal_body.find(\"input.hf_fwddid\").val(hf_fwddid);\n      modal_body.find(\"input.ff_ruleid\").val(ff_ruleid);\n      modal_body.find(\"input.ff_groupid\").val(ff_groupid);\n      modal_body.find(\"input.ff_fwddid\").val(ff_fwddid);\n\n      /* update options selected */\n      modal_body.find(\"select\").val('');\n      modal_body.find(\"select.gwgroupid\").val(gwgroupid);\n      modal_body.find(\"select.hf_gwgroupid\").val(hf_gwgroupid);\n      modal_body.find(\"select.ff_gwgroupid\").val(ff_gwgroupid);\n      if (lb_enabled) {\n        modal_body.find(\"select.gwgroupid option\").filter(function() {\n          return this.value.indexOf('lb_'+gwgroupid) !== -1;\n        }).prop(\"selected\", true);\n      }\n\n      /* update toggle buttons */\n      if (hf_ruleid.length > 0) {\n        modal_body.find(\"input.toggle-hardfwd\").bootstrapToggle('on');\n      }\n      else {\n        modal_body.find(\"input.toggle-hardfwd\").bootstrapToggle('off');\n      }\n\n      if (ff_ruleid.length > 0) {\n        modal_body.find(\"input.toggle-failfwd\").bootstrapToggle('on');\n      }\n      else {\n        modal_body.find(\"input.toggle-failfwd\").bootstrapToggle('off');\n      }\n    });\n\n    $('#inboundmapping').on('click', '#open-Delete', function() {\n      var row_index = $(this).parent().parent().parent().index() + 1;\n      var c = document.getElementById('inboundmapping');\n      var ruleid = $(c).find('tr:eq(' + row_index + ') td:eq(1)').text();\n      var hf_ruleid = $(c).find('tr:eq(' + row_index + ') td:eq(9)').text();\n      var hf_groupid = $(c).find('tr:eq(' + row_index + ') td:eq(10)').text();\n      var ff_ruleid = $(c).find('tr:eq(' + row_index + ') td:eq(13)').text();\n      var ff_groupid = $(c).find('tr:eq(' + row_index + ') td:eq(14)').text();\n\n      /* update modal fields */\n      var modal_body = $('#delete .modal-body');\n      modal_body.find(\"input.ruleid\").val(ruleid);\n      modal_body.find(\"input.hf_ruleid\").val(hf_ruleid);\n      modal_body.find(\"input.hf_groupid\").val(hf_groupid);\n      modal_body.find(\"input.ff_ruleid\").val(ff_ruleid);\n      modal_body.find(\"input.ff_groupid\").val(ff_groupid);\n    });\n\n    /* listener for hard forward toggle */\n    $('.modal-body .toggle-hardfwd').change(function() {\n      var modal = $(this).closest('div.modal');\n      var modal_body = modal.find('.modal-body');\n\n      if ($(this).is(\":checked\") || $(this).prop(\"checked\")) {\n        modal_body.find('.hardfwd-options').removeClass(\"hidden\");\n        modal_body.find('.hardfwd_enabled').val(1);\n      }\n      else {\n        modal_body.find('.hardfwd-options').addClass(\"hidden\");\n        modal_body.find('.hardfwd_enabled').val(0);\n      }\n    });\n\n    /* listener for failover forward toggle */\n    $('.modal-body .toggle-failfwd').change(function() {\n      var modal = $(this).closest('div.modal');\n      var modal_body = modal.find('.modal-body');\n\n      if ($(this).is(\":checked\") || $(this).prop(\"checked\")) {\n        modal_body.find('.failfwd-options').removeClass(\"hidden\");\n        modal_body.find('.failfwd_enabled').val(1);\n      }\n      else {\n        modal_body.find('.failfwd-options').addClass(\"hidden\");\n        modal_body.find('.failfwd_enabled').val(0);\n      }\n    });\n  });\n\n})(window, document);\n"
  },
  {
    "path": "gui/static/js/jquery.js",
    "content": "/*!\n * jQuery JavaScript Library v3.3.1\n * https://jquery.com/\n *\n * Includes Sizzle.js\n * https://sizzlejs.com/\n *\n * Copyright JS Foundation and other contributors\n * Released under the MIT license\n * https://jquery.org/license\n *\n * Date: 2018-01-20T17:24Z\n */\n( function( global, factory ) {\n\n\t\"use strict\";\n\n\tif ( typeof module === \"object\" && typeof module.exports === \"object\" ) {\n\n\t\t// For CommonJS and CommonJS-like environments where a proper `window`\n\t\t// is present, execute the factory and get jQuery.\n\t\t// For environments that do not have a `window` with a `document`\n\t\t// (such as Node.js), expose a factory as module.exports.\n\t\t// This accentuates the need for the creation of a real `window`.\n\t\t// e.g. var jQuery = require(\"jquery\")(window);\n\t\t// See ticket #14549 for more info.\n\t\tmodule.exports = global.document ?\n\t\t\tfactory( global, true ) :\n\t\t\tfunction( w ) {\n\t\t\t\tif ( !w.document ) {\n\t\t\t\t\tthrow new Error( \"jQuery requires a window with a document\" );\n\t\t\t\t}\n\t\t\t\treturn factory( w );\n\t\t\t};\n\t} else {\n\t\tfactory( global );\n\t}\n\n// Pass this if window is not defined yet\n} )( typeof window !== \"undefined\" ? window : this, function( window, noGlobal ) {\n\n// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1\n// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode\n// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common\n// enough that all such attempts are guarded in a try block.\n\"use strict\";\n\nvar arr = [];\n\nvar document = window.document;\n\nvar getProto = Object.getPrototypeOf;\n\nvar slice = arr.slice;\n\nvar concat = arr.concat;\n\nvar push = arr.push;\n\nvar indexOf = arr.indexOf;\n\nvar class2type = {};\n\nvar toString = class2type.toString;\n\nvar hasOwn = class2type.hasOwnProperty;\n\nvar fnToString = hasOwn.toString;\n\nvar ObjectFunctionString = fnToString.call( Object );\n\nvar support = {};\n\nvar isFunction = function isFunction( obj ) {\n\n      // Support: Chrome <=57, Firefox <=52\n      // In some browsers, typeof returns \"function\" for HTML <object> elements\n      // (i.e., `typeof document.createElement( \"object\" ) === \"function\"`).\n      // We don't want to classify *any* DOM node as a function.\n      return typeof obj === \"function\" && typeof obj.nodeType !== \"number\";\n  };\n\n\nvar isWindow = function isWindow( obj ) {\n\t\treturn obj != null && obj === obj.window;\n\t};\n\n\n\n\n\tvar preservedScriptAttributes = {\n\t\ttype: true,\n\t\tsrc: true,\n\t\tnoModule: true\n\t};\n\n\tfunction DOMEval( code, doc, node ) {\n\t\tdoc = doc || document;\n\n\t\tvar i,\n\t\t\tscript = doc.createElement( \"script\" );\n\n\t\tscript.text = code;\n\t\tif ( node ) {\n\t\t\tfor ( i in preservedScriptAttributes ) {\n\t\t\t\tif ( node[ i ] ) {\n\t\t\t\t\tscript[ i ] = node[ i ];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tdoc.head.appendChild( script ).parentNode.removeChild( script );\n\t}\n\n\nfunction toType( obj ) {\n\tif ( obj == null ) {\n\t\treturn obj + \"\";\n\t}\n\n\t// Support: Android <=2.3 only (functionish RegExp)\n\treturn typeof obj === \"object\" || typeof obj === \"function\" ?\n\t\tclass2type[ toString.call( obj ) ] || \"object\" :\n\t\ttypeof obj;\n}\n/* global Symbol */\n// Defining this global in .eslintrc.json would create a danger of using the global\n// unguarded in another place, it seems safer to define global only for this module\n\n\n\nvar\n\tversion = \"3.3.1\",\n\n\t// Define a local copy of jQuery\n\tjQuery = function( selector, context ) {\n\n\t\t// The jQuery object is actually just the init constructor 'enhanced'\n\t\t// Need init if jQuery is called (just allow error to be thrown if not included)\n\t\treturn new jQuery.fn.init( selector, context );\n\t},\n\n\t// Support: Android <=4.0 only\n\t// Make sure we trim BOM and NBSP\n\trtrim = /^[\\s\\uFEFF\\xA0]+|[\\s\\uFEFF\\xA0]+$/g;\n\njQuery.fn = jQuery.prototype = {\n\n\t// The current version of jQuery being used\n\tjquery: version,\n\n\tconstructor: jQuery,\n\n\t// The default length of a jQuery object is 0\n\tlength: 0,\n\n\ttoArray: function() {\n\t\treturn slice.call( this );\n\t},\n\n\t// Get the Nth element in the matched element set OR\n\t// Get the whole matched element set as a clean array\n\tget: function( num ) {\n\n\t\t// Return all the elements in a clean array\n\t\tif ( num == null ) {\n\t\t\treturn slice.call( this );\n\t\t}\n\n\t\t// Return just the one element from the set\n\t\treturn num < 0 ? this[ num + this.length ] : this[ num ];\n\t},\n\n\t// Take an array of elements and push it onto the stack\n\t// (returning the new matched element set)\n\tpushStack: function( elems ) {\n\n\t\t// Build a new jQuery matched element set\n\t\tvar ret = jQuery.merge( this.constructor(), elems );\n\n\t\t// Add the old object onto the stack (as a reference)\n\t\tret.prevObject = this;\n\n\t\t// Return the newly-formed element set\n\t\treturn ret;\n\t},\n\n\t// Execute a callback for every element in the matched set.\n\teach: function( callback ) {\n\t\treturn jQuery.each( this, callback );\n\t},\n\n\tmap: function( callback ) {\n\t\treturn this.pushStack( jQuery.map( this, function( elem, i ) {\n\t\t\treturn callback.call( elem, i, elem );\n\t\t} ) );\n\t},\n\n\tslice: function() {\n\t\treturn this.pushStack( slice.apply( this, arguments ) );\n\t},\n\n\tfirst: function() {\n\t\treturn this.eq( 0 );\n\t},\n\n\tlast: function() {\n\t\treturn this.eq( -1 );\n\t},\n\n\teq: function( i ) {\n\t\tvar len = this.length,\n\t\t\tj = +i + ( i < 0 ? len : 0 );\n\t\treturn this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] );\n\t},\n\n\tend: function() {\n\t\treturn this.prevObject || this.constructor();\n\t},\n\n\t// For internal use only.\n\t// Behaves like an Array's method, not like a jQuery method.\n\tpush: push,\n\tsort: arr.sort,\n\tsplice: arr.splice\n};\n\njQuery.extend = jQuery.fn.extend = function() {\n\tvar options, name, src, copy, copyIsArray, clone,\n\t\ttarget = arguments[ 0 ] || {},\n\t\ti = 1,\n\t\tlength = arguments.length,\n\t\tdeep = false;\n\n\t// Handle a deep copy situation\n\tif ( typeof target === \"boolean\" ) {\n\t\tdeep = target;\n\n\t\t// Skip the boolean and the target\n\t\ttarget = arguments[ i ] || {};\n\t\ti++;\n\t}\n\n\t// Handle case when target is a string or something (possible in deep copy)\n\tif ( typeof target !== \"object\" && !isFunction( target ) ) {\n\t\ttarget = {};\n\t}\n\n\t// Extend jQuery itself if only one argument is passed\n\tif ( i === length ) {\n\t\ttarget = this;\n\t\ti--;\n\t}\n\n\tfor ( ; i < length; i++ ) {\n\n\t\t// Only deal with non-null/undefined values\n\t\tif ( ( options = arguments[ i ] ) != null ) {\n\n\t\t\t// Extend the base object\n\t\t\tfor ( name in options ) {\n\t\t\t\tsrc = target[ name ];\n\t\t\t\tcopy = options[ name ];\n\n\t\t\t\t// Prevent never-ending loop\n\t\t\t\tif ( target === copy ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Recurse if we're merging plain objects or arrays\n\t\t\t\tif ( deep && copy && ( jQuery.isPlainObject( copy ) ||\n\t\t\t\t\t( copyIsArray = Array.isArray( copy ) ) ) ) {\n\n\t\t\t\t\tif ( copyIsArray ) {\n\t\t\t\t\t\tcopyIsArray = false;\n\t\t\t\t\t\tclone = src && Array.isArray( src ) ? src : [];\n\n\t\t\t\t\t} else {\n\t\t\t\t\t\tclone = src && jQuery.isPlainObject( src ) ? src : {};\n\t\t\t\t\t}\n\n\t\t\t\t\t// Never move original objects, clone them\n\t\t\t\t\ttarget[ name ] = jQuery.extend( deep, clone, copy );\n\n\t\t\t\t// Don't bring in undefined values\n\t\t\t\t} else if ( copy !== undefined ) {\n\t\t\t\t\ttarget[ name ] = copy;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Return the modified object\n\treturn target;\n};\n\njQuery.extend( {\n\n\t// Unique for each copy of jQuery on the page\n\texpando: \"jQuery\" + ( version + Math.random() ).replace( /\\D/g, \"\" ),\n\n\t// Assume jQuery is ready without the ready module\n\tisReady: true,\n\n\terror: function( msg ) {\n\t\tthrow new Error( msg );\n\t},\n\n\tnoop: function() {},\n\n\tisPlainObject: function( obj ) {\n\t\tvar proto, Ctor;\n\n\t\t// Detect obvious negatives\n\t\t// Use toString instead of jQuery.type to catch host objects\n\t\tif ( !obj || toString.call( obj ) !== \"[object Object]\" ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tproto = getProto( obj );\n\n\t\t// Objects with no prototype (e.g., `Object.create( null )`) are plain\n\t\tif ( !proto ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Objects with prototype are plain iff they were constructed by a global Object function\n\t\tCtor = hasOwn.call( proto, \"constructor\" ) && proto.constructor;\n\t\treturn typeof Ctor === \"function\" && fnToString.call( Ctor ) === ObjectFunctionString;\n\t},\n\n\tisEmptyObject: function( obj ) {\n\n\t\t/* eslint-disable no-unused-vars */\n\t\t// See https://github.com/eslint/eslint/issues/6125\n\t\tvar name;\n\n\t\tfor ( name in obj ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t},\n\n\t// Evaluates a script in a global context\n\tglobalEval: function( code ) {\n\t\tDOMEval( code );\n\t},\n\n\teach: function( obj, callback ) {\n\t\tvar length, i = 0;\n\n\t\tif ( isArrayLike( obj ) ) {\n\t\t\tlength = obj.length;\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tif ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor ( i in obj ) {\n\t\t\t\tif ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn obj;\n\t},\n\n\t// Support: Android <=4.0 only\n\ttrim: function( text ) {\n\t\treturn text == null ?\n\t\t\t\"\" :\n\t\t\t( text + \"\" ).replace( rtrim, \"\" );\n\t},\n\n\t// results is for internal usage only\n\tmakeArray: function( arr, results ) {\n\t\tvar ret = results || [];\n\n\t\tif ( arr != null ) {\n\t\t\tif ( isArrayLike( Object( arr ) ) ) {\n\t\t\t\tjQuery.merge( ret,\n\t\t\t\t\ttypeof arr === \"string\" ?\n\t\t\t\t\t[ arr ] : arr\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tpush.call( ret, arr );\n\t\t\t}\n\t\t}\n\n\t\treturn ret;\n\t},\n\n\tinArray: function( elem, arr, i ) {\n\t\treturn arr == null ? -1 : indexOf.call( arr, elem, i );\n\t},\n\n\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t// push.apply(_, arraylike) throws on ancient WebKit\n\tmerge: function( first, second ) {\n\t\tvar len = +second.length,\n\t\t\tj = 0,\n\t\t\ti = first.length;\n\n\t\tfor ( ; j < len; j++ ) {\n\t\t\tfirst[ i++ ] = second[ j ];\n\t\t}\n\n\t\tfirst.length = i;\n\n\t\treturn first;\n\t},\n\n\tgrep: function( elems, callback, invert ) {\n\t\tvar callbackInverse,\n\t\t\tmatches = [],\n\t\t\ti = 0,\n\t\t\tlength = elems.length,\n\t\t\tcallbackExpect = !invert;\n\n\t\t// Go through the array, only saving the items\n\t\t// that pass the validator function\n\t\tfor ( ; i < length; i++ ) {\n\t\t\tcallbackInverse = !callback( elems[ i ], i );\n\t\t\tif ( callbackInverse !== callbackExpect ) {\n\t\t\t\tmatches.push( elems[ i ] );\n\t\t\t}\n\t\t}\n\n\t\treturn matches;\n\t},\n\n\t// arg is for internal usage only\n\tmap: function( elems, callback, arg ) {\n\t\tvar length, value,\n\t\t\ti = 0,\n\t\t\tret = [];\n\n\t\t// Go through the array, translating each of the items to their new values\n\t\tif ( isArrayLike( elems ) ) {\n\t\t\tlength = elems.length;\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Go through every key on the object,\n\t\t} else {\n\t\t\tfor ( i in elems ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Flatten any nested arrays\n\t\treturn concat.apply( [], ret );\n\t},\n\n\t// A global GUID counter for objects\n\tguid: 1,\n\n\t// jQuery.support is not used in Core but other projects attach their\n\t// properties to it so it needs to exist.\n\tsupport: support\n} );\n\nif ( typeof Symbol === \"function\" ) {\n\tjQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ];\n}\n\n// Populate the class2type map\njQuery.each( \"Boolean Number String Function Array Date RegExp Object Error Symbol\".split( \" \" ),\nfunction( i, name ) {\n\tclass2type[ \"[object \" + name + \"]\" ] = name.toLowerCase();\n} );\n\nfunction isArrayLike( obj ) {\n\n\t// Support: real iOS 8.2 only (not reproducible in simulator)\n\t// `in` check used to prevent JIT error (gh-2145)\n\t// hasOwn isn't used here due to false negatives\n\t// regarding Nodelist length in IE\n\tvar length = !!obj && \"length\" in obj && obj.length,\n\t\ttype = toType( obj );\n\n\tif ( isFunction( obj ) || isWindow( obj ) ) {\n\t\treturn false;\n\t}\n\n\treturn type === \"array\" || length === 0 ||\n\t\ttypeof length === \"number\" && length > 0 && ( length - 1 ) in obj;\n}\nvar Sizzle =\n/*!\n * Sizzle CSS Selector Engine v2.3.3\n * https://sizzlejs.com/\n *\n * Copyright jQuery Foundation and other contributors\n * Released under the MIT license\n * http://jquery.org/license\n *\n * Date: 2016-08-08\n */\n(function( window ) {\n\nvar i,\n\tsupport,\n\tExpr,\n\tgetText,\n\tisXML,\n\ttokenize,\n\tcompile,\n\tselect,\n\toutermostContext,\n\tsortInput,\n\thasDuplicate,\n\n\t// Local document vars\n\tsetDocument,\n\tdocument,\n\tdocElem,\n\tdocumentIsHTML,\n\trbuggyQSA,\n\trbuggyMatches,\n\tmatches,\n\tcontains,\n\n\t// Instance-specific data\n\texpando = \"sizzle\" + 1 * new Date(),\n\tpreferredDoc = window.document,\n\tdirruns = 0,\n\tdone = 0,\n\tclassCache = createCache(),\n\ttokenCache = createCache(),\n\tcompilerCache = createCache(),\n\tsortOrder = function( a, b ) {\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t}\n\t\treturn 0;\n\t},\n\n\t// Instance methods\n\thasOwn = ({}).hasOwnProperty,\n\tarr = [],\n\tpop = arr.pop,\n\tpush_native = arr.push,\n\tpush = arr.push,\n\tslice = arr.slice,\n\t// Use a stripped-down indexOf as it's faster than native\n\t// https://jsperf.com/thor-indexof-vs-for/5\n\tindexOf = function( list, elem ) {\n\t\tvar i = 0,\n\t\t\tlen = list.length;\n\t\tfor ( ; i < len; i++ ) {\n\t\t\tif ( list[i] === elem ) {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t},\n\n\tbooleans = \"checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped\",\n\n\t// Regular expressions\n\n\t// http://www.w3.org/TR/css3-selectors/#whitespace\n\twhitespace = \"[\\\\x20\\\\t\\\\r\\\\n\\\\f]\",\n\n\t// http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier\n\tidentifier = \"(?:\\\\\\\\.|[\\\\w-]|[^\\0-\\\\xa0])+\",\n\n\t// Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors\n\tattributes = \"\\\\[\" + whitespace + \"*(\" + identifier + \")(?:\" + whitespace +\n\t\t// Operator (capture 2)\n\t\t\"*([*^$|!~]?=)\" + whitespace +\n\t\t// \"Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]\"\n\t\t\"*(?:'((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\"|(\" + identifier + \"))|)\" + whitespace +\n\t\t\"*\\\\]\",\n\n\tpseudos = \":(\" + identifier + \")(?:\\\\((\" +\n\t\t// To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:\n\t\t// 1. quoted (capture 3; capture 4 or capture 5)\n\t\t\"('((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\")|\" +\n\t\t// 2. simple (capture 6)\n\t\t\"((?:\\\\\\\\.|[^\\\\\\\\()[\\\\]]|\" + attributes + \")*)|\" +\n\t\t// 3. anything else (capture 2)\n\t\t\".*\" +\n\t\t\")\\\\)|)\",\n\n\t// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter\n\trwhitespace = new RegExp( whitespace + \"+\", \"g\" ),\n\trtrim = new RegExp( \"^\" + whitespace + \"+|((?:^|[^\\\\\\\\])(?:\\\\\\\\.)*)\" + whitespace + \"+$\", \"g\" ),\n\n\trcomma = new RegExp( \"^\" + whitespace + \"*,\" + whitespace + \"*\" ),\n\trcombinators = new RegExp( \"^\" + whitespace + \"*([>+~]|\" + whitespace + \")\" + whitespace + \"*\" ),\n\n\trattributeQuotes = new RegExp( \"=\" + whitespace + \"*([^\\\\]'\\\"]*?)\" + whitespace + \"*\\\\]\", \"g\" ),\n\n\trpseudo = new RegExp( pseudos ),\n\tridentifier = new RegExp( \"^\" + identifier + \"$\" ),\n\n\tmatchExpr = {\n\t\t\"ID\": new RegExp( \"^#(\" + identifier + \")\" ),\n\t\t\"CLASS\": new RegExp( \"^\\\\.(\" + identifier + \")\" ),\n\t\t\"TAG\": new RegExp( \"^(\" + identifier + \"|[*])\" ),\n\t\t\"ATTR\": new RegExp( \"^\" + attributes ),\n\t\t\"PSEUDO\": new RegExp( \"^\" + pseudos ),\n\t\t\"CHILD\": new RegExp( \"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\" + whitespace +\n\t\t\t\"*(even|odd|(([+-]|)(\\\\d*)n|)\" + whitespace + \"*(?:([+-]|)\" + whitespace +\n\t\t\t\"*(\\\\d+)|))\" + whitespace + \"*\\\\)|)\", \"i\" ),\n\t\t\"bool\": new RegExp( \"^(?:\" + booleans + \")$\", \"i\" ),\n\t\t// For use in libraries implementing .is()\n\t\t// We use this for POS matching in `select`\n\t\t\"needsContext\": new RegExp( \"^\" + whitespace + \"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\" +\n\t\t\twhitespace + \"*((?:-\\\\d)?\\\\d*)\" + whitespace + \"*\\\\)|)(?=[^-]|$)\", \"i\" )\n\t},\n\n\trinputs = /^(?:input|select|textarea|button)$/i,\n\trheader = /^h\\d$/i,\n\n\trnative = /^[^{]+\\{\\s*\\[native \\w/,\n\n\t// Easily-parseable/retrievable ID or TAG or CLASS selectors\n\trquickExpr = /^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,\n\n\trsibling = /[+~]/,\n\n\t// CSS escapes\n\t// http://www.w3.org/TR/CSS21/syndata.html#escaped-characters\n\trunescape = new RegExp( \"\\\\\\\\([\\\\da-f]{1,6}\" + whitespace + \"?|(\" + whitespace + \")|.)\", \"ig\" ),\n\tfunescape = function( _, escaped, escapedWhitespace ) {\n\t\tvar high = \"0x\" + escaped - 0x10000;\n\t\t// NaN means non-codepoint\n\t\t// Support: Firefox<24\n\t\t// Workaround erroneous numeric interpretation of +\"0x\"\n\t\treturn high !== high || escapedWhitespace ?\n\t\t\tescaped :\n\t\t\thigh < 0 ?\n\t\t\t\t// BMP codepoint\n\t\t\t\tString.fromCharCode( high + 0x10000 ) :\n\t\t\t\t// Supplemental Plane codepoint (surrogate pair)\n\t\t\t\tString.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );\n\t},\n\n\t// CSS string/identifier serialization\n\t// https://drafts.csswg.org/cssom/#common-serializing-idioms\n\trcssescape = /([\\0-\\x1f\\x7f]|^-?\\d)|^-$|[^\\0-\\x1f\\x7f-\\uFFFF\\w-]/g,\n\tfcssescape = function( ch, asCodePoint ) {\n\t\tif ( asCodePoint ) {\n\n\t\t\t// U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER\n\t\t\tif ( ch === \"\\0\" ) {\n\t\t\t\treturn \"\\uFFFD\";\n\t\t\t}\n\n\t\t\t// Control characters and (dependent upon position) numbers get escaped as code points\n\t\t\treturn ch.slice( 0, -1 ) + \"\\\\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + \" \";\n\t\t}\n\n\t\t// Other potentially-special ASCII characters get backslash-escaped\n\t\treturn \"\\\\\" + ch;\n\t},\n\n\t// Used for iframes\n\t// See setDocument()\n\t// Removing the function wrapper causes a \"Permission Denied\"\n\t// error in IE\n\tunloadHandler = function() {\n\t\tsetDocument();\n\t},\n\n\tdisabledAncestor = addCombinator(\n\t\tfunction( elem ) {\n\t\t\treturn elem.disabled === true && (\"form\" in elem || \"label\" in elem);\n\t\t},\n\t\t{ dir: \"parentNode\", next: \"legend\" }\n\t);\n\n// Optimize for push.apply( _, NodeList )\ntry {\n\tpush.apply(\n\t\t(arr = slice.call( preferredDoc.childNodes )),\n\t\tpreferredDoc.childNodes\n\t);\n\t// Support: Android<4.0\n\t// Detect silently failing push.apply\n\tarr[ preferredDoc.childNodes.length ].nodeType;\n} catch ( e ) {\n\tpush = { apply: arr.length ?\n\n\t\t// Leverage slice if possible\n\t\tfunction( target, els ) {\n\t\t\tpush_native.apply( target, slice.call(els) );\n\t\t} :\n\n\t\t// Support: IE<9\n\t\t// Otherwise append directly\n\t\tfunction( target, els ) {\n\t\t\tvar j = target.length,\n\t\t\t\ti = 0;\n\t\t\t// Can't trust NodeList.length\n\t\t\twhile ( (target[j++] = els[i++]) ) {}\n\t\t\ttarget.length = j - 1;\n\t\t}\n\t};\n}\n\nfunction Sizzle( selector, context, results, seed ) {\n\tvar m, i, elem, nid, match, groups, newSelector,\n\t\tnewContext = context && context.ownerDocument,\n\n\t\t// nodeType defaults to 9, since context defaults to document\n\t\tnodeType = context ? context.nodeType : 9;\n\n\tresults = results || [];\n\n\t// Return early from calls with invalid selector or context\n\tif ( typeof selector !== \"string\" || !selector ||\n\t\tnodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {\n\n\t\treturn results;\n\t}\n\n\t// Try to shortcut find operations (as opposed to filters) in HTML documents\n\tif ( !seed ) {\n\n\t\tif ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {\n\t\t\tsetDocument( context );\n\t\t}\n\t\tcontext = context || document;\n\n\t\tif ( documentIsHTML ) {\n\n\t\t\t// If the selector is sufficiently simple, try using a \"get*By*\" DOM method\n\t\t\t// (excepting DocumentFragment context, where the methods don't exist)\n\t\t\tif ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {\n\n\t\t\t\t// ID selector\n\t\t\t\tif ( (m = match[1]) ) {\n\n\t\t\t\t\t// Document context\n\t\t\t\t\tif ( nodeType === 9 ) {\n\t\t\t\t\t\tif ( (elem = context.getElementById( m )) ) {\n\n\t\t\t\t\t\t\t// Support: IE, Opera, Webkit\n\t\t\t\t\t\t\t// TODO: identify versions\n\t\t\t\t\t\t\t// getElementById can match elements by name instead of ID\n\t\t\t\t\t\t\tif ( elem.id === m ) {\n\t\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t// Element context\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t// Support: IE, Opera, Webkit\n\t\t\t\t\t\t// TODO: identify versions\n\t\t\t\t\t\t// getElementById can match elements by name instead of ID\n\t\t\t\t\t\tif ( newContext && (elem = newContext.getElementById( m )) &&\n\t\t\t\t\t\t\tcontains( context, elem ) &&\n\t\t\t\t\t\t\telem.id === m ) {\n\n\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t// Type selector\n\t\t\t\t} else if ( match[2] ) {\n\t\t\t\t\tpush.apply( results, context.getElementsByTagName( selector ) );\n\t\t\t\t\treturn results;\n\n\t\t\t\t// Class selector\n\t\t\t\t} else if ( (m = match[3]) && support.getElementsByClassName &&\n\t\t\t\t\tcontext.getElementsByClassName ) {\n\n\t\t\t\t\tpush.apply( results, context.getElementsByClassName( m ) );\n\t\t\t\t\treturn results;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Take advantage of querySelectorAll\n\t\t\tif ( support.qsa &&\n\t\t\t\t!compilerCache[ selector + \" \" ] &&\n\t\t\t\t(!rbuggyQSA || !rbuggyQSA.test( selector )) ) {\n\n\t\t\t\tif ( nodeType !== 1 ) {\n\t\t\t\t\tnewContext = context;\n\t\t\t\t\tnewSelector = selector;\n\n\t\t\t\t// qSA looks outside Element context, which is not what we want\n\t\t\t\t// Thanks to Andrew Dupont for this workaround technique\n\t\t\t\t// Support: IE <=8\n\t\t\t\t// Exclude object elements\n\t\t\t\t} else if ( context.nodeName.toLowerCase() !== \"object\" ) {\n\n\t\t\t\t\t// Capture the context ID, setting it first if necessary\n\t\t\t\t\tif ( (nid = context.getAttribute( \"id\" )) ) {\n\t\t\t\t\t\tnid = nid.replace( rcssescape, fcssescape );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcontext.setAttribute( \"id\", (nid = expando) );\n\t\t\t\t\t}\n\n\t\t\t\t\t// Prefix every selector in the list\n\t\t\t\t\tgroups = tokenize( selector );\n\t\t\t\t\ti = groups.length;\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tgroups[i] = \"#\" + nid + \" \" + toSelector( groups[i] );\n\t\t\t\t\t}\n\t\t\t\t\tnewSelector = groups.join( \",\" );\n\n\t\t\t\t\t// Expand context for sibling selectors\n\t\t\t\t\tnewContext = rsibling.test( selector ) && testContext( context.parentNode ) ||\n\t\t\t\t\t\tcontext;\n\t\t\t\t}\n\n\t\t\t\tif ( newSelector ) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tpush.apply( results,\n\t\t\t\t\t\t\tnewContext.querySelectorAll( newSelector )\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn results;\n\t\t\t\t\t} catch ( qsaError ) {\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif ( nid === expando ) {\n\t\t\t\t\t\t\tcontext.removeAttribute( \"id\" );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// All others\n\treturn select( selector.replace( rtrim, \"$1\" ), context, results, seed );\n}\n\n/**\n * Create key-value caches of limited size\n * @returns {function(string, object)} Returns the Object data after storing it on itself with\n *\tproperty name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)\n *\tdeleting the oldest entry\n */\nfunction createCache() {\n\tvar keys = [];\n\n\tfunction cache( key, value ) {\n\t\t// Use (key + \" \") to avoid collision with native prototype properties (see Issue #157)\n\t\tif ( keys.push( key + \" \" ) > Expr.cacheLength ) {\n\t\t\t// Only keep the most recent entries\n\t\t\tdelete cache[ keys.shift() ];\n\t\t}\n\t\treturn (cache[ key + \" \" ] = value);\n\t}\n\treturn cache;\n}\n\n/**\n * Mark a function for special use by Sizzle\n * @param {Function} fn The function to mark\n */\nfunction markFunction( fn ) {\n\tfn[ expando ] = true;\n\treturn fn;\n}\n\n/**\n * Support testing using an element\n * @param {Function} fn Passed the created element and returns a boolean result\n */\nfunction assert( fn ) {\n\tvar el = document.createElement(\"fieldset\");\n\n\ttry {\n\t\treturn !!fn( el );\n\t} catch (e) {\n\t\treturn false;\n\t} finally {\n\t\t// Remove from its parent by default\n\t\tif ( el.parentNode ) {\n\t\t\tel.parentNode.removeChild( el );\n\t\t}\n\t\t// release memory in IE\n\t\tel = null;\n\t}\n}\n\n/**\n * Adds the same handler for all of the specified attrs\n * @param {String} attrs Pipe-separated list of attributes\n * @param {Function} handler The method that will be applied\n */\nfunction addHandle( attrs, handler ) {\n\tvar arr = attrs.split(\"|\"),\n\t\ti = arr.length;\n\n\twhile ( i-- ) {\n\t\tExpr.attrHandle[ arr[i] ] = handler;\n\t}\n}\n\n/**\n * Checks document order of two siblings\n * @param {Element} a\n * @param {Element} b\n * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b\n */\nfunction siblingCheck( a, b ) {\n\tvar cur = b && a,\n\t\tdiff = cur && a.nodeType === 1 && b.nodeType === 1 &&\n\t\t\ta.sourceIndex - b.sourceIndex;\n\n\t// Use IE sourceIndex if available on both nodes\n\tif ( diff ) {\n\t\treturn diff;\n\t}\n\n\t// Check if b follows a\n\tif ( cur ) {\n\t\twhile ( (cur = cur.nextSibling) ) {\n\t\t\tif ( cur === b ) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn a ? 1 : -1;\n}\n\n/**\n * Returns a function to use in pseudos for input types\n * @param {String} type\n */\nfunction createInputPseudo( type ) {\n\treturn function( elem ) {\n\t\tvar name = elem.nodeName.toLowerCase();\n\t\treturn name === \"input\" && elem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for buttons\n * @param {String} type\n */\nfunction createButtonPseudo( type ) {\n\treturn function( elem ) {\n\t\tvar name = elem.nodeName.toLowerCase();\n\t\treturn (name === \"input\" || name === \"button\") && elem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for :enabled/:disabled\n * @param {Boolean} disabled true for :disabled; false for :enabled\n */\nfunction createDisabledPseudo( disabled ) {\n\n\t// Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable\n\treturn function( elem ) {\n\n\t\t// Only certain elements can match :enabled or :disabled\n\t\t// https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled\n\t\t// https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled\n\t\tif ( \"form\" in elem ) {\n\n\t\t\t// Check for inherited disabledness on relevant non-disabled elements:\n\t\t\t// * listed form-associated elements in a disabled fieldset\n\t\t\t//   https://html.spec.whatwg.org/multipage/forms.html#category-listed\n\t\t\t//   https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled\n\t\t\t// * option elements in a disabled optgroup\n\t\t\t//   https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled\n\t\t\t// All such elements have a \"form\" property.\n\t\t\tif ( elem.parentNode && elem.disabled === false ) {\n\n\t\t\t\t// Option elements defer to a parent optgroup if present\n\t\t\t\tif ( \"label\" in elem ) {\n\t\t\t\t\tif ( \"label\" in elem.parentNode ) {\n\t\t\t\t\t\treturn elem.parentNode.disabled === disabled;\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn elem.disabled === disabled;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Support: IE 6 - 11\n\t\t\t\t// Use the isDisabled shortcut property to check for disabled fieldset ancestors\n\t\t\t\treturn elem.isDisabled === disabled ||\n\n\t\t\t\t\t// Where there is no isDisabled, check manually\n\t\t\t\t\t/* jshint -W018 */\n\t\t\t\t\telem.isDisabled !== !disabled &&\n\t\t\t\t\t\tdisabledAncestor( elem ) === disabled;\n\t\t\t}\n\n\t\t\treturn elem.disabled === disabled;\n\n\t\t// Try to winnow out elements that can't be disabled before trusting the disabled property.\n\t\t// Some victims get caught in our net (label, legend, menu, track), but it shouldn't\n\t\t// even exist on them, let alone have a boolean value.\n\t\t} else if ( \"label\" in elem ) {\n\t\t\treturn elem.disabled === disabled;\n\t\t}\n\n\t\t// Remaining elements are neither :enabled nor :disabled\n\t\treturn false;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for positionals\n * @param {Function} fn\n */\nfunction createPositionalPseudo( fn ) {\n\treturn markFunction(function( argument ) {\n\t\targument = +argument;\n\t\treturn markFunction(function( seed, matches ) {\n\t\t\tvar j,\n\t\t\t\tmatchIndexes = fn( [], seed.length, argument ),\n\t\t\t\ti = matchIndexes.length;\n\n\t\t\t// Match elements found at the specified indexes\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( seed[ (j = matchIndexes[i]) ] ) {\n\t\t\t\t\tseed[j] = !(matches[j] = seed[j]);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n}\n\n/**\n * Checks a node for validity as a Sizzle context\n * @param {Element|Object=} context\n * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value\n */\nfunction testContext( context ) {\n\treturn context && typeof context.getElementsByTagName !== \"undefined\" && context;\n}\n\n// Expose support vars for convenience\nsupport = Sizzle.support = {};\n\n/**\n * Detects XML nodes\n * @param {Element|Object} elem An element or a document\n * @returns {Boolean} True iff elem is a non-HTML XML node\n */\nisXML = Sizzle.isXML = function( elem ) {\n\t// documentElement is verified for cases where it doesn't yet exist\n\t// (such as loading iframes in IE - #4833)\n\tvar documentElement = elem && (elem.ownerDocument || elem).documentElement;\n\treturn documentElement ? documentElement.nodeName !== \"HTML\" : false;\n};\n\n/**\n * Sets document-related variables once based on the current document\n * @param {Element|Object} [doc] An element or document object to use to set the document\n * @returns {Object} Returns the current document\n */\nsetDocument = Sizzle.setDocument = function( node ) {\n\tvar hasCompare, subWindow,\n\t\tdoc = node ? node.ownerDocument || node : preferredDoc;\n\n\t// Return early if doc is invalid or already selected\n\tif ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {\n\t\treturn document;\n\t}\n\n\t// Update global variables\n\tdocument = doc;\n\tdocElem = document.documentElement;\n\tdocumentIsHTML = !isXML( document );\n\n\t// Support: IE 9-11, Edge\n\t// Accessing iframe documents after unload throws \"permission denied\" errors (jQuery #13936)\n\tif ( preferredDoc !== document &&\n\t\t(subWindow = document.defaultView) && subWindow.top !== subWindow ) {\n\n\t\t// Support: IE 11, Edge\n\t\tif ( subWindow.addEventListener ) {\n\t\t\tsubWindow.addEventListener( \"unload\", unloadHandler, false );\n\n\t\t// Support: IE 9 - 10 only\n\t\t} else if ( subWindow.attachEvent ) {\n\t\t\tsubWindow.attachEvent( \"onunload\", unloadHandler );\n\t\t}\n\t}\n\n\t/* Attributes\n\t---------------------------------------------------------------------- */\n\n\t// Support: IE<8\n\t// Verify that getAttribute really returns attributes and not properties\n\t// (excepting IE8 booleans)\n\tsupport.attributes = assert(function( el ) {\n\t\tel.className = \"i\";\n\t\treturn !el.getAttribute(\"className\");\n\t});\n\n\t/* getElement(s)By*\n\t---------------------------------------------------------------------- */\n\n\t// Check if getElementsByTagName(\"*\") returns only elements\n\tsupport.getElementsByTagName = assert(function( el ) {\n\t\tel.appendChild( document.createComment(\"\") );\n\t\treturn !el.getElementsByTagName(\"*\").length;\n\t});\n\n\t// Support: IE<9\n\tsupport.getElementsByClassName = rnative.test( document.getElementsByClassName );\n\n\t// Support: IE<10\n\t// Check if getElementById returns elements by name\n\t// The broken getElementById methods don't pick up programmatically-set names,\n\t// so use a roundabout getElementsByName test\n\tsupport.getById = assert(function( el ) {\n\t\tdocElem.appendChild( el ).id = expando;\n\t\treturn !document.getElementsByName || !document.getElementsByName( expando ).length;\n\t});\n\n\t// ID filter and find\n\tif ( support.getById ) {\n\t\tExpr.filter[\"ID\"] = function( id ) {\n\t\t\tvar attrId = id.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\treturn elem.getAttribute(\"id\") === attrId;\n\t\t\t};\n\t\t};\n\t\tExpr.find[\"ID\"] = function( id, context ) {\n\t\t\tif ( typeof context.getElementById !== \"undefined\" && documentIsHTML ) {\n\t\t\t\tvar elem = context.getElementById( id );\n\t\t\t\treturn elem ? [ elem ] : [];\n\t\t\t}\n\t\t};\n\t} else {\n\t\tExpr.filter[\"ID\"] =  function( id ) {\n\t\t\tvar attrId = id.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\tvar node = typeof elem.getAttributeNode !== \"undefined\" &&\n\t\t\t\t\telem.getAttributeNode(\"id\");\n\t\t\t\treturn node && node.value === attrId;\n\t\t\t};\n\t\t};\n\n\t\t// Support: IE 6 - 7 only\n\t\t// getElementById is not reliable as a find shortcut\n\t\tExpr.find[\"ID\"] = function( id, context ) {\n\t\t\tif ( typeof context.getElementById !== \"undefined\" && documentIsHTML ) {\n\t\t\t\tvar node, i, elems,\n\t\t\t\t\telem = context.getElementById( id );\n\n\t\t\t\tif ( elem ) {\n\n\t\t\t\t\t// Verify the id attribute\n\t\t\t\t\tnode = elem.getAttributeNode(\"id\");\n\t\t\t\t\tif ( node && node.value === id ) {\n\t\t\t\t\t\treturn [ elem ];\n\t\t\t\t\t}\n\n\t\t\t\t\t// Fall back on getElementsByName\n\t\t\t\t\telems = context.getElementsByName( id );\n\t\t\t\t\ti = 0;\n\t\t\t\t\twhile ( (elem = elems[i++]) ) {\n\t\t\t\t\t\tnode = elem.getAttributeNode(\"id\");\n\t\t\t\t\t\tif ( node && node.value === id ) {\n\t\t\t\t\t\t\treturn [ elem ];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn [];\n\t\t\t}\n\t\t};\n\t}\n\n\t// Tag\n\tExpr.find[\"TAG\"] = support.getElementsByTagName ?\n\t\tfunction( tag, context ) {\n\t\t\tif ( typeof context.getElementsByTagName !== \"undefined\" ) {\n\t\t\t\treturn context.getElementsByTagName( tag );\n\n\t\t\t// DocumentFragment nodes don't have gEBTN\n\t\t\t} else if ( support.qsa ) {\n\t\t\t\treturn context.querySelectorAll( tag );\n\t\t\t}\n\t\t} :\n\n\t\tfunction( tag, context ) {\n\t\t\tvar elem,\n\t\t\t\ttmp = [],\n\t\t\t\ti = 0,\n\t\t\t\t// By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too\n\t\t\t\tresults = context.getElementsByTagName( tag );\n\n\t\t\t// Filter out possible comments\n\t\t\tif ( tag === \"*\" ) {\n\t\t\t\twhile ( (elem = results[i++]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\t\ttmp.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn tmp;\n\t\t\t}\n\t\t\treturn results;\n\t\t};\n\n\t// Class\n\tExpr.find[\"CLASS\"] = support.getElementsByClassName && function( className, context ) {\n\t\tif ( typeof context.getElementsByClassName !== \"undefined\" && documentIsHTML ) {\n\t\t\treturn context.getElementsByClassName( className );\n\t\t}\n\t};\n\n\t/* QSA/matchesSelector\n\t---------------------------------------------------------------------- */\n\n\t// QSA and matchesSelector support\n\n\t// matchesSelector(:active) reports false when true (IE9/Opera 11.5)\n\trbuggyMatches = [];\n\n\t// qSa(:focus) reports false when true (Chrome 21)\n\t// We allow this because of a bug in IE8/9 that throws an error\n\t// whenever `document.activeElement` is accessed on an iframe\n\t// So, we allow :focus to pass through QSA all the time to avoid the IE error\n\t// See https://bugs.jquery.com/ticket/13378\n\trbuggyQSA = [];\n\n\tif ( (support.qsa = rnative.test( document.querySelectorAll )) ) {\n\t\t// Build QSA regex\n\t\t// Regex strategy adopted from Diego Perini\n\t\tassert(function( el ) {\n\t\t\t// Select is set to empty string on purpose\n\t\t\t// This is to test IE's treatment of not explicitly\n\t\t\t// setting a boolean content attribute,\n\t\t\t// since its presence should be enough\n\t\t\t// https://bugs.jquery.com/ticket/12359\n\t\t\tdocElem.appendChild( el ).innerHTML = \"<a id='\" + expando + \"'></a>\" +\n\t\t\t\t\"<select id='\" + expando + \"-\\r\\\\' msallowcapture=''>\" +\n\t\t\t\t\"<option selected=''></option></select>\";\n\n\t\t\t// Support: IE8, Opera 11-12.16\n\t\t\t// Nothing should be selected when empty strings follow ^= or $= or *=\n\t\t\t// The test attribute must be unknown in Opera but \"safe\" for WinRT\n\t\t\t// https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section\n\t\t\tif ( el.querySelectorAll(\"[msallowcapture^='']\").length ) {\n\t\t\t\trbuggyQSA.push( \"[*^$]=\" + whitespace + \"*(?:''|\\\"\\\")\" );\n\t\t\t}\n\n\t\t\t// Support: IE8\n\t\t\t// Boolean attributes and \"value\" are not treated correctly\n\t\t\tif ( !el.querySelectorAll(\"[selected]\").length ) {\n\t\t\t\trbuggyQSA.push( \"\\\\[\" + whitespace + \"*(?:value|\" + booleans + \")\" );\n\t\t\t}\n\n\t\t\t// Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+\n\t\t\tif ( !el.querySelectorAll( \"[id~=\" + expando + \"-]\" ).length ) {\n\t\t\t\trbuggyQSA.push(\"~=\");\n\t\t\t}\n\n\t\t\t// Webkit/Opera - :checked should return selected option elements\n\t\t\t// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n\t\t\t// IE8 throws error here and will not see later tests\n\t\t\tif ( !el.querySelectorAll(\":checked\").length ) {\n\t\t\t\trbuggyQSA.push(\":checked\");\n\t\t\t}\n\n\t\t\t// Support: Safari 8+, iOS 8+\n\t\t\t// https://bugs.webkit.org/show_bug.cgi?id=136851\n\t\t\t// In-page `selector#id sibling-combinator selector` fails\n\t\t\tif ( !el.querySelectorAll( \"a#\" + expando + \"+*\" ).length ) {\n\t\t\t\trbuggyQSA.push(\".#.+[+~]\");\n\t\t\t}\n\t\t});\n\n\t\tassert(function( el ) {\n\t\t\tel.innerHTML = \"<a href='' disabled='disabled'></a>\" +\n\t\t\t\t\"<select disabled='disabled'><option/></select>\";\n\n\t\t\t// Support: Windows 8 Native Apps\n\t\t\t// The type and name attributes are restricted during .innerHTML assignment\n\t\t\tvar input = document.createElement(\"input\");\n\t\t\tinput.setAttribute( \"type\", \"hidden\" );\n\t\t\tel.appendChild( input ).setAttribute( \"name\", \"D\" );\n\n\t\t\t// Support: IE8\n\t\t\t// Enforce case-sensitivity of name attribute\n\t\t\tif ( el.querySelectorAll(\"[name=d]\").length ) {\n\t\t\t\trbuggyQSA.push( \"name\" + whitespace + \"*[*^$|!~]?=\" );\n\t\t\t}\n\n\t\t\t// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)\n\t\t\t// IE8 throws error here and will not see later tests\n\t\t\tif ( el.querySelectorAll(\":enabled\").length !== 2 ) {\n\t\t\t\trbuggyQSA.push( \":enabled\", \":disabled\" );\n\t\t\t}\n\n\t\t\t// Support: IE9-11+\n\t\t\t// IE's :disabled selector does not pick up the children of disabled fieldsets\n\t\t\tdocElem.appendChild( el ).disabled = true;\n\t\t\tif ( el.querySelectorAll(\":disabled\").length !== 2 ) {\n\t\t\t\trbuggyQSA.push( \":enabled\", \":disabled\" );\n\t\t\t}\n\n\t\t\t// Opera 10-11 does not throw on post-comma invalid pseudos\n\t\t\tel.querySelectorAll(\"*,:x\");\n\t\t\trbuggyQSA.push(\",.*:\");\n\t\t});\n\t}\n\n\tif ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||\n\t\tdocElem.webkitMatchesSelector ||\n\t\tdocElem.mozMatchesSelector ||\n\t\tdocElem.oMatchesSelector ||\n\t\tdocElem.msMatchesSelector) )) ) {\n\n\t\tassert(function( el ) {\n\t\t\t// Check to see if it's possible to do matchesSelector\n\t\t\t// on a disconnected node (IE 9)\n\t\t\tsupport.disconnectedMatch = matches.call( el, \"*\" );\n\n\t\t\t// This should fail with an exception\n\t\t\t// Gecko does not error, returns false instead\n\t\t\tmatches.call( el, \"[s!='']:x\" );\n\t\t\trbuggyMatches.push( \"!=\", pseudos );\n\t\t});\n\t}\n\n\trbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join(\"|\") );\n\trbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join(\"|\") );\n\n\t/* Contains\n\t---------------------------------------------------------------------- */\n\thasCompare = rnative.test( docElem.compareDocumentPosition );\n\n\t// Element contains another\n\t// Purposefully self-exclusive\n\t// As in, an element does not contain itself\n\tcontains = hasCompare || rnative.test( docElem.contains ) ?\n\t\tfunction( a, b ) {\n\t\t\tvar adown = a.nodeType === 9 ? a.documentElement : a,\n\t\t\t\tbup = b && b.parentNode;\n\t\t\treturn a === bup || !!( bup && bup.nodeType === 1 && (\n\t\t\t\tadown.contains ?\n\t\t\t\t\tadown.contains( bup ) :\n\t\t\t\t\ta.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16\n\t\t\t));\n\t\t} :\n\t\tfunction( a, b ) {\n\t\t\tif ( b ) {\n\t\t\t\twhile ( (b = b.parentNode) ) {\n\t\t\t\t\tif ( b === a ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\n\t/* Sorting\n\t---------------------------------------------------------------------- */\n\n\t// Document order sorting\n\tsortOrder = hasCompare ?\n\tfunction( a, b ) {\n\n\t\t// Flag for duplicate removal\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t\treturn 0;\n\t\t}\n\n\t\t// Sort on method existence if only one input has compareDocumentPosition\n\t\tvar compare = !a.compareDocumentPosition - !b.compareDocumentPosition;\n\t\tif ( compare ) {\n\t\t\treturn compare;\n\t\t}\n\n\t\t// Calculate position if both inputs belong to the same document\n\t\tcompare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?\n\t\t\ta.compareDocumentPosition( b ) :\n\n\t\t\t// Otherwise we know they are disconnected\n\t\t\t1;\n\n\t\t// Disconnected nodes\n\t\tif ( compare & 1 ||\n\t\t\t(!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {\n\n\t\t\t// Choose the first element that is related to our preferred document\n\t\t\tif ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tif ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\t// Maintain original order\n\t\t\treturn sortInput ?\n\t\t\t\t( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :\n\t\t\t\t0;\n\t\t}\n\n\t\treturn compare & 4 ? -1 : 1;\n\t} :\n\tfunction( a, b ) {\n\t\t// Exit early if the nodes are identical\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t\treturn 0;\n\t\t}\n\n\t\tvar cur,\n\t\t\ti = 0,\n\t\t\taup = a.parentNode,\n\t\t\tbup = b.parentNode,\n\t\t\tap = [ a ],\n\t\t\tbp = [ b ];\n\n\t\t// Parentless nodes are either documents or disconnected\n\t\tif ( !aup || !bup ) {\n\t\t\treturn a === document ? -1 :\n\t\t\t\tb === document ? 1 :\n\t\t\t\taup ? -1 :\n\t\t\t\tbup ? 1 :\n\t\t\t\tsortInput ?\n\t\t\t\t( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :\n\t\t\t\t0;\n\n\t\t// If the nodes are siblings, we can do a quick check\n\t\t} else if ( aup === bup ) {\n\t\t\treturn siblingCheck( a, b );\n\t\t}\n\n\t\t// Otherwise we need full lists of their ancestors for comparison\n\t\tcur = a;\n\t\twhile ( (cur = cur.parentNode) ) {\n\t\t\tap.unshift( cur );\n\t\t}\n\t\tcur = b;\n\t\twhile ( (cur = cur.parentNode) ) {\n\t\t\tbp.unshift( cur );\n\t\t}\n\n\t\t// Walk down the tree looking for a discrepancy\n\t\twhile ( ap[i] === bp[i] ) {\n\t\t\ti++;\n\t\t}\n\n\t\treturn i ?\n\t\t\t// Do a sibling check if the nodes have a common ancestor\n\t\t\tsiblingCheck( ap[i], bp[i] ) :\n\n\t\t\t// Otherwise nodes in our document sort first\n\t\t\tap[i] === preferredDoc ? -1 :\n\t\t\tbp[i] === preferredDoc ? 1 :\n\t\t\t0;\n\t};\n\n\treturn document;\n};\n\nSizzle.matches = function( expr, elements ) {\n\treturn Sizzle( expr, null, null, elements );\n};\n\nSizzle.matchesSelector = function( elem, expr ) {\n\t// Set document vars if needed\n\tif ( ( elem.ownerDocument || elem ) !== document ) {\n\t\tsetDocument( elem );\n\t}\n\n\t// Make sure that attribute selectors are quoted\n\texpr = expr.replace( rattributeQuotes, \"='$1']\" );\n\n\tif ( support.matchesSelector && documentIsHTML &&\n\t\t!compilerCache[ expr + \" \" ] &&\n\t\t( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&\n\t\t( !rbuggyQSA     || !rbuggyQSA.test( expr ) ) ) {\n\n\t\ttry {\n\t\t\tvar ret = matches.call( elem, expr );\n\n\t\t\t// IE 9's matchesSelector returns false on disconnected nodes\n\t\t\tif ( ret || support.disconnectedMatch ||\n\t\t\t\t\t// As well, disconnected nodes are said to be in a document\n\t\t\t\t\t// fragment in IE 9\n\t\t\t\t\telem.document && elem.document.nodeType !== 11 ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\t\t} catch (e) {}\n\t}\n\n\treturn Sizzle( expr, document, null, [ elem ] ).length > 0;\n};\n\nSizzle.contains = function( context, elem ) {\n\t// Set document vars if needed\n\tif ( ( context.ownerDocument || context ) !== document ) {\n\t\tsetDocument( context );\n\t}\n\treturn contains( context, elem );\n};\n\nSizzle.attr = function( elem, name ) {\n\t// Set document vars if needed\n\tif ( ( elem.ownerDocument || elem ) !== document ) {\n\t\tsetDocument( elem );\n\t}\n\n\tvar fn = Expr.attrHandle[ name.toLowerCase() ],\n\t\t// Don't get fooled by Object.prototype properties (jQuery #13807)\n\t\tval = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?\n\t\t\tfn( elem, name, !documentIsHTML ) :\n\t\t\tundefined;\n\n\treturn val !== undefined ?\n\t\tval :\n\t\tsupport.attributes || !documentIsHTML ?\n\t\t\telem.getAttribute( name ) :\n\t\t\t(val = elem.getAttributeNode(name)) && val.specified ?\n\t\t\t\tval.value :\n\t\t\t\tnull;\n};\n\nSizzle.escape = function( sel ) {\n\treturn (sel + \"\").replace( rcssescape, fcssescape );\n};\n\nSizzle.error = function( msg ) {\n\tthrow new Error( \"Syntax error, unrecognized expression: \" + msg );\n};\n\n/**\n * Document sorting and removing duplicates\n * @param {ArrayLike} results\n */\nSizzle.uniqueSort = function( results ) {\n\tvar elem,\n\t\tduplicates = [],\n\t\tj = 0,\n\t\ti = 0;\n\n\t// Unless we *know* we can detect duplicates, assume their presence\n\thasDuplicate = !support.detectDuplicates;\n\tsortInput = !support.sortStable && results.slice( 0 );\n\tresults.sort( sortOrder );\n\n\tif ( hasDuplicate ) {\n\t\twhile ( (elem = results[i++]) ) {\n\t\t\tif ( elem === results[ i ] ) {\n\t\t\t\tj = duplicates.push( i );\n\t\t\t}\n\t\t}\n\t\twhile ( j-- ) {\n\t\t\tresults.splice( duplicates[ j ], 1 );\n\t\t}\n\t}\n\n\t// Clear input after sorting to release objects\n\t// See https://github.com/jquery/sizzle/pull/225\n\tsortInput = null;\n\n\treturn results;\n};\n\n/**\n * Utility function for retrieving the text value of an array of DOM nodes\n * @param {Array|Element} elem\n */\ngetText = Sizzle.getText = function( elem ) {\n\tvar node,\n\t\tret = \"\",\n\t\ti = 0,\n\t\tnodeType = elem.nodeType;\n\n\tif ( !nodeType ) {\n\t\t// If no nodeType, this is expected to be an array\n\t\twhile ( (node = elem[i++]) ) {\n\t\t\t// Do not traverse comment nodes\n\t\t\tret += getText( node );\n\t\t}\n\t} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {\n\t\t// Use textContent for elements\n\t\t// innerText usage removed for consistency of new lines (jQuery #11153)\n\t\tif ( typeof elem.textContent === \"string\" ) {\n\t\t\treturn elem.textContent;\n\t\t} else {\n\t\t\t// Traverse its children\n\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n\t\t\t\tret += getText( elem );\n\t\t\t}\n\t\t}\n\t} else if ( nodeType === 3 || nodeType === 4 ) {\n\t\treturn elem.nodeValue;\n\t}\n\t// Do not include comment or processing instruction nodes\n\n\treturn ret;\n};\n\nExpr = Sizzle.selectors = {\n\n\t// Can be adjusted by the user\n\tcacheLength: 50,\n\n\tcreatePseudo: markFunction,\n\n\tmatch: matchExpr,\n\n\tattrHandle: {},\n\n\tfind: {},\n\n\trelative: {\n\t\t\">\": { dir: \"parentNode\", first: true },\n\t\t\" \": { dir: \"parentNode\" },\n\t\t\"+\": { dir: \"previousSibling\", first: true },\n\t\t\"~\": { dir: \"previousSibling\" }\n\t},\n\n\tpreFilter: {\n\t\t\"ATTR\": function( match ) {\n\t\t\tmatch[1] = match[1].replace( runescape, funescape );\n\n\t\t\t// Move the given value to match[3] whether quoted or unquoted\n\t\t\tmatch[3] = ( match[3] || match[4] || match[5] || \"\" ).replace( runescape, funescape );\n\n\t\t\tif ( match[2] === \"~=\" ) {\n\t\t\t\tmatch[3] = \" \" + match[3] + \" \";\n\t\t\t}\n\n\t\t\treturn match.slice( 0, 4 );\n\t\t},\n\n\t\t\"CHILD\": function( match ) {\n\t\t\t/* matches from matchExpr[\"CHILD\"]\n\t\t\t\t1 type (only|nth|...)\n\t\t\t\t2 what (child|of-type)\n\t\t\t\t3 argument (even|odd|\\d*|\\d*n([+-]\\d+)?|...)\n\t\t\t\t4 xn-component of xn+y argument ([+-]?\\d*n|)\n\t\t\t\t5 sign of xn-component\n\t\t\t\t6 x of xn-component\n\t\t\t\t7 sign of y-component\n\t\t\t\t8 y of y-component\n\t\t\t*/\n\t\t\tmatch[1] = match[1].toLowerCase();\n\n\t\t\tif ( match[1].slice( 0, 3 ) === \"nth\" ) {\n\t\t\t\t// nth-* requires argument\n\t\t\t\tif ( !match[3] ) {\n\t\t\t\t\tSizzle.error( match[0] );\n\t\t\t\t}\n\n\t\t\t\t// numeric x and y parameters for Expr.filter.CHILD\n\t\t\t\t// remember that false/true cast respectively to 0/1\n\t\t\t\tmatch[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === \"even\" || match[3] === \"odd\" ) );\n\t\t\t\tmatch[5] = +( ( match[7] + match[8] ) || match[3] === \"odd\" );\n\n\t\t\t// other types prohibit arguments\n\t\t\t} else if ( match[3] ) {\n\t\t\t\tSizzle.error( match[0] );\n\t\t\t}\n\n\t\t\treturn match;\n\t\t},\n\n\t\t\"PSEUDO\": function( match ) {\n\t\t\tvar excess,\n\t\t\t\tunquoted = !match[6] && match[2];\n\n\t\t\tif ( matchExpr[\"CHILD\"].test( match[0] ) ) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// Accept quoted arguments as-is\n\t\t\tif ( match[3] ) {\n\t\t\t\tmatch[2] = match[4] || match[5] || \"\";\n\n\t\t\t// Strip excess characters from unquoted arguments\n\t\t\t} else if ( unquoted && rpseudo.test( unquoted ) &&\n\t\t\t\t// Get excess from tokenize (recursively)\n\t\t\t\t(excess = tokenize( unquoted, true )) &&\n\t\t\t\t// advance to the next closing parenthesis\n\t\t\t\t(excess = unquoted.indexOf( \")\", unquoted.length - excess ) - unquoted.length) ) {\n\n\t\t\t\t// excess is a negative index\n\t\t\t\tmatch[0] = match[0].slice( 0, excess );\n\t\t\t\tmatch[2] = unquoted.slice( 0, excess );\n\t\t\t}\n\n\t\t\t// Return only captures needed by the pseudo filter method (type and argument)\n\t\t\treturn match.slice( 0, 3 );\n\t\t}\n\t},\n\n\tfilter: {\n\n\t\t\"TAG\": function( nodeNameSelector ) {\n\t\t\tvar nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();\n\t\t\treturn nodeNameSelector === \"*\" ?\n\t\t\t\tfunction() { return true; } :\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn elem.nodeName && elem.nodeName.toLowerCase() === nodeName;\n\t\t\t\t};\n\t\t},\n\n\t\t\"CLASS\": function( className ) {\n\t\t\tvar pattern = classCache[ className + \" \" ];\n\n\t\t\treturn pattern ||\n\t\t\t\t(pattern = new RegExp( \"(^|\" + whitespace + \")\" + className + \"(\" + whitespace + \"|$)\" )) &&\n\t\t\t\tclassCache( className, function( elem ) {\n\t\t\t\t\treturn pattern.test( typeof elem.className === \"string\" && elem.className || typeof elem.getAttribute !== \"undefined\" && elem.getAttribute(\"class\") || \"\" );\n\t\t\t\t});\n\t\t},\n\n\t\t\"ATTR\": function( name, operator, check ) {\n\t\t\treturn function( elem ) {\n\t\t\t\tvar result = Sizzle.attr( elem, name );\n\n\t\t\t\tif ( result == null ) {\n\t\t\t\t\treturn operator === \"!=\";\n\t\t\t\t}\n\t\t\t\tif ( !operator ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tresult += \"\";\n\n\t\t\t\treturn operator === \"=\" ? result === check :\n\t\t\t\t\toperator === \"!=\" ? result !== check :\n\t\t\t\t\toperator === \"^=\" ? check && result.indexOf( check ) === 0 :\n\t\t\t\t\toperator === \"*=\" ? check && result.indexOf( check ) > -1 :\n\t\t\t\t\toperator === \"$=\" ? check && result.slice( -check.length ) === check :\n\t\t\t\t\toperator === \"~=\" ? ( \" \" + result.replace( rwhitespace, \" \" ) + \" \" ).indexOf( check ) > -1 :\n\t\t\t\t\toperator === \"|=\" ? result === check || result.slice( 0, check.length + 1 ) === check + \"-\" :\n\t\t\t\t\tfalse;\n\t\t\t};\n\t\t},\n\n\t\t\"CHILD\": function( type, what, argument, first, last ) {\n\t\t\tvar simple = type.slice( 0, 3 ) !== \"nth\",\n\t\t\t\tforward = type.slice( -4 ) !== \"last\",\n\t\t\t\tofType = what === \"of-type\";\n\n\t\t\treturn first === 1 && last === 0 ?\n\n\t\t\t\t// Shortcut for :nth-*(n)\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn !!elem.parentNode;\n\t\t\t\t} :\n\n\t\t\t\tfunction( elem, context, xml ) {\n\t\t\t\t\tvar cache, uniqueCache, outerCache, node, nodeIndex, start,\n\t\t\t\t\t\tdir = simple !== forward ? \"nextSibling\" : \"previousSibling\",\n\t\t\t\t\t\tparent = elem.parentNode,\n\t\t\t\t\t\tname = ofType && elem.nodeName.toLowerCase(),\n\t\t\t\t\t\tuseCache = !xml && !ofType,\n\t\t\t\t\t\tdiff = false;\n\n\t\t\t\t\tif ( parent ) {\n\n\t\t\t\t\t\t// :(first|last|only)-(child|of-type)\n\t\t\t\t\t\tif ( simple ) {\n\t\t\t\t\t\t\twhile ( dir ) {\n\t\t\t\t\t\t\t\tnode = elem;\n\t\t\t\t\t\t\t\twhile ( (node = node[ dir ]) ) {\n\t\t\t\t\t\t\t\t\tif ( ofType ?\n\t\t\t\t\t\t\t\t\t\tnode.nodeName.toLowerCase() === name :\n\t\t\t\t\t\t\t\t\t\tnode.nodeType === 1 ) {\n\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Reverse direction for :only-* (if we haven't yet done so)\n\t\t\t\t\t\t\t\tstart = dir = type === \"only\" && !start && \"nextSibling\";\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tstart = [ forward ? parent.firstChild : parent.lastChild ];\n\n\t\t\t\t\t\t// non-xml :nth-child(...) stores cache data on `parent`\n\t\t\t\t\t\tif ( forward && useCache ) {\n\n\t\t\t\t\t\t\t// Seek `elem` from a previously-cached index\n\n\t\t\t\t\t\t\t// ...in a gzip-friendly way\n\t\t\t\t\t\t\tnode = parent;\n\t\t\t\t\t\t\touterCache = node[ expando ] || (node[ expando ] = {});\n\n\t\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\t\tuniqueCache = outerCache[ node.uniqueID ] ||\n\t\t\t\t\t\t\t\t(outerCache[ node.uniqueID ] = {});\n\n\t\t\t\t\t\t\tcache = uniqueCache[ type ] || [];\n\t\t\t\t\t\t\tnodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];\n\t\t\t\t\t\t\tdiff = nodeIndex && cache[ 2 ];\n\t\t\t\t\t\t\tnode = nodeIndex && parent.childNodes[ nodeIndex ];\n\n\t\t\t\t\t\t\twhile ( (node = ++nodeIndex && node && node[ dir ] ||\n\n\t\t\t\t\t\t\t\t// Fallback to seeking `elem` from the start\n\t\t\t\t\t\t\t\t(diff = nodeIndex = 0) || start.pop()) ) {\n\n\t\t\t\t\t\t\t\t// When found, cache indexes on `parent` and break\n\t\t\t\t\t\t\t\tif ( node.nodeType === 1 && ++diff && node === elem ) {\n\t\t\t\t\t\t\t\t\tuniqueCache[ type ] = [ dirruns, nodeIndex, diff ];\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Use previously-cached element index if available\n\t\t\t\t\t\t\tif ( useCache ) {\n\t\t\t\t\t\t\t\t// ...in a gzip-friendly way\n\t\t\t\t\t\t\t\tnode = elem;\n\t\t\t\t\t\t\t\touterCache = node[ expando ] || (node[ expando ] = {});\n\n\t\t\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\t\t\tuniqueCache = outerCache[ node.uniqueID ] ||\n\t\t\t\t\t\t\t\t\t(outerCache[ node.uniqueID ] = {});\n\n\t\t\t\t\t\t\t\tcache = uniqueCache[ type ] || [];\n\t\t\t\t\t\t\t\tnodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];\n\t\t\t\t\t\t\t\tdiff = nodeIndex;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// xml :nth-child(...)\n\t\t\t\t\t\t\t// or :nth-last-child(...) or :nth(-last)?-of-type(...)\n\t\t\t\t\t\t\tif ( diff === false ) {\n\t\t\t\t\t\t\t\t// Use the same loop as above to seek `elem` from the start\n\t\t\t\t\t\t\t\twhile ( (node = ++nodeIndex && node && node[ dir ] ||\n\t\t\t\t\t\t\t\t\t(diff = nodeIndex = 0) || start.pop()) ) {\n\n\t\t\t\t\t\t\t\t\tif ( ( ofType ?\n\t\t\t\t\t\t\t\t\t\tnode.nodeName.toLowerCase() === name :\n\t\t\t\t\t\t\t\t\t\tnode.nodeType === 1 ) &&\n\t\t\t\t\t\t\t\t\t\t++diff ) {\n\n\t\t\t\t\t\t\t\t\t\t// Cache the index of each encountered element\n\t\t\t\t\t\t\t\t\t\tif ( useCache ) {\n\t\t\t\t\t\t\t\t\t\t\touterCache = node[ expando ] || (node[ expando ] = {});\n\n\t\t\t\t\t\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\t\t\t\t\t\tuniqueCache = outerCache[ node.uniqueID ] ||\n\t\t\t\t\t\t\t\t\t\t\t\t(outerCache[ node.uniqueID ] = {});\n\n\t\t\t\t\t\t\t\t\t\t\tuniqueCache[ type ] = [ dirruns, diff ];\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\tif ( node === elem ) {\n\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Incorporate the offset, then check against cycle size\n\t\t\t\t\t\tdiff -= last;\n\t\t\t\t\t\treturn diff === first || ( diff % first === 0 && diff / first >= 0 );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t},\n\n\t\t\"PSEUDO\": function( pseudo, argument ) {\n\t\t\t// pseudo-class names are case-insensitive\n\t\t\t// http://www.w3.org/TR/selectors/#pseudo-classes\n\t\t\t// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters\n\t\t\t// Remember that setFilters inherits from pseudos\n\t\t\tvar args,\n\t\t\t\tfn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||\n\t\t\t\t\tSizzle.error( \"unsupported pseudo: \" + pseudo );\n\n\t\t\t// The user may use createPseudo to indicate that\n\t\t\t// arguments are needed to create the filter function\n\t\t\t// just as Sizzle does\n\t\t\tif ( fn[ expando ] ) {\n\t\t\t\treturn fn( argument );\n\t\t\t}\n\n\t\t\t// But maintain support for old signatures\n\t\t\tif ( fn.length > 1 ) {\n\t\t\t\targs = [ pseudo, pseudo, \"\", argument ];\n\t\t\t\treturn Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?\n\t\t\t\t\tmarkFunction(function( seed, matches ) {\n\t\t\t\t\t\tvar idx,\n\t\t\t\t\t\t\tmatched = fn( seed, argument ),\n\t\t\t\t\t\t\ti = matched.length;\n\t\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\t\tidx = indexOf( seed, matched[i] );\n\t\t\t\t\t\t\tseed[ idx ] = !( matches[ idx ] = matched[i] );\n\t\t\t\t\t\t}\n\t\t\t\t\t}) :\n\t\t\t\t\tfunction( elem ) {\n\t\t\t\t\t\treturn fn( elem, 0, args );\n\t\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn fn;\n\t\t}\n\t},\n\n\tpseudos: {\n\t\t// Potentially complex pseudos\n\t\t\"not\": markFunction(function( selector ) {\n\t\t\t// Trim the selector passed to compile\n\t\t\t// to avoid treating leading and trailing\n\t\t\t// spaces as combinators\n\t\t\tvar input = [],\n\t\t\t\tresults = [],\n\t\t\t\tmatcher = compile( selector.replace( rtrim, \"$1\" ) );\n\n\t\t\treturn matcher[ expando ] ?\n\t\t\t\tmarkFunction(function( seed, matches, context, xml ) {\n\t\t\t\t\tvar elem,\n\t\t\t\t\t\tunmatched = matcher( seed, null, xml, [] ),\n\t\t\t\t\t\ti = seed.length;\n\n\t\t\t\t\t// Match elements unmatched by `matcher`\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( (elem = unmatched[i]) ) {\n\t\t\t\t\t\t\tseed[i] = !(matches[i] = elem);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}) :\n\t\t\t\tfunction( elem, context, xml ) {\n\t\t\t\t\tinput[0] = elem;\n\t\t\t\t\tmatcher( input, null, xml, results );\n\t\t\t\t\t// Don't keep the element (issue #299)\n\t\t\t\t\tinput[0] = null;\n\t\t\t\t\treturn !results.pop();\n\t\t\t\t};\n\t\t}),\n\n\t\t\"has\": markFunction(function( selector ) {\n\t\t\treturn function( elem ) {\n\t\t\t\treturn Sizzle( selector, elem ).length > 0;\n\t\t\t};\n\t\t}),\n\n\t\t\"contains\": markFunction(function( text ) {\n\t\t\ttext = text.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\treturn ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;\n\t\t\t};\n\t\t}),\n\n\t\t// \"Whether an element is represented by a :lang() selector\n\t\t// is based solely on the element's language value\n\t\t// being equal to the identifier C,\n\t\t// or beginning with the identifier C immediately followed by \"-\".\n\t\t// The matching of C against the element's language value is performed case-insensitively.\n\t\t// The identifier C does not have to be a valid language name.\"\n\t\t// http://www.w3.org/TR/selectors/#lang-pseudo\n\t\t\"lang\": markFunction( function( lang ) {\n\t\t\t// lang value must be a valid identifier\n\t\t\tif ( !ridentifier.test(lang || \"\") ) {\n\t\t\t\tSizzle.error( \"unsupported lang: \" + lang );\n\t\t\t}\n\t\t\tlang = lang.replace( runescape, funescape ).toLowerCase();\n\t\t\treturn function( elem ) {\n\t\t\t\tvar elemLang;\n\t\t\t\tdo {\n\t\t\t\t\tif ( (elemLang = documentIsHTML ?\n\t\t\t\t\t\telem.lang :\n\t\t\t\t\t\telem.getAttribute(\"xml:lang\") || elem.getAttribute(\"lang\")) ) {\n\n\t\t\t\t\t\telemLang = elemLang.toLowerCase();\n\t\t\t\t\t\treturn elemLang === lang || elemLang.indexOf( lang + \"-\" ) === 0;\n\t\t\t\t\t}\n\t\t\t\t} while ( (elem = elem.parentNode) && elem.nodeType === 1 );\n\t\t\t\treturn false;\n\t\t\t};\n\t\t}),\n\n\t\t// Miscellaneous\n\t\t\"target\": function( elem ) {\n\t\t\tvar hash = window.location && window.location.hash;\n\t\t\treturn hash && hash.slice( 1 ) === elem.id;\n\t\t},\n\n\t\t\"root\": function( elem ) {\n\t\t\treturn elem === docElem;\n\t\t},\n\n\t\t\"focus\": function( elem ) {\n\t\t\treturn elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);\n\t\t},\n\n\t\t// Boolean properties\n\t\t\"enabled\": createDisabledPseudo( false ),\n\t\t\"disabled\": createDisabledPseudo( true ),\n\n\t\t\"checked\": function( elem ) {\n\t\t\t// In CSS3, :checked should return both checked and selected elements\n\t\t\t// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n\t\t\tvar nodeName = elem.nodeName.toLowerCase();\n\t\t\treturn (nodeName === \"input\" && !!elem.checked) || (nodeName === \"option\" && !!elem.selected);\n\t\t},\n\n\t\t\"selected\": function( elem ) {\n\t\t\t// Accessing this property makes selected-by-default\n\t\t\t// options in Safari work properly\n\t\t\tif ( elem.parentNode ) {\n\t\t\t\telem.parentNode.selectedIndex;\n\t\t\t}\n\n\t\t\treturn elem.selected === true;\n\t\t},\n\n\t\t// Contents\n\t\t\"empty\": function( elem ) {\n\t\t\t// http://www.w3.org/TR/selectors/#empty-pseudo\n\t\t\t// :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),\n\t\t\t//   but not by others (comment: 8; processing instruction: 7; etc.)\n\t\t\t// nodeType < 6 works because attributes (2) do not appear as children\n\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n\t\t\t\tif ( elem.nodeType < 6 ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\n\t\t\"parent\": function( elem ) {\n\t\t\treturn !Expr.pseudos[\"empty\"]( elem );\n\t\t},\n\n\t\t// Element/input types\n\t\t\"header\": function( elem ) {\n\t\t\treturn rheader.test( elem.nodeName );\n\t\t},\n\n\t\t\"input\": function( elem ) {\n\t\t\treturn rinputs.test( elem.nodeName );\n\t\t},\n\n\t\t\"button\": function( elem ) {\n\t\t\tvar name = elem.nodeName.toLowerCase();\n\t\t\treturn name === \"input\" && elem.type === \"button\" || name === \"button\";\n\t\t},\n\n\t\t\"text\": function( elem ) {\n\t\t\tvar attr;\n\t\t\treturn elem.nodeName.toLowerCase() === \"input\" &&\n\t\t\t\telem.type === \"text\" &&\n\n\t\t\t\t// Support: IE<8\n\t\t\t\t// New HTML5 attribute values (e.g., \"search\") appear with elem.type === \"text\"\n\t\t\t\t( (attr = elem.getAttribute(\"type\")) == null || attr.toLowerCase() === \"text\" );\n\t\t},\n\n\t\t// Position-in-collection\n\t\t\"first\": createPositionalPseudo(function() {\n\t\t\treturn [ 0 ];\n\t\t}),\n\n\t\t\"last\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\treturn [ length - 1 ];\n\t\t}),\n\n\t\t\"eq\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\treturn [ argument < 0 ? argument + length : argument ];\n\t\t}),\n\n\t\t\"even\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\tvar i = 0;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"odd\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\tvar i = 1;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"lt\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\tvar i = argument < 0 ? argument + length : argument;\n\t\t\tfor ( ; --i >= 0; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"gt\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\tvar i = argument < 0 ? argument + length : argument;\n\t\t\tfor ( ; ++i < length; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t})\n\t}\n};\n\nExpr.pseudos[\"nth\"] = Expr.pseudos[\"eq\"];\n\n// Add button/input type pseudos\nfor ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {\n\tExpr.pseudos[ i ] = createInputPseudo( i );\n}\nfor ( i in { submit: true, reset: true } ) {\n\tExpr.pseudos[ i ] = createButtonPseudo( i );\n}\n\n// Easy API for creating new setFilters\nfunction setFilters() {}\nsetFilters.prototype = Expr.filters = Expr.pseudos;\nExpr.setFilters = new setFilters();\n\ntokenize = Sizzle.tokenize = function( selector, parseOnly ) {\n\tvar matched, match, tokens, type,\n\t\tsoFar, groups, preFilters,\n\t\tcached = tokenCache[ selector + \" \" ];\n\n\tif ( cached ) {\n\t\treturn parseOnly ? 0 : cached.slice( 0 );\n\t}\n\n\tsoFar = selector;\n\tgroups = [];\n\tpreFilters = Expr.preFilter;\n\n\twhile ( soFar ) {\n\n\t\t// Comma and first run\n\t\tif ( !matched || (match = rcomma.exec( soFar )) ) {\n\t\t\tif ( match ) {\n\t\t\t\t// Don't consume trailing commas as valid\n\t\t\t\tsoFar = soFar.slice( match[0].length ) || soFar;\n\t\t\t}\n\t\t\tgroups.push( (tokens = []) );\n\t\t}\n\n\t\tmatched = false;\n\n\t\t// Combinators\n\t\tif ( (match = rcombinators.exec( soFar )) ) {\n\t\t\tmatched = match.shift();\n\t\t\ttokens.push({\n\t\t\t\tvalue: matched,\n\t\t\t\t// Cast descendant combinators to space\n\t\t\t\ttype: match[0].replace( rtrim, \" \" )\n\t\t\t});\n\t\t\tsoFar = soFar.slice( matched.length );\n\t\t}\n\n\t\t// Filters\n\t\tfor ( type in Expr.filter ) {\n\t\t\tif ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||\n\t\t\t\t(match = preFilters[ type ]( match ))) ) {\n\t\t\t\tmatched = match.shift();\n\t\t\t\ttokens.push({\n\t\t\t\t\tvalue: matched,\n\t\t\t\t\ttype: type,\n\t\t\t\t\tmatches: match\n\t\t\t\t});\n\t\t\t\tsoFar = soFar.slice( matched.length );\n\t\t\t}\n\t\t}\n\n\t\tif ( !matched ) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Return the length of the invalid excess\n\t// if we're just parsing\n\t// Otherwise, throw an error or return tokens\n\treturn parseOnly ?\n\t\tsoFar.length :\n\t\tsoFar ?\n\t\t\tSizzle.error( selector ) :\n\t\t\t// Cache the tokens\n\t\t\ttokenCache( selector, groups ).slice( 0 );\n};\n\nfunction toSelector( tokens ) {\n\tvar i = 0,\n\t\tlen = tokens.length,\n\t\tselector = \"\";\n\tfor ( ; i < len; i++ ) {\n\t\tselector += tokens[i].value;\n\t}\n\treturn selector;\n}\n\nfunction addCombinator( matcher, combinator, base ) {\n\tvar dir = combinator.dir,\n\t\tskip = combinator.next,\n\t\tkey = skip || dir,\n\t\tcheckNonElements = base && key === \"parentNode\",\n\t\tdoneName = done++;\n\n\treturn combinator.first ?\n\t\t// Check against closest ancestor/preceding element\n\t\tfunction( elem, context, xml ) {\n\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\treturn matcher( elem, context, xml );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t} :\n\n\t\t// Check against all ancestor/preceding elements\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar oldCache, uniqueCache, outerCache,\n\t\t\t\tnewCache = [ dirruns, doneName ];\n\n\t\t\t// We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching\n\t\t\tif ( xml ) {\n\t\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\tif ( matcher( elem, context, xml ) ) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\touterCache = elem[ expando ] || (elem[ expando ] = {});\n\n\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\tuniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {});\n\n\t\t\t\t\t\tif ( skip && skip === elem.nodeName.toLowerCase() ) {\n\t\t\t\t\t\t\telem = elem[ dir ] || elem;\n\t\t\t\t\t\t} else if ( (oldCache = uniqueCache[ key ]) &&\n\t\t\t\t\t\t\toldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {\n\n\t\t\t\t\t\t\t// Assign to newCache so results back-propagate to previous elements\n\t\t\t\t\t\t\treturn (newCache[ 2 ] = oldCache[ 2 ]);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Reuse newcache so results back-propagate to previous elements\n\t\t\t\t\t\t\tuniqueCache[ key ] = newCache;\n\n\t\t\t\t\t\t\t// A match means we're done; a fail means we have to keep checking\n\t\t\t\t\t\t\tif ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {\n\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n}\n\nfunction elementMatcher( matchers ) {\n\treturn matchers.length > 1 ?\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar i = matchers.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( !matchers[i]( elem, context, xml ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t} :\n\t\tmatchers[0];\n}\n\nfunction multipleContexts( selector, contexts, results ) {\n\tvar i = 0,\n\t\tlen = contexts.length;\n\tfor ( ; i < len; i++ ) {\n\t\tSizzle( selector, contexts[i], results );\n\t}\n\treturn results;\n}\n\nfunction condense( unmatched, map, filter, context, xml ) {\n\tvar elem,\n\t\tnewUnmatched = [],\n\t\ti = 0,\n\t\tlen = unmatched.length,\n\t\tmapped = map != null;\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( (elem = unmatched[i]) ) {\n\t\t\tif ( !filter || filter( elem, context, xml ) ) {\n\t\t\t\tnewUnmatched.push( elem );\n\t\t\t\tif ( mapped ) {\n\t\t\t\t\tmap.push( i );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn newUnmatched;\n}\n\nfunction setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {\n\tif ( postFilter && !postFilter[ expando ] ) {\n\t\tpostFilter = setMatcher( postFilter );\n\t}\n\tif ( postFinder && !postFinder[ expando ] ) {\n\t\tpostFinder = setMatcher( postFinder, postSelector );\n\t}\n\treturn markFunction(function( seed, results, context, xml ) {\n\t\tvar temp, i, elem,\n\t\t\tpreMap = [],\n\t\t\tpostMap = [],\n\t\t\tpreexisting = results.length,\n\n\t\t\t// Get initial elements from seed or context\n\t\t\telems = seed || multipleContexts( selector || \"*\", context.nodeType ? [ context ] : context, [] ),\n\n\t\t\t// Prefilter to get matcher input, preserving a map for seed-results synchronization\n\t\t\tmatcherIn = preFilter && ( seed || !selector ) ?\n\t\t\t\tcondense( elems, preMap, preFilter, context, xml ) :\n\t\t\t\telems,\n\n\t\t\tmatcherOut = matcher ?\n\t\t\t\t// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,\n\t\t\t\tpostFinder || ( seed ? preFilter : preexisting || postFilter ) ?\n\n\t\t\t\t\t// ...intermediate processing is necessary\n\t\t\t\t\t[] :\n\n\t\t\t\t\t// ...otherwise use results directly\n\t\t\t\t\tresults :\n\t\t\t\tmatcherIn;\n\n\t\t// Find primary matches\n\t\tif ( matcher ) {\n\t\t\tmatcher( matcherIn, matcherOut, context, xml );\n\t\t}\n\n\t\t// Apply postFilter\n\t\tif ( postFilter ) {\n\t\t\ttemp = condense( matcherOut, postMap );\n\t\t\tpostFilter( temp, [], context, xml );\n\n\t\t\t// Un-match failing elements by moving them back to matcherIn\n\t\t\ti = temp.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( (elem = temp[i]) ) {\n\t\t\t\t\tmatcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( seed ) {\n\t\t\tif ( postFinder || preFilter ) {\n\t\t\t\tif ( postFinder ) {\n\t\t\t\t\t// Get the final matcherOut by condensing this intermediate into postFinder contexts\n\t\t\t\t\ttemp = [];\n\t\t\t\t\ti = matcherOut.length;\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( (elem = matcherOut[i]) ) {\n\t\t\t\t\t\t\t// Restore matcherIn since elem is not yet a final match\n\t\t\t\t\t\t\ttemp.push( (matcherIn[i] = elem) );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpostFinder( null, (matcherOut = []), temp, xml );\n\t\t\t\t}\n\n\t\t\t\t// Move matched elements from seed to results to keep them synchronized\n\t\t\t\ti = matcherOut.length;\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\tif ( (elem = matcherOut[i]) &&\n\t\t\t\t\t\t(temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) {\n\n\t\t\t\t\t\tseed[temp] = !(results[temp] = elem);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Add elements to results, through postFinder if defined\n\t\t} else {\n\t\t\tmatcherOut = condense(\n\t\t\t\tmatcherOut === results ?\n\t\t\t\t\tmatcherOut.splice( preexisting, matcherOut.length ) :\n\t\t\t\t\tmatcherOut\n\t\t\t);\n\t\t\tif ( postFinder ) {\n\t\t\t\tpostFinder( null, results, matcherOut, xml );\n\t\t\t} else {\n\t\t\t\tpush.apply( results, matcherOut );\n\t\t\t}\n\t\t}\n\t});\n}\n\nfunction matcherFromTokens( tokens ) {\n\tvar checkContext, matcher, j,\n\t\tlen = tokens.length,\n\t\tleadingRelative = Expr.relative[ tokens[0].type ],\n\t\timplicitRelative = leadingRelative || Expr.relative[\" \"],\n\t\ti = leadingRelative ? 1 : 0,\n\n\t\t// The foundational matcher ensures that elements are reachable from top-level context(s)\n\t\tmatchContext = addCombinator( function( elem ) {\n\t\t\treturn elem === checkContext;\n\t\t}, implicitRelative, true ),\n\t\tmatchAnyContext = addCombinator( function( elem ) {\n\t\t\treturn indexOf( checkContext, elem ) > -1;\n\t\t}, implicitRelative, true ),\n\t\tmatchers = [ function( elem, context, xml ) {\n\t\t\tvar ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (\n\t\t\t\t(checkContext = context).nodeType ?\n\t\t\t\t\tmatchContext( elem, context, xml ) :\n\t\t\t\t\tmatchAnyContext( elem, context, xml ) );\n\t\t\t// Avoid hanging onto element (issue #299)\n\t\t\tcheckContext = null;\n\t\t\treturn ret;\n\t\t} ];\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( (matcher = Expr.relative[ tokens[i].type ]) ) {\n\t\t\tmatchers = [ addCombinator(elementMatcher( matchers ), matcher) ];\n\t\t} else {\n\t\t\tmatcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );\n\n\t\t\t// Return special upon seeing a positional matcher\n\t\t\tif ( matcher[ expando ] ) {\n\t\t\t\t// Find the next relative operator (if any) for proper handling\n\t\t\t\tj = ++i;\n\t\t\t\tfor ( ; j < len; j++ ) {\n\t\t\t\t\tif ( Expr.relative[ tokens[j].type ] ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn setMatcher(\n\t\t\t\t\ti > 1 && elementMatcher( matchers ),\n\t\t\t\t\ti > 1 && toSelector(\n\t\t\t\t\t\t// If the preceding token was a descendant combinator, insert an implicit any-element `*`\n\t\t\t\t\t\ttokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === \" \" ? \"*\" : \"\" })\n\t\t\t\t\t).replace( rtrim, \"$1\" ),\n\t\t\t\t\tmatcher,\n\t\t\t\t\ti < j && matcherFromTokens( tokens.slice( i, j ) ),\n\t\t\t\t\tj < len && matcherFromTokens( (tokens = tokens.slice( j )) ),\n\t\t\t\t\tj < len && toSelector( tokens )\n\t\t\t\t);\n\t\t\t}\n\t\t\tmatchers.push( matcher );\n\t\t}\n\t}\n\n\treturn elementMatcher( matchers );\n}\n\nfunction matcherFromGroupMatchers( elementMatchers, setMatchers ) {\n\tvar bySet = setMatchers.length > 0,\n\t\tbyElement = elementMatchers.length > 0,\n\t\tsuperMatcher = function( seed, context, xml, results, outermost ) {\n\t\t\tvar elem, j, matcher,\n\t\t\t\tmatchedCount = 0,\n\t\t\t\ti = \"0\",\n\t\t\t\tunmatched = seed && [],\n\t\t\t\tsetMatched = [],\n\t\t\t\tcontextBackup = outermostContext,\n\t\t\t\t// We must always have either seed elements or outermost context\n\t\t\t\telems = seed || byElement && Expr.find[\"TAG\"]( \"*\", outermost ),\n\t\t\t\t// Use integer dirruns iff this is the outermost matcher\n\t\t\t\tdirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),\n\t\t\t\tlen = elems.length;\n\n\t\t\tif ( outermost ) {\n\t\t\t\toutermostContext = context === document || context || outermost;\n\t\t\t}\n\n\t\t\t// Add elements passing elementMatchers directly to results\n\t\t\t// Support: IE<9, Safari\n\t\t\t// Tolerate NodeList properties (IE: \"length\"; Safari: <number>) matching elements by id\n\t\t\tfor ( ; i !== len && (elem = elems[i]) != null; i++ ) {\n\t\t\t\tif ( byElement && elem ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\tif ( !context && elem.ownerDocument !== document ) {\n\t\t\t\t\t\tsetDocument( elem );\n\t\t\t\t\t\txml = !documentIsHTML;\n\t\t\t\t\t}\n\t\t\t\t\twhile ( (matcher = elementMatchers[j++]) ) {\n\t\t\t\t\t\tif ( matcher( elem, context || document, xml) ) {\n\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( outermost ) {\n\t\t\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Track unmatched elements for set filters\n\t\t\t\tif ( bySet ) {\n\t\t\t\t\t// They will have gone through all possible matchers\n\t\t\t\t\tif ( (elem = !matcher && elem) ) {\n\t\t\t\t\t\tmatchedCount--;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Lengthen the array for every element, matched or not\n\t\t\t\t\tif ( seed ) {\n\t\t\t\t\t\tunmatched.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// `i` is now the count of elements visited above, and adding it to `matchedCount`\n\t\t\t// makes the latter nonnegative.\n\t\t\tmatchedCount += i;\n\n\t\t\t// Apply set filters to unmatched elements\n\t\t\t// NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`\n\t\t\t// equals `i`), unless we didn't visit _any_ elements in the above loop because we have\n\t\t\t// no element matchers and no seed.\n\t\t\t// Incrementing an initially-string \"0\" `i` allows `i` to remain a string only in that\n\t\t\t// case, which will result in a \"00\" `matchedCount` that differs from `i` but is also\n\t\t\t// numerically zero.\n\t\t\tif ( bySet && i !== matchedCount ) {\n\t\t\t\tj = 0;\n\t\t\t\twhile ( (matcher = setMatchers[j++]) ) {\n\t\t\t\t\tmatcher( unmatched, setMatched, context, xml );\n\t\t\t\t}\n\n\t\t\t\tif ( seed ) {\n\t\t\t\t\t// Reintegrate element matches to eliminate the need for sorting\n\t\t\t\t\tif ( matchedCount > 0 ) {\n\t\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\t\tif ( !(unmatched[i] || setMatched[i]) ) {\n\t\t\t\t\t\t\t\tsetMatched[i] = pop.call( results );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Discard index placeholder values to get only actual matches\n\t\t\t\t\tsetMatched = condense( setMatched );\n\t\t\t\t}\n\n\t\t\t\t// Add matches to results\n\t\t\t\tpush.apply( results, setMatched );\n\n\t\t\t\t// Seedless set matches succeeding multiple successful matchers stipulate sorting\n\t\t\t\tif ( outermost && !seed && setMatched.length > 0 &&\n\t\t\t\t\t( matchedCount + setMatchers.length ) > 1 ) {\n\n\t\t\t\t\tSizzle.uniqueSort( results );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Override manipulation of globals by nested matchers\n\t\t\tif ( outermost ) {\n\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\toutermostContext = contextBackup;\n\t\t\t}\n\n\t\t\treturn unmatched;\n\t\t};\n\n\treturn bySet ?\n\t\tmarkFunction( superMatcher ) :\n\t\tsuperMatcher;\n}\n\ncompile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {\n\tvar i,\n\t\tsetMatchers = [],\n\t\telementMatchers = [],\n\t\tcached = compilerCache[ selector + \" \" ];\n\n\tif ( !cached ) {\n\t\t// Generate a function of recursive functions that can be used to check each element\n\t\tif ( !match ) {\n\t\t\tmatch = tokenize( selector );\n\t\t}\n\t\ti = match.length;\n\t\twhile ( i-- ) {\n\t\t\tcached = matcherFromTokens( match[i] );\n\t\t\tif ( cached[ expando ] ) {\n\t\t\t\tsetMatchers.push( cached );\n\t\t\t} else {\n\t\t\t\telementMatchers.push( cached );\n\t\t\t}\n\t\t}\n\n\t\t// Cache the compiled function\n\t\tcached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );\n\n\t\t// Save selector and tokenization\n\t\tcached.selector = selector;\n\t}\n\treturn cached;\n};\n\n/**\n * A low-level selection function that works with Sizzle's compiled\n *  selector functions\n * @param {String|Function} selector A selector or a pre-compiled\n *  selector function built with Sizzle.compile\n * @param {Element} context\n * @param {Array} [results]\n * @param {Array} [seed] A set of elements to match against\n */\nselect = Sizzle.select = function( selector, context, results, seed ) {\n\tvar i, tokens, token, type, find,\n\t\tcompiled = typeof selector === \"function\" && selector,\n\t\tmatch = !seed && tokenize( (selector = compiled.selector || selector) );\n\n\tresults = results || [];\n\n\t// Try to minimize operations if there is only one selector in the list and no seed\n\t// (the latter of which guarantees us context)\n\tif ( match.length === 1 ) {\n\n\t\t// Reduce context if the leading compound selector is an ID\n\t\ttokens = match[0] = match[0].slice( 0 );\n\t\tif ( tokens.length > 2 && (token = tokens[0]).type === \"ID\" &&\n\t\t\t\tcontext.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[1].type ] ) {\n\n\t\t\tcontext = ( Expr.find[\"ID\"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];\n\t\t\tif ( !context ) {\n\t\t\t\treturn results;\n\n\t\t\t// Precompiled matchers will still verify ancestry, so step up a level\n\t\t\t} else if ( compiled ) {\n\t\t\t\tcontext = context.parentNode;\n\t\t\t}\n\n\t\t\tselector = selector.slice( tokens.shift().value.length );\n\t\t}\n\n\t\t// Fetch a seed set for right-to-left matching\n\t\ti = matchExpr[\"needsContext\"].test( selector ) ? 0 : tokens.length;\n\t\twhile ( i-- ) {\n\t\t\ttoken = tokens[i];\n\n\t\t\t// Abort if we hit a combinator\n\t\t\tif ( Expr.relative[ (type = token.type) ] ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( (find = Expr.find[ type ]) ) {\n\t\t\t\t// Search, expanding context for leading sibling combinators\n\t\t\t\tif ( (seed = find(\n\t\t\t\t\ttoken.matches[0].replace( runescape, funescape ),\n\t\t\t\t\trsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context\n\t\t\t\t)) ) {\n\n\t\t\t\t\t// If seed is empty or no tokens remain, we can return early\n\t\t\t\t\ttokens.splice( i, 1 );\n\t\t\t\t\tselector = seed.length && toSelector( tokens );\n\t\t\t\t\tif ( !selector ) {\n\t\t\t\t\t\tpush.apply( results, seed );\n\t\t\t\t\t\treturn results;\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Compile and execute a filtering function if one is not provided\n\t// Provide `match` to avoid retokenization if we modified the selector above\n\t( compiled || compile( selector, match ) )(\n\t\tseed,\n\t\tcontext,\n\t\t!documentIsHTML,\n\t\tresults,\n\t\t!context || rsibling.test( selector ) && testContext( context.parentNode ) || context\n\t);\n\treturn results;\n};\n\n// One-time assignments\n\n// Sort stability\nsupport.sortStable = expando.split(\"\").sort( sortOrder ).join(\"\") === expando;\n\n// Support: Chrome 14-35+\n// Always assume duplicates if they aren't passed to the comparison function\nsupport.detectDuplicates = !!hasDuplicate;\n\n// Initialize against the default document\nsetDocument();\n\n// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)\n// Detached nodes confoundingly follow *each other*\nsupport.sortDetached = assert(function( el ) {\n\t// Should return 1, but returns 4 (following)\n\treturn el.compareDocumentPosition( document.createElement(\"fieldset\") ) & 1;\n});\n\n// Support: IE<8\n// Prevent attribute/property \"interpolation\"\n// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx\nif ( !assert(function( el ) {\n\tel.innerHTML = \"<a href='#'></a>\";\n\treturn el.firstChild.getAttribute(\"href\") === \"#\" ;\n}) ) {\n\taddHandle( \"type|href|height|width\", function( elem, name, isXML ) {\n\t\tif ( !isXML ) {\n\t\t\treturn elem.getAttribute( name, name.toLowerCase() === \"type\" ? 1 : 2 );\n\t\t}\n\t});\n}\n\n// Support: IE<9\n// Use defaultValue in place of getAttribute(\"value\")\nif ( !support.attributes || !assert(function( el ) {\n\tel.innerHTML = \"<input/>\";\n\tel.firstChild.setAttribute( \"value\", \"\" );\n\treturn el.firstChild.getAttribute( \"value\" ) === \"\";\n}) ) {\n\taddHandle( \"value\", function( elem, name, isXML ) {\n\t\tif ( !isXML && elem.nodeName.toLowerCase() === \"input\" ) {\n\t\t\treturn elem.defaultValue;\n\t\t}\n\t});\n}\n\n// Support: IE<9\n// Use getAttributeNode to fetch booleans when getAttribute lies\nif ( !assert(function( el ) {\n\treturn el.getAttribute(\"disabled\") == null;\n}) ) {\n\taddHandle( booleans, function( elem, name, isXML ) {\n\t\tvar val;\n\t\tif ( !isXML ) {\n\t\t\treturn elem[ name ] === true ? name.toLowerCase() :\n\t\t\t\t\t(val = elem.getAttributeNode( name )) && val.specified ?\n\t\t\t\t\tval.value :\n\t\t\t\tnull;\n\t\t}\n\t});\n}\n\nreturn Sizzle;\n\n})( window );\n\n\n\njQuery.find = Sizzle;\njQuery.expr = Sizzle.selectors;\n\n// Deprecated\njQuery.expr[ \":\" ] = jQuery.expr.pseudos;\njQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort;\njQuery.text = Sizzle.getText;\njQuery.isXMLDoc = Sizzle.isXML;\njQuery.contains = Sizzle.contains;\njQuery.escapeSelector = Sizzle.escape;\n\n\n\n\nvar dir = function( elem, dir, until ) {\n\tvar matched = [],\n\t\ttruncate = until !== undefined;\n\n\twhile ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {\n\t\tif ( elem.nodeType === 1 ) {\n\t\t\tif ( truncate && jQuery( elem ).is( until ) ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tmatched.push( elem );\n\t\t}\n\t}\n\treturn matched;\n};\n\n\nvar siblings = function( n, elem ) {\n\tvar matched = [];\n\n\tfor ( ; n; n = n.nextSibling ) {\n\t\tif ( n.nodeType === 1 && n !== elem ) {\n\t\t\tmatched.push( n );\n\t\t}\n\t}\n\n\treturn matched;\n};\n\n\nvar rneedsContext = jQuery.expr.match.needsContext;\n\n\n\nfunction nodeName( elem, name ) {\n\n  return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();\n\n};\nvar rsingleTag = ( /^<([a-z][^\\/\\0>:\\x20\\t\\r\\n\\f]*)[\\x20\\t\\r\\n\\f]*\\/?>(?:<\\/\\1>|)$/i );\n\n\n\n// Implement the identical functionality for filter and not\nfunction winnow( elements, qualifier, not ) {\n\tif ( isFunction( qualifier ) ) {\n\t\treturn jQuery.grep( elements, function( elem, i ) {\n\t\t\treturn !!qualifier.call( elem, i, elem ) !== not;\n\t\t} );\n\t}\n\n\t// Single element\n\tif ( qualifier.nodeType ) {\n\t\treturn jQuery.grep( elements, function( elem ) {\n\t\t\treturn ( elem === qualifier ) !== not;\n\t\t} );\n\t}\n\n\t// Arraylike of elements (jQuery, arguments, Array)\n\tif ( typeof qualifier !== \"string\" ) {\n\t\treturn jQuery.grep( elements, function( elem ) {\n\t\t\treturn ( indexOf.call( qualifier, elem ) > -1 ) !== not;\n\t\t} );\n\t}\n\n\t// Filtered directly for both simple and complex selectors\n\treturn jQuery.filter( qualifier, elements, not );\n}\n\njQuery.filter = function( expr, elems, not ) {\n\tvar elem = elems[ 0 ];\n\n\tif ( not ) {\n\t\texpr = \":not(\" + expr + \")\";\n\t}\n\n\tif ( elems.length === 1 && elem.nodeType === 1 ) {\n\t\treturn jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [];\n\t}\n\n\treturn jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {\n\t\treturn elem.nodeType === 1;\n\t} ) );\n};\n\njQuery.fn.extend( {\n\tfind: function( selector ) {\n\t\tvar i, ret,\n\t\t\tlen = this.length,\n\t\t\tself = this;\n\n\t\tif ( typeof selector !== \"string\" ) {\n\t\t\treturn this.pushStack( jQuery( selector ).filter( function() {\n\t\t\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\t\t\tif ( jQuery.contains( self[ i ], this ) ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} ) );\n\t\t}\n\n\t\tret = this.pushStack( [] );\n\n\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\tjQuery.find( selector, self[ i ], ret );\n\t\t}\n\n\t\treturn len > 1 ? jQuery.uniqueSort( ret ) : ret;\n\t},\n\tfilter: function( selector ) {\n\t\treturn this.pushStack( winnow( this, selector || [], false ) );\n\t},\n\tnot: function( selector ) {\n\t\treturn this.pushStack( winnow( this, selector || [], true ) );\n\t},\n\tis: function( selector ) {\n\t\treturn !!winnow(\n\t\t\tthis,\n\n\t\t\t// If this is a positional/relative selector, check membership in the returned set\n\t\t\t// so $(\"p:first\").is(\"p:last\") won't return true for a doc with two \"p\".\n\t\t\ttypeof selector === \"string\" && rneedsContext.test( selector ) ?\n\t\t\t\tjQuery( selector ) :\n\t\t\t\tselector || [],\n\t\t\tfalse\n\t\t).length;\n\t}\n} );\n\n\n// Initialize a jQuery object\n\n\n// A central reference to the root jQuery(document)\nvar rootjQuery,\n\n\t// A simple way to check for HTML strings\n\t// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)\n\t// Strict HTML recognition (#11290: must start with <)\n\t// Shortcut simple #id case for speed\n\trquickExpr = /^(?:\\s*(<[\\w\\W]+>)[^>]*|#([\\w-]+))$/,\n\n\tinit = jQuery.fn.init = function( selector, context, root ) {\n\t\tvar match, elem;\n\n\t\t// HANDLE: $(\"\"), $(null), $(undefined), $(false)\n\t\tif ( !selector ) {\n\t\t\treturn this;\n\t\t}\n\n\t\t// Method init() accepts an alternate rootjQuery\n\t\t// so migrate can support jQuery.sub (gh-2101)\n\t\troot = root || rootjQuery;\n\n\t\t// Handle HTML strings\n\t\tif ( typeof selector === \"string\" ) {\n\t\t\tif ( selector[ 0 ] === \"<\" &&\n\t\t\t\tselector[ selector.length - 1 ] === \">\" &&\n\t\t\t\tselector.length >= 3 ) {\n\n\t\t\t\t// Assume that strings that start and end with <> are HTML and skip the regex check\n\t\t\t\tmatch = [ null, selector, null ];\n\n\t\t\t} else {\n\t\t\t\tmatch = rquickExpr.exec( selector );\n\t\t\t}\n\n\t\t\t// Match html or make sure no context is specified for #id\n\t\t\tif ( match && ( match[ 1 ] || !context ) ) {\n\n\t\t\t\t// HANDLE: $(html) -> $(array)\n\t\t\t\tif ( match[ 1 ] ) {\n\t\t\t\t\tcontext = context instanceof jQuery ? context[ 0 ] : context;\n\n\t\t\t\t\t// Option to run scripts is true for back-compat\n\t\t\t\t\t// Intentionally let the error be thrown if parseHTML is not present\n\t\t\t\t\tjQuery.merge( this, jQuery.parseHTML(\n\t\t\t\t\t\tmatch[ 1 ],\n\t\t\t\t\t\tcontext && context.nodeType ? context.ownerDocument || context : document,\n\t\t\t\t\t\ttrue\n\t\t\t\t\t) );\n\n\t\t\t\t\t// HANDLE: $(html, props)\n\t\t\t\t\tif ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) {\n\t\t\t\t\t\tfor ( match in context ) {\n\n\t\t\t\t\t\t\t// Properties of context are called as methods if possible\n\t\t\t\t\t\t\tif ( isFunction( this[ match ] ) ) {\n\t\t\t\t\t\t\t\tthis[ match ]( context[ match ] );\n\n\t\t\t\t\t\t\t// ...and otherwise set as attributes\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tthis.attr( match, context[ match ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn this;\n\n\t\t\t\t// HANDLE: $(#id)\n\t\t\t\t} else {\n\t\t\t\t\telem = document.getElementById( match[ 2 ] );\n\n\t\t\t\t\tif ( elem ) {\n\n\t\t\t\t\t\t// Inject the element directly into the jQuery object\n\t\t\t\t\t\tthis[ 0 ] = elem;\n\t\t\t\t\t\tthis.length = 1;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\n\t\t\t// HANDLE: $(expr, $(...))\n\t\t\t} else if ( !context || context.jquery ) {\n\t\t\t\treturn ( context || root ).find( selector );\n\n\t\t\t// HANDLE: $(expr, context)\n\t\t\t// (which is just equivalent to: $(context).find(expr)\n\t\t\t} else {\n\t\t\t\treturn this.constructor( context ).find( selector );\n\t\t\t}\n\n\t\t// HANDLE: $(DOMElement)\n\t\t} else if ( selector.nodeType ) {\n\t\t\tthis[ 0 ] = selector;\n\t\t\tthis.length = 1;\n\t\t\treturn this;\n\n\t\t// HANDLE: $(function)\n\t\t// Shortcut for document ready\n\t\t} else if ( isFunction( selector ) ) {\n\t\t\treturn root.ready !== undefined ?\n\t\t\t\troot.ready( selector ) :\n\n\t\t\t\t// Execute immediately if ready is not present\n\t\t\t\tselector( jQuery );\n\t\t}\n\n\t\treturn jQuery.makeArray( selector, this );\n\t};\n\n// Give the init function the jQuery prototype for later instantiation\ninit.prototype = jQuery.fn;\n\n// Initialize central reference\nrootjQuery = jQuery( document );\n\n\nvar rparentsprev = /^(?:parents|prev(?:Until|All))/,\n\n\t// Methods guaranteed to produce a unique set when starting from a unique set\n\tguaranteedUnique = {\n\t\tchildren: true,\n\t\tcontents: true,\n\t\tnext: true,\n\t\tprev: true\n\t};\n\njQuery.fn.extend( {\n\thas: function( target ) {\n\t\tvar targets = jQuery( target, this ),\n\t\t\tl = targets.length;\n\n\t\treturn this.filter( function() {\n\t\t\tvar i = 0;\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tif ( jQuery.contains( this, targets[ i ] ) ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t},\n\n\tclosest: function( selectors, context ) {\n\t\tvar cur,\n\t\t\ti = 0,\n\t\t\tl = this.length,\n\t\t\tmatched = [],\n\t\t\ttargets = typeof selectors !== \"string\" && jQuery( selectors );\n\n\t\t// Positional selectors never match, since there's no _selection_ context\n\t\tif ( !rneedsContext.test( selectors ) ) {\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tfor ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) {\n\n\t\t\t\t\t// Always skip document fragments\n\t\t\t\t\tif ( cur.nodeType < 11 && ( targets ?\n\t\t\t\t\t\ttargets.index( cur ) > -1 :\n\n\t\t\t\t\t\t// Don't pass non-elements to Sizzle\n\t\t\t\t\t\tcur.nodeType === 1 &&\n\t\t\t\t\t\t\tjQuery.find.matchesSelector( cur, selectors ) ) ) {\n\n\t\t\t\t\t\tmatched.push( cur );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched );\n\t},\n\n\t// Determine the position of an element within the set\n\tindex: function( elem ) {\n\n\t\t// No argument, return index in parent\n\t\tif ( !elem ) {\n\t\t\treturn ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;\n\t\t}\n\n\t\t// Index in selector\n\t\tif ( typeof elem === \"string\" ) {\n\t\t\treturn indexOf.call( jQuery( elem ), this[ 0 ] );\n\t\t}\n\n\t\t// Locate the position of the desired element\n\t\treturn indexOf.call( this,\n\n\t\t\t// If it receives a jQuery object, the first element is used\n\t\t\telem.jquery ? elem[ 0 ] : elem\n\t\t);\n\t},\n\n\tadd: function( selector, context ) {\n\t\treturn this.pushStack(\n\t\t\tjQuery.uniqueSort(\n\t\t\t\tjQuery.merge( this.get(), jQuery( selector, context ) )\n\t\t\t)\n\t\t);\n\t},\n\n\taddBack: function( selector ) {\n\t\treturn this.add( selector == null ?\n\t\t\tthis.prevObject : this.prevObject.filter( selector )\n\t\t);\n\t}\n} );\n\nfunction sibling( cur, dir ) {\n\twhile ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {}\n\treturn cur;\n}\n\njQuery.each( {\n\tparent: function( elem ) {\n\t\tvar parent = elem.parentNode;\n\t\treturn parent && parent.nodeType !== 11 ? parent : null;\n\t},\n\tparents: function( elem ) {\n\t\treturn dir( elem, \"parentNode\" );\n\t},\n\tparentsUntil: function( elem, i, until ) {\n\t\treturn dir( elem, \"parentNode\", until );\n\t},\n\tnext: function( elem ) {\n\t\treturn sibling( elem, \"nextSibling\" );\n\t},\n\tprev: function( elem ) {\n\t\treturn sibling( elem, \"previousSibling\" );\n\t},\n\tnextAll: function( elem ) {\n\t\treturn dir( elem, \"nextSibling\" );\n\t},\n\tprevAll: function( elem ) {\n\t\treturn dir( elem, \"previousSibling\" );\n\t},\n\tnextUntil: function( elem, i, until ) {\n\t\treturn dir( elem, \"nextSibling\", until );\n\t},\n\tprevUntil: function( elem, i, until ) {\n\t\treturn dir( elem, \"previousSibling\", until );\n\t},\n\tsiblings: function( elem ) {\n\t\treturn siblings( ( elem.parentNode || {} ).firstChild, elem );\n\t},\n\tchildren: function( elem ) {\n\t\treturn siblings( elem.firstChild );\n\t},\n\tcontents: function( elem ) {\n        if ( nodeName( elem, \"iframe\" ) ) {\n            return elem.contentDocument;\n        }\n\n        // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only\n        // Treat the template element as a regular one in browsers that\n        // don't support it.\n        if ( nodeName( elem, \"template\" ) ) {\n            elem = elem.content || elem;\n        }\n\n        return jQuery.merge( [], elem.childNodes );\n\t}\n}, function( name, fn ) {\n\tjQuery.fn[ name ] = function( until, selector ) {\n\t\tvar matched = jQuery.map( this, fn, until );\n\n\t\tif ( name.slice( -5 ) !== \"Until\" ) {\n\t\t\tselector = until;\n\t\t}\n\n\t\tif ( selector && typeof selector === \"string\" ) {\n\t\t\tmatched = jQuery.filter( selector, matched );\n\t\t}\n\n\t\tif ( this.length > 1 ) {\n\n\t\t\t// Remove duplicates\n\t\t\tif ( !guaranteedUnique[ name ] ) {\n\t\t\t\tjQuery.uniqueSort( matched );\n\t\t\t}\n\n\t\t\t// Reverse order for parents* and prev-derivatives\n\t\t\tif ( rparentsprev.test( name ) ) {\n\t\t\t\tmatched.reverse();\n\t\t\t}\n\t\t}\n\n\t\treturn this.pushStack( matched );\n\t};\n} );\nvar rnothtmlwhite = ( /[^\\x20\\t\\r\\n\\f]+/g );\n\n\n\n// Convert String-formatted options into Object-formatted ones\nfunction createOptions( options ) {\n\tvar object = {};\n\tjQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) {\n\t\tobject[ flag ] = true;\n\t} );\n\treturn object;\n}\n\n/*\n * Create a callback list using the following parameters:\n *\n *\toptions: an optional list of space-separated options that will change how\n *\t\t\tthe callback list behaves or a more traditional option object\n *\n * By default a callback list will act like an event callback list and can be\n * \"fired\" multiple times.\n *\n * Possible options:\n *\n *\tonce:\t\t\twill ensure the callback list can only be fired once (like a Deferred)\n *\n *\tmemory:\t\t\twill keep track of previous values and will call any callback added\n *\t\t\t\t\tafter the list has been fired right away with the latest \"memorized\"\n *\t\t\t\t\tvalues (like a Deferred)\n *\n *\tunique:\t\t\twill ensure a callback can only be added once (no duplicate in the list)\n *\n *\tstopOnFalse:\tinterrupt callings when a callback returns false\n *\n */\njQuery.Callbacks = function( options ) {\n\n\t// Convert options from String-formatted to Object-formatted if needed\n\t// (we check in cache first)\n\toptions = typeof options === \"string\" ?\n\t\tcreateOptions( options ) :\n\t\tjQuery.extend( {}, options );\n\n\tvar // Flag to know if list is currently firing\n\t\tfiring,\n\n\t\t// Last fire value for non-forgettable lists\n\t\tmemory,\n\n\t\t// Flag to know if list was already fired\n\t\tfired,\n\n\t\t// Flag to prevent firing\n\t\tlocked,\n\n\t\t// Actual callback list\n\t\tlist = [],\n\n\t\t// Queue of execution data for repeatable lists\n\t\tqueue = [],\n\n\t\t// Index of currently firing callback (modified by add/remove as needed)\n\t\tfiringIndex = -1,\n\n\t\t// Fire callbacks\n\t\tfire = function() {\n\n\t\t\t// Enforce single-firing\n\t\t\tlocked = locked || options.once;\n\n\t\t\t// Execute callbacks for all pending executions,\n\t\t\t// respecting firingIndex overrides and runtime changes\n\t\t\tfired = firing = true;\n\t\t\tfor ( ; queue.length; firingIndex = -1 ) {\n\t\t\t\tmemory = queue.shift();\n\t\t\t\twhile ( ++firingIndex < list.length ) {\n\n\t\t\t\t\t// Run callback and check for early termination\n\t\t\t\t\tif ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&\n\t\t\t\t\t\toptions.stopOnFalse ) {\n\n\t\t\t\t\t\t// Jump to end and forget the data so .add doesn't re-fire\n\t\t\t\t\t\tfiringIndex = list.length;\n\t\t\t\t\t\tmemory = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Forget the data if we're done with it\n\t\t\tif ( !options.memory ) {\n\t\t\t\tmemory = false;\n\t\t\t}\n\n\t\t\tfiring = false;\n\n\t\t\t// Clean up if we're done firing for good\n\t\t\tif ( locked ) {\n\n\t\t\t\t// Keep an empty list if we have data for future add calls\n\t\t\t\tif ( memory ) {\n\t\t\t\t\tlist = [];\n\n\t\t\t\t// Otherwise, this object is spent\n\t\t\t\t} else {\n\t\t\t\t\tlist = \"\";\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t// Actual Callbacks object\n\t\tself = {\n\n\t\t\t// Add a callback or a collection of callbacks to the list\n\t\t\tadd: function() {\n\t\t\t\tif ( list ) {\n\n\t\t\t\t\t// If we have memory from a past run, we should fire after adding\n\t\t\t\t\tif ( memory && !firing ) {\n\t\t\t\t\t\tfiringIndex = list.length - 1;\n\t\t\t\t\t\tqueue.push( memory );\n\t\t\t\t\t}\n\n\t\t\t\t\t( function add( args ) {\n\t\t\t\t\t\tjQuery.each( args, function( _, arg ) {\n\t\t\t\t\t\t\tif ( isFunction( arg ) ) {\n\t\t\t\t\t\t\t\tif ( !options.unique || !self.has( arg ) ) {\n\t\t\t\t\t\t\t\t\tlist.push( arg );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if ( arg && arg.length && toType( arg ) !== \"string\" ) {\n\n\t\t\t\t\t\t\t\t// Inspect recursively\n\t\t\t\t\t\t\t\tadd( arg );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} );\n\t\t\t\t\t} )( arguments );\n\n\t\t\t\t\tif ( memory && !firing ) {\n\t\t\t\t\t\tfire();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Remove a callback from the list\n\t\t\tremove: function() {\n\t\t\t\tjQuery.each( arguments, function( _, arg ) {\n\t\t\t\t\tvar index;\n\t\t\t\t\twhile ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {\n\t\t\t\t\t\tlist.splice( index, 1 );\n\n\t\t\t\t\t\t// Handle firing indexes\n\t\t\t\t\t\tif ( index <= firingIndex ) {\n\t\t\t\t\t\t\tfiringIndex--;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Check if a given callback is in the list.\n\t\t\t// If no argument is given, return whether or not list has callbacks attached.\n\t\t\thas: function( fn ) {\n\t\t\t\treturn fn ?\n\t\t\t\t\tjQuery.inArray( fn, list ) > -1 :\n\t\t\t\t\tlist.length > 0;\n\t\t\t},\n\n\t\t\t// Remove all callbacks from the list\n\t\t\tempty: function() {\n\t\t\t\tif ( list ) {\n\t\t\t\t\tlist = [];\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Disable .fire and .add\n\t\t\t// Abort any current/pending executions\n\t\t\t// Clear all callbacks and values\n\t\t\tdisable: function() {\n\t\t\t\tlocked = queue = [];\n\t\t\t\tlist = memory = \"\";\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\tdisabled: function() {\n\t\t\t\treturn !list;\n\t\t\t},\n\n\t\t\t// Disable .fire\n\t\t\t// Also disable .add unless we have memory (since it would have no effect)\n\t\t\t// Abort any pending executions\n\t\t\tlock: function() {\n\t\t\t\tlocked = queue = [];\n\t\t\t\tif ( !memory && !firing ) {\n\t\t\t\t\tlist = memory = \"\";\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\tlocked: function() {\n\t\t\t\treturn !!locked;\n\t\t\t},\n\n\t\t\t// Call all callbacks with the given context and arguments\n\t\t\tfireWith: function( context, args ) {\n\t\t\t\tif ( !locked ) {\n\t\t\t\t\targs = args || [];\n\t\t\t\t\targs = [ context, args.slice ? args.slice() : args ];\n\t\t\t\t\tqueue.push( args );\n\t\t\t\t\tif ( !firing ) {\n\t\t\t\t\t\tfire();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Call all the callbacks with the given arguments\n\t\t\tfire: function() {\n\t\t\t\tself.fireWith( this, arguments );\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// To know if the callbacks have already been called at least once\n\t\t\tfired: function() {\n\t\t\t\treturn !!fired;\n\t\t\t}\n\t\t};\n\n\treturn self;\n};\n\n\nfunction Identity( v ) {\n\treturn v;\n}\nfunction Thrower( ex ) {\n\tthrow ex;\n}\n\nfunction adoptValue( value, resolve, reject, noValue ) {\n\tvar method;\n\n\ttry {\n\n\t\t// Check for promise aspect first to privilege synchronous behavior\n\t\tif ( value && isFunction( ( method = value.promise ) ) ) {\n\t\t\tmethod.call( value ).done( resolve ).fail( reject );\n\n\t\t// Other thenables\n\t\t} else if ( value && isFunction( ( method = value.then ) ) ) {\n\t\t\tmethod.call( value, resolve, reject );\n\n\t\t// Other non-thenables\n\t\t} else {\n\n\t\t\t// Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer:\n\t\t\t// * false: [ value ].slice( 0 ) => resolve( value )\n\t\t\t// * true: [ value ].slice( 1 ) => resolve()\n\t\t\tresolve.apply( undefined, [ value ].slice( noValue ) );\n\t\t}\n\n\t// For Promises/A+, convert exceptions into rejections\n\t// Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in\n\t// Deferred#then to conditionally suppress rejection.\n\t} catch ( value ) {\n\n\t\t// Support: Android 4.0 only\n\t\t// Strict mode functions invoked without .call/.apply get global-object context\n\t\treject.apply( undefined, [ value ] );\n\t}\n}\n\njQuery.extend( {\n\n\tDeferred: function( func ) {\n\t\tvar tuples = [\n\n\t\t\t\t// action, add listener, callbacks,\n\t\t\t\t// ... .then handlers, argument index, [final state]\n\t\t\t\t[ \"notify\", \"progress\", jQuery.Callbacks( \"memory\" ),\n\t\t\t\t\tjQuery.Callbacks( \"memory\" ), 2 ],\n\t\t\t\t[ \"resolve\", \"done\", jQuery.Callbacks( \"once memory\" ),\n\t\t\t\t\tjQuery.Callbacks( \"once memory\" ), 0, \"resolved\" ],\n\t\t\t\t[ \"reject\", \"fail\", jQuery.Callbacks( \"once memory\" ),\n\t\t\t\t\tjQuery.Callbacks( \"once memory\" ), 1, \"rejected\" ]\n\t\t\t],\n\t\t\tstate = \"pending\",\n\t\t\tpromise = {\n\t\t\t\tstate: function() {\n\t\t\t\t\treturn state;\n\t\t\t\t},\n\t\t\t\talways: function() {\n\t\t\t\t\tdeferred.done( arguments ).fail( arguments );\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\t\t\t\t\"catch\": function( fn ) {\n\t\t\t\t\treturn promise.then( null, fn );\n\t\t\t\t},\n\n\t\t\t\t// Keep pipe for back-compat\n\t\t\t\tpipe: function( /* fnDone, fnFail, fnProgress */ ) {\n\t\t\t\t\tvar fns = arguments;\n\n\t\t\t\t\treturn jQuery.Deferred( function( newDefer ) {\n\t\t\t\t\t\tjQuery.each( tuples, function( i, tuple ) {\n\n\t\t\t\t\t\t\t// Map tuples (progress, done, fail) to arguments (done, fail, progress)\n\t\t\t\t\t\t\tvar fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ];\n\n\t\t\t\t\t\t\t// deferred.progress(function() { bind to newDefer or newDefer.notify })\n\t\t\t\t\t\t\t// deferred.done(function() { bind to newDefer or newDefer.resolve })\n\t\t\t\t\t\t\t// deferred.fail(function() { bind to newDefer or newDefer.reject })\n\t\t\t\t\t\t\tdeferred[ tuple[ 1 ] ]( function() {\n\t\t\t\t\t\t\t\tvar returned = fn && fn.apply( this, arguments );\n\t\t\t\t\t\t\t\tif ( returned && isFunction( returned.promise ) ) {\n\t\t\t\t\t\t\t\t\treturned.promise()\n\t\t\t\t\t\t\t\t\t\t.progress( newDefer.notify )\n\t\t\t\t\t\t\t\t\t\t.done( newDefer.resolve )\n\t\t\t\t\t\t\t\t\t\t.fail( newDefer.reject );\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tnewDefer[ tuple[ 0 ] + \"With\" ](\n\t\t\t\t\t\t\t\t\t\tthis,\n\t\t\t\t\t\t\t\t\t\tfn ? [ returned ] : arguments\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t} );\n\t\t\t\t\t\tfns = null;\n\t\t\t\t\t} ).promise();\n\t\t\t\t},\n\t\t\t\tthen: function( onFulfilled, onRejected, onProgress ) {\n\t\t\t\t\tvar maxDepth = 0;\n\t\t\t\t\tfunction resolve( depth, deferred, handler, special ) {\n\t\t\t\t\t\treturn function() {\n\t\t\t\t\t\t\tvar that = this,\n\t\t\t\t\t\t\t\targs = arguments,\n\t\t\t\t\t\t\t\tmightThrow = function() {\n\t\t\t\t\t\t\t\t\tvar returned, then;\n\n\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.3.3.3\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-59\n\t\t\t\t\t\t\t\t\t// Ignore double-resolution attempts\n\t\t\t\t\t\t\t\t\tif ( depth < maxDepth ) {\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\treturned = handler.apply( that, args );\n\n\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.1\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-48\n\t\t\t\t\t\t\t\t\tif ( returned === deferred.promise() ) {\n\t\t\t\t\t\t\t\t\t\tthrow new TypeError( \"Thenable self-resolution\" );\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t// Support: Promises/A+ sections 2.3.3.1, 3.5\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-54\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-75\n\t\t\t\t\t\t\t\t\t// Retrieve `then` only once\n\t\t\t\t\t\t\t\t\tthen = returned &&\n\n\t\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.4\n\t\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-64\n\t\t\t\t\t\t\t\t\t\t// Only check objects and functions for thenability\n\t\t\t\t\t\t\t\t\t\t( typeof returned === \"object\" ||\n\t\t\t\t\t\t\t\t\t\t\ttypeof returned === \"function\" ) &&\n\t\t\t\t\t\t\t\t\t\treturned.then;\n\n\t\t\t\t\t\t\t\t\t// Handle a returned thenable\n\t\t\t\t\t\t\t\t\tif ( isFunction( then ) ) {\n\n\t\t\t\t\t\t\t\t\t\t// Special processors (notify) just wait for resolution\n\t\t\t\t\t\t\t\t\t\tif ( special ) {\n\t\t\t\t\t\t\t\t\t\t\tthen.call(\n\t\t\t\t\t\t\t\t\t\t\t\treturned,\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Identity, special ),\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Thrower, special )\n\t\t\t\t\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\t\t\t\t// Normal processors (resolve) also hook into progress\n\t\t\t\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t\t\t\t// ...and disregard older resolution values\n\t\t\t\t\t\t\t\t\t\t\tmaxDepth++;\n\n\t\t\t\t\t\t\t\t\t\t\tthen.call(\n\t\t\t\t\t\t\t\t\t\t\t\treturned,\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Identity, special ),\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Thrower, special ),\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Identity,\n\t\t\t\t\t\t\t\t\t\t\t\t\tdeferred.notifyWith )\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t// Handle all other returned values\n\t\t\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t\t\t// Only substitute handlers pass on context\n\t\t\t\t\t\t\t\t\t\t// and multiple values (non-spec behavior)\n\t\t\t\t\t\t\t\t\t\tif ( handler !== Identity ) {\n\t\t\t\t\t\t\t\t\t\t\tthat = undefined;\n\t\t\t\t\t\t\t\t\t\t\targs = [ returned ];\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t// Process the value(s)\n\t\t\t\t\t\t\t\t\t\t// Default process is resolve\n\t\t\t\t\t\t\t\t\t\t( special || deferred.resolveWith )( that, args );\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\n\t\t\t\t\t\t\t\t// Only normal processors (resolve) catch and reject exceptions\n\t\t\t\t\t\t\t\tprocess = special ?\n\t\t\t\t\t\t\t\t\tmightThrow :\n\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\tmightThrow();\n\t\t\t\t\t\t\t\t\t\t} catch ( e ) {\n\n\t\t\t\t\t\t\t\t\t\t\tif ( jQuery.Deferred.exceptionHook ) {\n\t\t\t\t\t\t\t\t\t\t\t\tjQuery.Deferred.exceptionHook( e,\n\t\t\t\t\t\t\t\t\t\t\t\t\tprocess.stackTrace );\n\t\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.3.3.4.1\n\t\t\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-61\n\t\t\t\t\t\t\t\t\t\t\t// Ignore post-resolution exceptions\n\t\t\t\t\t\t\t\t\t\t\tif ( depth + 1 >= maxDepth ) {\n\n\t\t\t\t\t\t\t\t\t\t\t\t// Only substitute handlers pass on context\n\t\t\t\t\t\t\t\t\t\t\t\t// and multiple values (non-spec behavior)\n\t\t\t\t\t\t\t\t\t\t\t\tif ( handler !== Thrower ) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tthat = undefined;\n\t\t\t\t\t\t\t\t\t\t\t\t\targs = [ e ];\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t\t\tdeferred.rejectWith( that, args );\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.3.3.1\n\t\t\t\t\t\t\t// https://promisesaplus.com/#point-57\n\t\t\t\t\t\t\t// Re-resolve promises immediately to dodge false rejection from\n\t\t\t\t\t\t\t// subsequent errors\n\t\t\t\t\t\t\tif ( depth ) {\n\t\t\t\t\t\t\t\tprocess();\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t// Call an optional hook to record the stack, in case of exception\n\t\t\t\t\t\t\t\t// since it's otherwise lost when execution goes async\n\t\t\t\t\t\t\t\tif ( jQuery.Deferred.getStackHook ) {\n\t\t\t\t\t\t\t\t\tprocess.stackTrace = jQuery.Deferred.getStackHook();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\twindow.setTimeout( process );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\n\t\t\t\t\treturn jQuery.Deferred( function( newDefer ) {\n\n\t\t\t\t\t\t// progress_handlers.add( ... )\n\t\t\t\t\t\ttuples[ 0 ][ 3 ].add(\n\t\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\tnewDefer,\n\t\t\t\t\t\t\t\tisFunction( onProgress ) ?\n\t\t\t\t\t\t\t\t\tonProgress :\n\t\t\t\t\t\t\t\t\tIdentity,\n\t\t\t\t\t\t\t\tnewDefer.notifyWith\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// fulfilled_handlers.add( ... )\n\t\t\t\t\t\ttuples[ 1 ][ 3 ].add(\n\t\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\tnewDefer,\n\t\t\t\t\t\t\t\tisFunction( onFulfilled ) ?\n\t\t\t\t\t\t\t\t\tonFulfilled :\n\t\t\t\t\t\t\t\t\tIdentity\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// rejected_handlers.add( ... )\n\t\t\t\t\t\ttuples[ 2 ][ 3 ].add(\n\t\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\tnewDefer,\n\t\t\t\t\t\t\t\tisFunction( onRejected ) ?\n\t\t\t\t\t\t\t\t\tonRejected :\n\t\t\t\t\t\t\t\t\tThrower\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\t\t\t\t\t} ).promise();\n\t\t\t\t},\n\n\t\t\t\t// Get a promise for this deferred\n\t\t\t\t// If obj is provided, the promise aspect is added to the object\n\t\t\t\tpromise: function( obj ) {\n\t\t\t\t\treturn obj != null ? jQuery.extend( obj, promise ) : promise;\n\t\t\t\t}\n\t\t\t},\n\t\t\tdeferred = {};\n\n\t\t// Add list-specific methods\n\t\tjQuery.each( tuples, function( i, tuple ) {\n\t\t\tvar list = tuple[ 2 ],\n\t\t\t\tstateString = tuple[ 5 ];\n\n\t\t\t// promise.progress = list.add\n\t\t\t// promise.done = list.add\n\t\t\t// promise.fail = list.add\n\t\t\tpromise[ tuple[ 1 ] ] = list.add;\n\n\t\t\t// Handle state\n\t\t\tif ( stateString ) {\n\t\t\t\tlist.add(\n\t\t\t\t\tfunction() {\n\n\t\t\t\t\t\t// state = \"resolved\" (i.e., fulfilled)\n\t\t\t\t\t\t// state = \"rejected\"\n\t\t\t\t\t\tstate = stateString;\n\t\t\t\t\t},\n\n\t\t\t\t\t// rejected_callbacks.disable\n\t\t\t\t\t// fulfilled_callbacks.disable\n\t\t\t\t\ttuples[ 3 - i ][ 2 ].disable,\n\n\t\t\t\t\t// rejected_handlers.disable\n\t\t\t\t\t// fulfilled_handlers.disable\n\t\t\t\t\ttuples[ 3 - i ][ 3 ].disable,\n\n\t\t\t\t\t// progress_callbacks.lock\n\t\t\t\t\ttuples[ 0 ][ 2 ].lock,\n\n\t\t\t\t\t// progress_handlers.lock\n\t\t\t\t\ttuples[ 0 ][ 3 ].lock\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// progress_handlers.fire\n\t\t\t// fulfilled_handlers.fire\n\t\t\t// rejected_handlers.fire\n\t\t\tlist.add( tuple[ 3 ].fire );\n\n\t\t\t// deferred.notify = function() { deferred.notifyWith(...) }\n\t\t\t// deferred.resolve = function() { deferred.resolveWith(...) }\n\t\t\t// deferred.reject = function() { deferred.rejectWith(...) }\n\t\t\tdeferred[ tuple[ 0 ] ] = function() {\n\t\t\t\tdeferred[ tuple[ 0 ] + \"With\" ]( this === deferred ? undefined : this, arguments );\n\t\t\t\treturn this;\n\t\t\t};\n\n\t\t\t// deferred.notifyWith = list.fireWith\n\t\t\t// deferred.resolveWith = list.fireWith\n\t\t\t// deferred.rejectWith = list.fireWith\n\t\t\tdeferred[ tuple[ 0 ] + \"With\" ] = list.fireWith;\n\t\t} );\n\n\t\t// Make the deferred a promise\n\t\tpromise.promise( deferred );\n\n\t\t// Call given func if any\n\t\tif ( func ) {\n\t\t\tfunc.call( deferred, deferred );\n\t\t}\n\n\t\t// All done!\n\t\treturn deferred;\n\t},\n\n\t// Deferred helper\n\twhen: function( singleValue ) {\n\t\tvar\n\n\t\t\t// count of uncompleted subordinates\n\t\t\tremaining = arguments.length,\n\n\t\t\t// count of unprocessed arguments\n\t\t\ti = remaining,\n\n\t\t\t// subordinate fulfillment data\n\t\t\tresolveContexts = Array( i ),\n\t\t\tresolveValues = slice.call( arguments ),\n\n\t\t\t// the master Deferred\n\t\t\tmaster = jQuery.Deferred(),\n\n\t\t\t// subordinate callback factory\n\t\t\tupdateFunc = function( i ) {\n\t\t\t\treturn function( value ) {\n\t\t\t\t\tresolveContexts[ i ] = this;\n\t\t\t\t\tresolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;\n\t\t\t\t\tif ( !( --remaining ) ) {\n\t\t\t\t\t\tmaster.resolveWith( resolveContexts, resolveValues );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t};\n\n\t\t// Single- and empty arguments are adopted like Promise.resolve\n\t\tif ( remaining <= 1 ) {\n\t\t\tadoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject,\n\t\t\t\t!remaining );\n\n\t\t\t// Use .then() to unwrap secondary thenables (cf. gh-3000)\n\t\t\tif ( master.state() === \"pending\" ||\n\t\t\t\tisFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {\n\n\t\t\t\treturn master.then();\n\t\t\t}\n\t\t}\n\n\t\t// Multiple arguments are aggregated like Promise.all array elements\n\t\twhile ( i-- ) {\n\t\t\tadoptValue( resolveValues[ i ], updateFunc( i ), master.reject );\n\t\t}\n\n\t\treturn master.promise();\n\t}\n} );\n\n\n// These usually indicate a programmer mistake during development,\n// warn about them ASAP rather than swallowing them by default.\nvar rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;\n\njQuery.Deferred.exceptionHook = function( error, stack ) {\n\n\t// Support: IE 8 - 9 only\n\t// Console exists when dev tools are open, which can happen at any time\n\tif ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) {\n\t\twindow.console.warn( \"jQuery.Deferred exception: \" + error.message, error.stack, stack );\n\t}\n};\n\n\n\n\njQuery.readyException = function( error ) {\n\twindow.setTimeout( function() {\n\t\tthrow error;\n\t} );\n};\n\n\n\n\n// The deferred used on DOM ready\nvar readyList = jQuery.Deferred();\n\njQuery.fn.ready = function( fn ) {\n\n\treadyList\n\t\t.then( fn )\n\n\t\t// Wrap jQuery.readyException in a function so that the lookup\n\t\t// happens at the time of error handling instead of callback\n\t\t// registration.\n\t\t.catch( function( error ) {\n\t\t\tjQuery.readyException( error );\n\t\t} );\n\n\treturn this;\n};\n\njQuery.extend( {\n\n\t// Is the DOM ready to be used? Set to true once it occurs.\n\tisReady: false,\n\n\t// A counter to track how many items to wait for before\n\t// the ready event fires. See #6781\n\treadyWait: 1,\n\n\t// Handle when the DOM is ready\n\tready: function( wait ) {\n\n\t\t// Abort if there are pending holds or we're already ready\n\t\tif ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Remember that the DOM is ready\n\t\tjQuery.isReady = true;\n\n\t\t// If a normal DOM Ready event fired, decrement, and wait if need be\n\t\tif ( wait !== true && --jQuery.readyWait > 0 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// If there are functions bound, to execute\n\t\treadyList.resolveWith( document, [ jQuery ] );\n\t}\n} );\n\njQuery.ready.then = readyList.then;\n\n// The ready event handler and self cleanup method\nfunction completed() {\n\tdocument.removeEventListener( \"DOMContentLoaded\", completed );\n\twindow.removeEventListener( \"load\", completed );\n\tjQuery.ready();\n}\n\n// Catch cases where $(document).ready() is called\n// after the browser event has already occurred.\n// Support: IE <=9 - 10 only\n// Older IE sometimes signals \"interactive\" too soon\nif ( document.readyState === \"complete\" ||\n\t( document.readyState !== \"loading\" && !document.documentElement.doScroll ) ) {\n\n\t// Handle it asynchronously to allow scripts the opportunity to delay ready\n\twindow.setTimeout( jQuery.ready );\n\n} else {\n\n\t// Use the handy event callback\n\tdocument.addEventListener( \"DOMContentLoaded\", completed );\n\n\t// A fallback to window.onload, that will always work\n\twindow.addEventListener( \"load\", completed );\n}\n\n\n\n\n// Multifunctional method to get and set values of a collection\n// The value/s can optionally be executed if it's a function\nvar access = function( elems, fn, key, value, chainable, emptyGet, raw ) {\n\tvar i = 0,\n\t\tlen = elems.length,\n\t\tbulk = key == null;\n\n\t// Sets many values\n\tif ( toType( key ) === \"object\" ) {\n\t\tchainable = true;\n\t\tfor ( i in key ) {\n\t\t\taccess( elems, fn, i, key[ i ], true, emptyGet, raw );\n\t\t}\n\n\t// Sets one value\n\t} else if ( value !== undefined ) {\n\t\tchainable = true;\n\n\t\tif ( !isFunction( value ) ) {\n\t\t\traw = true;\n\t\t}\n\n\t\tif ( bulk ) {\n\n\t\t\t// Bulk operations run against the entire set\n\t\t\tif ( raw ) {\n\t\t\t\tfn.call( elems, value );\n\t\t\t\tfn = null;\n\n\t\t\t// ...except when executing function values\n\t\t\t} else {\n\t\t\t\tbulk = fn;\n\t\t\t\tfn = function( elem, key, value ) {\n\t\t\t\t\treturn bulk.call( jQuery( elem ), value );\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif ( fn ) {\n\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\tfn(\n\t\t\t\t\telems[ i ], key, raw ?\n\t\t\t\t\tvalue :\n\t\t\t\t\tvalue.call( elems[ i ], i, fn( elems[ i ], key ) )\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\tif ( chainable ) {\n\t\treturn elems;\n\t}\n\n\t// Gets\n\tif ( bulk ) {\n\t\treturn fn.call( elems );\n\t}\n\n\treturn len ? fn( elems[ 0 ], key ) : emptyGet;\n};\n\n\n// Matches dashed string for camelizing\nvar rmsPrefix = /^-ms-/,\n\trdashAlpha = /-([a-z])/g;\n\n// Used by camelCase as callback to replace()\nfunction fcamelCase( all, letter ) {\n\treturn letter.toUpperCase();\n}\n\n// Convert dashed to camelCase; used by the css and data modules\n// Support: IE <=9 - 11, Edge 12 - 15\n// Microsoft forgot to hump their vendor prefix (#9572)\nfunction camelCase( string ) {\n\treturn string.replace( rmsPrefix, \"ms-\" ).replace( rdashAlpha, fcamelCase );\n}\nvar acceptData = function( owner ) {\n\n\t// Accepts only:\n\t//  - Node\n\t//    - Node.ELEMENT_NODE\n\t//    - Node.DOCUMENT_NODE\n\t//  - Object\n\t//    - Any\n\treturn owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );\n};\n\n\n\n\nfunction Data() {\n\tthis.expando = jQuery.expando + Data.uid++;\n}\n\nData.uid = 1;\n\nData.prototype = {\n\n\tcache: function( owner ) {\n\n\t\t// Check if the owner object already has a cache\n\t\tvar value = owner[ this.expando ];\n\n\t\t// If not, create one\n\t\tif ( !value ) {\n\t\t\tvalue = {};\n\n\t\t\t// We can accept data for non-element nodes in modern browsers,\n\t\t\t// but we should not, see #8335.\n\t\t\t// Always return an empty object.\n\t\t\tif ( acceptData( owner ) ) {\n\n\t\t\t\t// If it is a node unlikely to be stringify-ed or looped over\n\t\t\t\t// use plain assignment\n\t\t\t\tif ( owner.nodeType ) {\n\t\t\t\t\towner[ this.expando ] = value;\n\n\t\t\t\t// Otherwise secure it in a non-enumerable property\n\t\t\t\t// configurable must be true to allow the property to be\n\t\t\t\t// deleted when data is removed\n\t\t\t\t} else {\n\t\t\t\t\tObject.defineProperty( owner, this.expando, {\n\t\t\t\t\t\tvalue: value,\n\t\t\t\t\t\tconfigurable: true\n\t\t\t\t\t} );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn value;\n\t},\n\tset: function( owner, data, value ) {\n\t\tvar prop,\n\t\t\tcache = this.cache( owner );\n\n\t\t// Handle: [ owner, key, value ] args\n\t\t// Always use camelCase key (gh-2257)\n\t\tif ( typeof data === \"string\" ) {\n\t\t\tcache[ camelCase( data ) ] = value;\n\n\t\t// Handle: [ owner, { properties } ] args\n\t\t} else {\n\n\t\t\t// Copy the properties one-by-one to the cache object\n\t\t\tfor ( prop in data ) {\n\t\t\t\tcache[ camelCase( prop ) ] = data[ prop ];\n\t\t\t}\n\t\t}\n\t\treturn cache;\n\t},\n\tget: function( owner, key ) {\n\t\treturn key === undefined ?\n\t\t\tthis.cache( owner ) :\n\n\t\t\t// Always use camelCase key (gh-2257)\n\t\t\towner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ];\n\t},\n\taccess: function( owner, key, value ) {\n\n\t\t// In cases where either:\n\t\t//\n\t\t//   1. No key was specified\n\t\t//   2. A string key was specified, but no value provided\n\t\t//\n\t\t// Take the \"read\" path and allow the get method to determine\n\t\t// which value to return, respectively either:\n\t\t//\n\t\t//   1. The entire cache object\n\t\t//   2. The data stored at the key\n\t\t//\n\t\tif ( key === undefined ||\n\t\t\t\t( ( key && typeof key === \"string\" ) && value === undefined ) ) {\n\n\t\t\treturn this.get( owner, key );\n\t\t}\n\n\t\t// When the key is not a string, or both a key and value\n\t\t// are specified, set or extend (existing objects) with either:\n\t\t//\n\t\t//   1. An object of properties\n\t\t//   2. A key and value\n\t\t//\n\t\tthis.set( owner, key, value );\n\n\t\t// Since the \"set\" path can have two possible entry points\n\t\t// return the expected data based on which path was taken[*]\n\t\treturn value !== undefined ? value : key;\n\t},\n\tremove: function( owner, key ) {\n\t\tvar i,\n\t\t\tcache = owner[ this.expando ];\n\n\t\tif ( cache === undefined ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( key !== undefined ) {\n\n\t\t\t// Support array or space separated string of keys\n\t\t\tif ( Array.isArray( key ) ) {\n\n\t\t\t\t// If key is an array of keys...\n\t\t\t\t// We always set camelCase keys, so remove that.\n\t\t\t\tkey = key.map( camelCase );\n\t\t\t} else {\n\t\t\t\tkey = camelCase( key );\n\n\t\t\t\t// If a key with the spaces exists, use it.\n\t\t\t\t// Otherwise, create an array by matching non-whitespace\n\t\t\t\tkey = key in cache ?\n\t\t\t\t\t[ key ] :\n\t\t\t\t\t( key.match( rnothtmlwhite ) || [] );\n\t\t\t}\n\n\t\t\ti = key.length;\n\n\t\t\twhile ( i-- ) {\n\t\t\t\tdelete cache[ key[ i ] ];\n\t\t\t}\n\t\t}\n\n\t\t// Remove the expando if there's no more data\n\t\tif ( key === undefined || jQuery.isEmptyObject( cache ) ) {\n\n\t\t\t// Support: Chrome <=35 - 45\n\t\t\t// Webkit & Blink performance suffers when deleting properties\n\t\t\t// from DOM nodes, so set to undefined instead\n\t\t\t// https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted)\n\t\t\tif ( owner.nodeType ) {\n\t\t\t\towner[ this.expando ] = undefined;\n\t\t\t} else {\n\t\t\t\tdelete owner[ this.expando ];\n\t\t\t}\n\t\t}\n\t},\n\thasData: function( owner ) {\n\t\tvar cache = owner[ this.expando ];\n\t\treturn cache !== undefined && !jQuery.isEmptyObject( cache );\n\t}\n};\nvar dataPriv = new Data();\n\nvar dataUser = new Data();\n\n\n\n//\tImplementation Summary\n//\n//\t1. Enforce API surface and semantic compatibility with 1.9.x branch\n//\t2. Improve the module's maintainability by reducing the storage\n//\t\tpaths to a single mechanism.\n//\t3. Use the same single mechanism to support \"private\" and \"user\" data.\n//\t4. _Never_ expose \"private\" data to user code (TODO: Drop _data, _removeData)\n//\t5. Avoid exposing implementation details on user objects (eg. expando properties)\n//\t6. Provide a clear path for implementation upgrade to WeakMap in 2014\n\nvar rbrace = /^(?:\\{[\\w\\W]*\\}|\\[[\\w\\W]*\\])$/,\n\trmultiDash = /[A-Z]/g;\n\nfunction getData( data ) {\n\tif ( data === \"true\" ) {\n\t\treturn true;\n\t}\n\n\tif ( data === \"false\" ) {\n\t\treturn false;\n\t}\n\n\tif ( data === \"null\" ) {\n\t\treturn null;\n\t}\n\n\t// Only convert to a number if it doesn't change the string\n\tif ( data === +data + \"\" ) {\n\t\treturn +data;\n\t}\n\n\tif ( rbrace.test( data ) ) {\n\t\treturn JSON.parse( data );\n\t}\n\n\treturn data;\n}\n\nfunction dataAttr( elem, key, data ) {\n\tvar name;\n\n\t// If nothing was found internally, try to fetch any\n\t// data from the HTML5 data-* attribute\n\tif ( data === undefined && elem.nodeType === 1 ) {\n\t\tname = \"data-\" + key.replace( rmultiDash, \"-$&\" ).toLowerCase();\n\t\tdata = elem.getAttribute( name );\n\n\t\tif ( typeof data === \"string\" ) {\n\t\t\ttry {\n\t\t\t\tdata = getData( data );\n\t\t\t} catch ( e ) {}\n\n\t\t\t// Make sure we set the data so it isn't changed later\n\t\t\tdataUser.set( elem, key, data );\n\t\t} else {\n\t\t\tdata = undefined;\n\t\t}\n\t}\n\treturn data;\n}\n\njQuery.extend( {\n\thasData: function( elem ) {\n\t\treturn dataUser.hasData( elem ) || dataPriv.hasData( elem );\n\t},\n\n\tdata: function( elem, name, data ) {\n\t\treturn dataUser.access( elem, name, data );\n\t},\n\n\tremoveData: function( elem, name ) {\n\t\tdataUser.remove( elem, name );\n\t},\n\n\t// TODO: Now that all calls to _data and _removeData have been replaced\n\t// with direct calls to dataPriv methods, these can be deprecated.\n\t_data: function( elem, name, data ) {\n\t\treturn dataPriv.access( elem, name, data );\n\t},\n\n\t_removeData: function( elem, name ) {\n\t\tdataPriv.remove( elem, name );\n\t}\n} );\n\njQuery.fn.extend( {\n\tdata: function( key, value ) {\n\t\tvar i, name, data,\n\t\t\telem = this[ 0 ],\n\t\t\tattrs = elem && elem.attributes;\n\n\t\t// Gets all values\n\t\tif ( key === undefined ) {\n\t\t\tif ( this.length ) {\n\t\t\t\tdata = dataUser.get( elem );\n\n\t\t\t\tif ( elem.nodeType === 1 && !dataPriv.get( elem, \"hasDataAttrs\" ) ) {\n\t\t\t\t\ti = attrs.length;\n\t\t\t\t\twhile ( i-- ) {\n\n\t\t\t\t\t\t// Support: IE 11 only\n\t\t\t\t\t\t// The attrs elements can be null (#14894)\n\t\t\t\t\t\tif ( attrs[ i ] ) {\n\t\t\t\t\t\t\tname = attrs[ i ].name;\n\t\t\t\t\t\t\tif ( name.indexOf( \"data-\" ) === 0 ) {\n\t\t\t\t\t\t\t\tname = camelCase( name.slice( 5 ) );\n\t\t\t\t\t\t\t\tdataAttr( elem, name, data[ name ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdataPriv.set( elem, \"hasDataAttrs\", true );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn data;\n\t\t}\n\n\t\t// Sets multiple values\n\t\tif ( typeof key === \"object\" ) {\n\t\t\treturn this.each( function() {\n\t\t\t\tdataUser.set( this, key );\n\t\t\t} );\n\t\t}\n\n\t\treturn access( this, function( value ) {\n\t\t\tvar data;\n\n\t\t\t// The calling jQuery object (element matches) is not empty\n\t\t\t// (and therefore has an element appears at this[ 0 ]) and the\n\t\t\t// `value` parameter was not undefined. An empty jQuery object\n\t\t\t// will result in `undefined` for elem = this[ 0 ] which will\n\t\t\t// throw an exception if an attempt to read a data cache is made.\n\t\t\tif ( elem && value === undefined ) {\n\n\t\t\t\t// Attempt to get data from the cache\n\t\t\t\t// The key will always be camelCased in Data\n\t\t\t\tdata = dataUser.get( elem, key );\n\t\t\t\tif ( data !== undefined ) {\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\n\t\t\t\t// Attempt to \"discover\" the data in\n\t\t\t\t// HTML5 custom data-* attrs\n\t\t\t\tdata = dataAttr( elem, key );\n\t\t\t\tif ( data !== undefined ) {\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\n\t\t\t\t// We tried really hard, but the data doesn't exist.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Set the data...\n\t\t\tthis.each( function() {\n\n\t\t\t\t// We always store the camelCased key\n\t\t\t\tdataUser.set( this, key, value );\n\t\t\t} );\n\t\t}, null, value, arguments.length > 1, null, true );\n\t},\n\n\tremoveData: function( key ) {\n\t\treturn this.each( function() {\n\t\t\tdataUser.remove( this, key );\n\t\t} );\n\t}\n} );\n\n\njQuery.extend( {\n\tqueue: function( elem, type, data ) {\n\t\tvar queue;\n\n\t\tif ( elem ) {\n\t\t\ttype = ( type || \"fx\" ) + \"queue\";\n\t\t\tqueue = dataPriv.get( elem, type );\n\n\t\t\t// Speed up dequeue by getting out quickly if this is just a lookup\n\t\t\tif ( data ) {\n\t\t\t\tif ( !queue || Array.isArray( data ) ) {\n\t\t\t\t\tqueue = dataPriv.access( elem, type, jQuery.makeArray( data ) );\n\t\t\t\t} else {\n\t\t\t\t\tqueue.push( data );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn queue || [];\n\t\t}\n\t},\n\n\tdequeue: function( elem, type ) {\n\t\ttype = type || \"fx\";\n\n\t\tvar queue = jQuery.queue( elem, type ),\n\t\t\tstartLength = queue.length,\n\t\t\tfn = queue.shift(),\n\t\t\thooks = jQuery._queueHooks( elem, type ),\n\t\t\tnext = function() {\n\t\t\t\tjQuery.dequeue( elem, type );\n\t\t\t};\n\n\t\t// If the fx queue is dequeued, always remove the progress sentinel\n\t\tif ( fn === \"inprogress\" ) {\n\t\t\tfn = queue.shift();\n\t\t\tstartLength--;\n\t\t}\n\n\t\tif ( fn ) {\n\n\t\t\t// Add a progress sentinel to prevent the fx queue from being\n\t\t\t// automatically dequeued\n\t\t\tif ( type === \"fx\" ) {\n\t\t\t\tqueue.unshift( \"inprogress\" );\n\t\t\t}\n\n\t\t\t// Clear up the last queue stop function\n\t\t\tdelete hooks.stop;\n\t\t\tfn.call( elem, next, hooks );\n\t\t}\n\n\t\tif ( !startLength && hooks ) {\n\t\t\thooks.empty.fire();\n\t\t}\n\t},\n\n\t// Not public - generate a queueHooks object, or return the current one\n\t_queueHooks: function( elem, type ) {\n\t\tvar key = type + \"queueHooks\";\n\t\treturn dataPriv.get( elem, key ) || dataPriv.access( elem, key, {\n\t\t\tempty: jQuery.Callbacks( \"once memory\" ).add( function() {\n\t\t\t\tdataPriv.remove( elem, [ type + \"queue\", key ] );\n\t\t\t} )\n\t\t} );\n\t}\n} );\n\njQuery.fn.extend( {\n\tqueue: function( type, data ) {\n\t\tvar setter = 2;\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tdata = type;\n\t\t\ttype = \"fx\";\n\t\t\tsetter--;\n\t\t}\n\n\t\tif ( arguments.length < setter ) {\n\t\t\treturn jQuery.queue( this[ 0 ], type );\n\t\t}\n\n\t\treturn data === undefined ?\n\t\t\tthis :\n\t\t\tthis.each( function() {\n\t\t\t\tvar queue = jQuery.queue( this, type, data );\n\n\t\t\t\t// Ensure a hooks for this queue\n\t\t\t\tjQuery._queueHooks( this, type );\n\n\t\t\t\tif ( type === \"fx\" && queue[ 0 ] !== \"inprogress\" ) {\n\t\t\t\t\tjQuery.dequeue( this, type );\n\t\t\t\t}\n\t\t\t} );\n\t},\n\tdequeue: function( type ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.dequeue( this, type );\n\t\t} );\n\t},\n\tclearQueue: function( type ) {\n\t\treturn this.queue( type || \"fx\", [] );\n\t},\n\n\t// Get a promise resolved when queues of a certain type\n\t// are emptied (fx is the type by default)\n\tpromise: function( type, obj ) {\n\t\tvar tmp,\n\t\t\tcount = 1,\n\t\t\tdefer = jQuery.Deferred(),\n\t\t\telements = this,\n\t\t\ti = this.length,\n\t\t\tresolve = function() {\n\t\t\t\tif ( !( --count ) ) {\n\t\t\t\t\tdefer.resolveWith( elements, [ elements ] );\n\t\t\t\t}\n\t\t\t};\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tobj = type;\n\t\t\ttype = undefined;\n\t\t}\n\t\ttype = type || \"fx\";\n\n\t\twhile ( i-- ) {\n\t\t\ttmp = dataPriv.get( elements[ i ], type + \"queueHooks\" );\n\t\t\tif ( tmp && tmp.empty ) {\n\t\t\t\tcount++;\n\t\t\t\ttmp.empty.add( resolve );\n\t\t\t}\n\t\t}\n\t\tresolve();\n\t\treturn defer.promise( obj );\n\t}\n} );\nvar pnum = ( /[+-]?(?:\\d*\\.|)\\d+(?:[eE][+-]?\\d+|)/ ).source;\n\nvar rcssNum = new RegExp( \"^(?:([+-])=|)(\" + pnum + \")([a-z%]*)$\", \"i\" );\n\n\nvar cssExpand = [ \"Top\", \"Right\", \"Bottom\", \"Left\" ];\n\nvar isHiddenWithinTree = function( elem, el ) {\n\n\t\t// isHiddenWithinTree might be called from jQuery#filter function;\n\t\t// in that case, element will be second argument\n\t\telem = el || elem;\n\n\t\t// Inline style trumps all\n\t\treturn elem.style.display === \"none\" ||\n\t\t\telem.style.display === \"\" &&\n\n\t\t\t// Otherwise, check computed style\n\t\t\t// Support: Firefox <=43 - 45\n\t\t\t// Disconnected elements can have computed display: none, so first confirm that elem is\n\t\t\t// in the document.\n\t\t\tjQuery.contains( elem.ownerDocument, elem ) &&\n\n\t\t\tjQuery.css( elem, \"display\" ) === \"none\";\n\t};\n\nvar swap = function( elem, options, callback, args ) {\n\tvar ret, name,\n\t\told = {};\n\n\t// Remember the old values, and insert the new ones\n\tfor ( name in options ) {\n\t\told[ name ] = elem.style[ name ];\n\t\telem.style[ name ] = options[ name ];\n\t}\n\n\tret = callback.apply( elem, args || [] );\n\n\t// Revert the old values\n\tfor ( name in options ) {\n\t\telem.style[ name ] = old[ name ];\n\t}\n\n\treturn ret;\n};\n\n\n\n\nfunction adjustCSS( elem, prop, valueParts, tween ) {\n\tvar adjusted, scale,\n\t\tmaxIterations = 20,\n\t\tcurrentValue = tween ?\n\t\t\tfunction() {\n\t\t\t\treturn tween.cur();\n\t\t\t} :\n\t\t\tfunction() {\n\t\t\t\treturn jQuery.css( elem, prop, \"\" );\n\t\t\t},\n\t\tinitial = currentValue(),\n\t\tunit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? \"\" : \"px\" ),\n\n\t\t// Starting value computation is required for potential unit mismatches\n\t\tinitialInUnit = ( jQuery.cssNumber[ prop ] || unit !== \"px\" && +initial ) &&\n\t\t\trcssNum.exec( jQuery.css( elem, prop ) );\n\n\tif ( initialInUnit && initialInUnit[ 3 ] !== unit ) {\n\n\t\t// Support: Firefox <=54\n\t\t// Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144)\n\t\tinitial = initial / 2;\n\n\t\t// Trust units reported by jQuery.css\n\t\tunit = unit || initialInUnit[ 3 ];\n\n\t\t// Iteratively approximate from a nonzero starting point\n\t\tinitialInUnit = +initial || 1;\n\n\t\twhile ( maxIterations-- ) {\n\n\t\t\t// Evaluate and update our best guess (doubling guesses that zero out).\n\t\t\t// Finish if the scale equals or crosses 1 (making the old*new product non-positive).\n\t\t\tjQuery.style( elem, prop, initialInUnit + unit );\n\t\t\tif ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) {\n\t\t\t\tmaxIterations = 0;\n\t\t\t}\n\t\t\tinitialInUnit = initialInUnit / scale;\n\n\t\t}\n\n\t\tinitialInUnit = initialInUnit * 2;\n\t\tjQuery.style( elem, prop, initialInUnit + unit );\n\n\t\t// Make sure we update the tween properties later on\n\t\tvalueParts = valueParts || [];\n\t}\n\n\tif ( valueParts ) {\n\t\tinitialInUnit = +initialInUnit || +initial || 0;\n\n\t\t// Apply relative offset (+=/-=) if specified\n\t\tadjusted = valueParts[ 1 ] ?\n\t\t\tinitialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] :\n\t\t\t+valueParts[ 2 ];\n\t\tif ( tween ) {\n\t\t\ttween.unit = unit;\n\t\t\ttween.start = initialInUnit;\n\t\t\ttween.end = adjusted;\n\t\t}\n\t}\n\treturn adjusted;\n}\n\n\nvar defaultDisplayMap = {};\n\nfunction getDefaultDisplay( elem ) {\n\tvar temp,\n\t\tdoc = elem.ownerDocument,\n\t\tnodeName = elem.nodeName,\n\t\tdisplay = defaultDisplayMap[ nodeName ];\n\n\tif ( display ) {\n\t\treturn display;\n\t}\n\n\ttemp = doc.body.appendChild( doc.createElement( nodeName ) );\n\tdisplay = jQuery.css( temp, \"display\" );\n\n\ttemp.parentNode.removeChild( temp );\n\n\tif ( display === \"none\" ) {\n\t\tdisplay = \"block\";\n\t}\n\tdefaultDisplayMap[ nodeName ] = display;\n\n\treturn display;\n}\n\nfunction showHide( elements, show ) {\n\tvar display, elem,\n\t\tvalues = [],\n\t\tindex = 0,\n\t\tlength = elements.length;\n\n\t// Determine new display value for elements that need to change\n\tfor ( ; index < length; index++ ) {\n\t\telem = elements[ index ];\n\t\tif ( !elem.style ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tdisplay = elem.style.display;\n\t\tif ( show ) {\n\n\t\t\t// Since we force visibility upon cascade-hidden elements, an immediate (and slow)\n\t\t\t// check is required in this first loop unless we have a nonempty display value (either\n\t\t\t// inline or about-to-be-restored)\n\t\t\tif ( display === \"none\" ) {\n\t\t\t\tvalues[ index ] = dataPriv.get( elem, \"display\" ) || null;\n\t\t\t\tif ( !values[ index ] ) {\n\t\t\t\t\telem.style.display = \"\";\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ( elem.style.display === \"\" && isHiddenWithinTree( elem ) ) {\n\t\t\t\tvalues[ index ] = getDefaultDisplay( elem );\n\t\t\t}\n\t\t} else {\n\t\t\tif ( display !== \"none\" ) {\n\t\t\t\tvalues[ index ] = \"none\";\n\n\t\t\t\t// Remember what we're overwriting\n\t\t\t\tdataPriv.set( elem, \"display\", display );\n\t\t\t}\n\t\t}\n\t}\n\n\t// Set the display of the elements in a second loop to avoid constant reflow\n\tfor ( index = 0; index < length; index++ ) {\n\t\tif ( values[ index ] != null ) {\n\t\t\telements[ index ].style.display = values[ index ];\n\t\t}\n\t}\n\n\treturn elements;\n}\n\njQuery.fn.extend( {\n\tshow: function() {\n\t\treturn showHide( this, true );\n\t},\n\thide: function() {\n\t\treturn showHide( this );\n\t},\n\ttoggle: function( state ) {\n\t\tif ( typeof state === \"boolean\" ) {\n\t\t\treturn state ? this.show() : this.hide();\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tif ( isHiddenWithinTree( this ) ) {\n\t\t\t\tjQuery( this ).show();\n\t\t\t} else {\n\t\t\t\tjQuery( this ).hide();\n\t\t\t}\n\t\t} );\n\t}\n} );\nvar rcheckableType = ( /^(?:checkbox|radio)$/i );\n\nvar rtagName = ( /<([a-z][^\\/\\0>\\x20\\t\\r\\n\\f]+)/i );\n\nvar rscriptType = ( /^$|^module$|\\/(?:java|ecma)script/i );\n\n\n\n// We have to close these tags to support XHTML (#13200)\nvar wrapMap = {\n\n\t// Support: IE <=9 only\n\toption: [ 1, \"<select multiple='multiple'>\", \"</select>\" ],\n\n\t// XHTML parsers do not magically insert elements in the\n\t// same way that tag soup parsers do. So we cannot shorten\n\t// this by omitting <tbody> or other required elements.\n\tthead: [ 1, \"<table>\", \"</table>\" ],\n\tcol: [ 2, \"<table><colgroup>\", \"</colgroup></table>\" ],\n\ttr: [ 2, \"<table><tbody>\", \"</tbody></table>\" ],\n\ttd: [ 3, \"<table><tbody><tr>\", \"</tr></tbody></table>\" ],\n\n\t_default: [ 0, \"\", \"\" ]\n};\n\n// Support: IE <=9 only\nwrapMap.optgroup = wrapMap.option;\n\nwrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;\nwrapMap.th = wrapMap.td;\n\n\nfunction getAll( context, tag ) {\n\n\t// Support: IE <=9 - 11 only\n\t// Use typeof to avoid zero-argument method invocation on host objects (#15151)\n\tvar ret;\n\n\tif ( typeof context.getElementsByTagName !== \"undefined\" ) {\n\t\tret = context.getElementsByTagName( tag || \"*\" );\n\n\t} else if ( typeof context.querySelectorAll !== \"undefined\" ) {\n\t\tret = context.querySelectorAll( tag || \"*\" );\n\n\t} else {\n\t\tret = [];\n\t}\n\n\tif ( tag === undefined || tag && nodeName( context, tag ) ) {\n\t\treturn jQuery.merge( [ context ], ret );\n\t}\n\n\treturn ret;\n}\n\n\n// Mark scripts as having already been evaluated\nfunction setGlobalEval( elems, refElements ) {\n\tvar i = 0,\n\t\tl = elems.length;\n\n\tfor ( ; i < l; i++ ) {\n\t\tdataPriv.set(\n\t\t\telems[ i ],\n\t\t\t\"globalEval\",\n\t\t\t!refElements || dataPriv.get( refElements[ i ], \"globalEval\" )\n\t\t);\n\t}\n}\n\n\nvar rhtml = /<|&#?\\w+;/;\n\nfunction buildFragment( elems, context, scripts, selection, ignored ) {\n\tvar elem, tmp, tag, wrap, contains, j,\n\t\tfragment = context.createDocumentFragment(),\n\t\tnodes = [],\n\t\ti = 0,\n\t\tl = elems.length;\n\n\tfor ( ; i < l; i++ ) {\n\t\telem = elems[ i ];\n\n\t\tif ( elem || elem === 0 ) {\n\n\t\t\t// Add nodes directly\n\t\t\tif ( toType( elem ) === \"object\" ) {\n\n\t\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t\t// push.apply(_, arraylike) throws on ancient WebKit\n\t\t\t\tjQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );\n\n\t\t\t// Convert non-html into a text node\n\t\t\t} else if ( !rhtml.test( elem ) ) {\n\t\t\t\tnodes.push( context.createTextNode( elem ) );\n\n\t\t\t// Convert html into DOM nodes\n\t\t\t} else {\n\t\t\t\ttmp = tmp || fragment.appendChild( context.createElement( \"div\" ) );\n\n\t\t\t\t// Deserialize a standard representation\n\t\t\t\ttag = ( rtagName.exec( elem ) || [ \"\", \"\" ] )[ 1 ].toLowerCase();\n\t\t\t\twrap = wrapMap[ tag ] || wrapMap._default;\n\t\t\t\ttmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];\n\n\t\t\t\t// Descend through wrappers to the right content\n\t\t\t\tj = wrap[ 0 ];\n\t\t\t\twhile ( j-- ) {\n\t\t\t\t\ttmp = tmp.lastChild;\n\t\t\t\t}\n\n\t\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t\t// push.apply(_, arraylike) throws on ancient WebKit\n\t\t\t\tjQuery.merge( nodes, tmp.childNodes );\n\n\t\t\t\t// Remember the top-level container\n\t\t\t\ttmp = fragment.firstChild;\n\n\t\t\t\t// Ensure the created nodes are orphaned (#12392)\n\t\t\t\ttmp.textContent = \"\";\n\t\t\t}\n\t\t}\n\t}\n\n\t// Remove wrapper from fragment\n\tfragment.textContent = \"\";\n\n\ti = 0;\n\twhile ( ( elem = nodes[ i++ ] ) ) {\n\n\t\t// Skip elements already in the context collection (trac-4087)\n\t\tif ( selection && jQuery.inArray( elem, selection ) > -1 ) {\n\t\t\tif ( ignored ) {\n\t\t\t\tignored.push( elem );\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tcontains = jQuery.contains( elem.ownerDocument, elem );\n\n\t\t// Append to fragment\n\t\ttmp = getAll( fragment.appendChild( elem ), \"script\" );\n\n\t\t// Preserve script evaluation history\n\t\tif ( contains ) {\n\t\t\tsetGlobalEval( tmp );\n\t\t}\n\n\t\t// Capture executables\n\t\tif ( scripts ) {\n\t\t\tj = 0;\n\t\t\twhile ( ( elem = tmp[ j++ ] ) ) {\n\t\t\t\tif ( rscriptType.test( elem.type || \"\" ) ) {\n\t\t\t\t\tscripts.push( elem );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn fragment;\n}\n\n\n( function() {\n\tvar fragment = document.createDocumentFragment(),\n\t\tdiv = fragment.appendChild( document.createElement( \"div\" ) ),\n\t\tinput = document.createElement( \"input\" );\n\n\t// Support: Android 4.0 - 4.3 only\n\t// Check state lost if the name is set (#11217)\n\t// Support: Windows Web Apps (WWA)\n\t// `name` and `type` must use .setAttribute for WWA (#14901)\n\tinput.setAttribute( \"type\", \"radio\" );\n\tinput.setAttribute( \"checked\", \"checked\" );\n\tinput.setAttribute( \"name\", \"t\" );\n\n\tdiv.appendChild( input );\n\n\t// Support: Android <=4.1 only\n\t// Older WebKit doesn't clone checked state correctly in fragments\n\tsupport.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;\n\n\t// Support: IE <=11 only\n\t// Make sure textarea (and checkbox) defaultValue is properly cloned\n\tdiv.innerHTML = \"<textarea>x</textarea>\";\n\tsupport.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;\n} )();\nvar documentElement = document.documentElement;\n\n\n\nvar\n\trkeyEvent = /^key/,\n\trmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,\n\trtypenamespace = /^([^.]*)(?:\\.(.+)|)/;\n\nfunction returnTrue() {\n\treturn true;\n}\n\nfunction returnFalse() {\n\treturn false;\n}\n\n// Support: IE <=9 only\n// See #13393 for more info\nfunction safeActiveElement() {\n\ttry {\n\t\treturn document.activeElement;\n\t} catch ( err ) { }\n}\n\nfunction on( elem, types, selector, data, fn, one ) {\n\tvar origFn, type;\n\n\t// Types can be a map of types/handlers\n\tif ( typeof types === \"object\" ) {\n\n\t\t// ( types-Object, selector, data )\n\t\tif ( typeof selector !== \"string\" ) {\n\n\t\t\t// ( types-Object, data )\n\t\t\tdata = data || selector;\n\t\t\tselector = undefined;\n\t\t}\n\t\tfor ( type in types ) {\n\t\t\ton( elem, type, selector, data, types[ type ], one );\n\t\t}\n\t\treturn elem;\n\t}\n\n\tif ( data == null && fn == null ) {\n\n\t\t// ( types, fn )\n\t\tfn = selector;\n\t\tdata = selector = undefined;\n\t} else if ( fn == null ) {\n\t\tif ( typeof selector === \"string\" ) {\n\n\t\t\t// ( types, selector, fn )\n\t\t\tfn = data;\n\t\t\tdata = undefined;\n\t\t} else {\n\n\t\t\t// ( types, data, fn )\n\t\t\tfn = data;\n\t\t\tdata = selector;\n\t\t\tselector = undefined;\n\t\t}\n\t}\n\tif ( fn === false ) {\n\t\tfn = returnFalse;\n\t} else if ( !fn ) {\n\t\treturn elem;\n\t}\n\n\tif ( one === 1 ) {\n\t\torigFn = fn;\n\t\tfn = function( event ) {\n\n\t\t\t// Can use an empty set, since event contains the info\n\t\t\tjQuery().off( event );\n\t\t\treturn origFn.apply( this, arguments );\n\t\t};\n\n\t\t// Use same guid so caller can remove using origFn\n\t\tfn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );\n\t}\n\treturn elem.each( function() {\n\t\tjQuery.event.add( this, types, fn, data, selector );\n\t} );\n}\n\n/*\n * Helper functions for managing events -- not part of the public interface.\n * Props to Dean Edwards' addEvent library for many of the ideas.\n */\njQuery.event = {\n\n\tglobal: {},\n\n\tadd: function( elem, types, handler, data, selector ) {\n\n\t\tvar handleObjIn, eventHandle, tmp,\n\t\t\tevents, t, handleObj,\n\t\t\tspecial, handlers, type, namespaces, origType,\n\t\t\telemData = dataPriv.get( elem );\n\n\t\t// Don't attach events to noData or text/comment nodes (but allow plain objects)\n\t\tif ( !elemData ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Caller can pass in an object of custom data in lieu of the handler\n\t\tif ( handler.handler ) {\n\t\t\thandleObjIn = handler;\n\t\t\thandler = handleObjIn.handler;\n\t\t\tselector = handleObjIn.selector;\n\t\t}\n\n\t\t// Ensure that invalid selectors throw exceptions at attach time\n\t\t// Evaluate against documentElement in case elem is a non-element node (e.g., document)\n\t\tif ( selector ) {\n\t\t\tjQuery.find.matchesSelector( documentElement, selector );\n\t\t}\n\n\t\t// Make sure that the handler has a unique ID, used to find/remove it later\n\t\tif ( !handler.guid ) {\n\t\t\thandler.guid = jQuery.guid++;\n\t\t}\n\n\t\t// Init the element's event structure and main handler, if this is the first\n\t\tif ( !( events = elemData.events ) ) {\n\t\t\tevents = elemData.events = {};\n\t\t}\n\t\tif ( !( eventHandle = elemData.handle ) ) {\n\t\t\teventHandle = elemData.handle = function( e ) {\n\n\t\t\t\t// Discard the second event of a jQuery.event.trigger() and\n\t\t\t\t// when an event is called after a page has unloaded\n\t\t\t\treturn typeof jQuery !== \"undefined\" && jQuery.event.triggered !== e.type ?\n\t\t\t\t\tjQuery.event.dispatch.apply( elem, arguments ) : undefined;\n\t\t\t};\n\t\t}\n\n\t\t// Handle multiple events separated by a space\n\t\ttypes = ( types || \"\" ).match( rnothtmlwhite ) || [ \"\" ];\n\t\tt = types.length;\n\t\twhile ( t-- ) {\n\t\t\ttmp = rtypenamespace.exec( types[ t ] ) || [];\n\t\t\ttype = origType = tmp[ 1 ];\n\t\t\tnamespaces = ( tmp[ 2 ] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// There *must* be a type, no attaching namespace-only handlers\n\t\t\tif ( !type ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// If event changes its type, use the special event handlers for the changed type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// If selector defined, determine special event api type, otherwise given type\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\n\t\t\t// Update special based on newly reset type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// handleObj is passed to all event handlers\n\t\t\thandleObj = jQuery.extend( {\n\t\t\t\ttype: type,\n\t\t\t\torigType: origType,\n\t\t\t\tdata: data,\n\t\t\t\thandler: handler,\n\t\t\t\tguid: handler.guid,\n\t\t\t\tselector: selector,\n\t\t\t\tneedsContext: selector && jQuery.expr.match.needsContext.test( selector ),\n\t\t\t\tnamespace: namespaces.join( \".\" )\n\t\t\t}, handleObjIn );\n\n\t\t\t// Init the event handler queue if we're the first\n\t\t\tif ( !( handlers = events[ type ] ) ) {\n\t\t\t\thandlers = events[ type ] = [];\n\t\t\t\thandlers.delegateCount = 0;\n\n\t\t\t\t// Only use addEventListener if the special events handler returns false\n\t\t\t\tif ( !special.setup ||\n\t\t\t\t\tspecial.setup.call( elem, data, namespaces, eventHandle ) === false ) {\n\n\t\t\t\t\tif ( elem.addEventListener ) {\n\t\t\t\t\t\telem.addEventListener( type, eventHandle );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( special.add ) {\n\t\t\t\tspecial.add.call( elem, handleObj );\n\n\t\t\t\tif ( !handleObj.handler.guid ) {\n\t\t\t\t\thandleObj.handler.guid = handler.guid;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add to the element's handler list, delegates in front\n\t\t\tif ( selector ) {\n\t\t\t\thandlers.splice( handlers.delegateCount++, 0, handleObj );\n\t\t\t} else {\n\t\t\t\thandlers.push( handleObj );\n\t\t\t}\n\n\t\t\t// Keep track of which events have ever been used, for event optimization\n\t\t\tjQuery.event.global[ type ] = true;\n\t\t}\n\n\t},\n\n\t// Detach an event or set of events from an element\n\tremove: function( elem, types, handler, selector, mappedTypes ) {\n\n\t\tvar j, origCount, tmp,\n\t\t\tevents, t, handleObj,\n\t\t\tspecial, handlers, type, namespaces, origType,\n\t\t\telemData = dataPriv.hasData( elem ) && dataPriv.get( elem );\n\n\t\tif ( !elemData || !( events = elemData.events ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Once for each type.namespace in types; type may be omitted\n\t\ttypes = ( types || \"\" ).match( rnothtmlwhite ) || [ \"\" ];\n\t\tt = types.length;\n\t\twhile ( t-- ) {\n\t\t\ttmp = rtypenamespace.exec( types[ t ] ) || [];\n\t\t\ttype = origType = tmp[ 1 ];\n\t\t\tnamespaces = ( tmp[ 2 ] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// Unbind all events (on this namespace, if provided) for the element\n\t\t\tif ( !type ) {\n\t\t\t\tfor ( type in events ) {\n\t\t\t\t\tjQuery.event.remove( elem, type + types[ t ], handler, selector, true );\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\t\t\thandlers = events[ type ] || [];\n\t\t\ttmp = tmp[ 2 ] &&\n\t\t\t\tnew RegExp( \"(^|\\\\.)\" + namespaces.join( \"\\\\.(?:.*\\\\.|)\" ) + \"(\\\\.|$)\" );\n\n\t\t\t// Remove matching events\n\t\t\torigCount = j = handlers.length;\n\t\t\twhile ( j-- ) {\n\t\t\t\thandleObj = handlers[ j ];\n\n\t\t\t\tif ( ( mappedTypes || origType === handleObj.origType ) &&\n\t\t\t\t\t( !handler || handler.guid === handleObj.guid ) &&\n\t\t\t\t\t( !tmp || tmp.test( handleObj.namespace ) ) &&\n\t\t\t\t\t( !selector || selector === handleObj.selector ||\n\t\t\t\t\t\tselector === \"**\" && handleObj.selector ) ) {\n\t\t\t\t\thandlers.splice( j, 1 );\n\n\t\t\t\t\tif ( handleObj.selector ) {\n\t\t\t\t\t\thandlers.delegateCount--;\n\t\t\t\t\t}\n\t\t\t\t\tif ( special.remove ) {\n\t\t\t\t\t\tspecial.remove.call( elem, handleObj );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Remove generic event handler if we removed something and no more handlers exist\n\t\t\t// (avoids potential for endless recursion during removal of special event handlers)\n\t\t\tif ( origCount && !handlers.length ) {\n\t\t\t\tif ( !special.teardown ||\n\t\t\t\t\tspecial.teardown.call( elem, namespaces, elemData.handle ) === false ) {\n\n\t\t\t\t\tjQuery.removeEvent( elem, type, elemData.handle );\n\t\t\t\t}\n\n\t\t\t\tdelete events[ type ];\n\t\t\t}\n\t\t}\n\n\t\t// Remove data and the expando if it's no longer used\n\t\tif ( jQuery.isEmptyObject( events ) ) {\n\t\t\tdataPriv.remove( elem, \"handle events\" );\n\t\t}\n\t},\n\n\tdispatch: function( nativeEvent ) {\n\n\t\t// Make a writable jQuery.Event from the native event object\n\t\tvar event = jQuery.event.fix( nativeEvent );\n\n\t\tvar i, j, ret, matched, handleObj, handlerQueue,\n\t\t\targs = new Array( arguments.length ),\n\t\t\thandlers = ( dataPriv.get( this, \"events\" ) || {} )[ event.type ] || [],\n\t\t\tspecial = jQuery.event.special[ event.type ] || {};\n\n\t\t// Use the fix-ed jQuery.Event rather than the (read-only) native event\n\t\targs[ 0 ] = event;\n\n\t\tfor ( i = 1; i < arguments.length; i++ ) {\n\t\t\targs[ i ] = arguments[ i ];\n\t\t}\n\n\t\tevent.delegateTarget = this;\n\n\t\t// Call the preDispatch hook for the mapped type, and let it bail if desired\n\t\tif ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine handlers\n\t\thandlerQueue = jQuery.event.handlers.call( this, event, handlers );\n\n\t\t// Run delegates first; they may want to stop propagation beneath us\n\t\ti = 0;\n\t\twhile ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {\n\t\t\tevent.currentTarget = matched.elem;\n\n\t\t\tj = 0;\n\t\t\twhile ( ( handleObj = matched.handlers[ j++ ] ) &&\n\t\t\t\t!event.isImmediatePropagationStopped() ) {\n\n\t\t\t\t// Triggered event must either 1) have no namespace, or 2) have namespace(s)\n\t\t\t\t// a subset or equal to those in the bound event (both can have no namespace).\n\t\t\t\tif ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) {\n\n\t\t\t\t\tevent.handleObj = handleObj;\n\t\t\t\t\tevent.data = handleObj.data;\n\n\t\t\t\t\tret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||\n\t\t\t\t\t\thandleObj.handler ).apply( matched.elem, args );\n\n\t\t\t\t\tif ( ret !== undefined ) {\n\t\t\t\t\t\tif ( ( event.result = ret ) === false ) {\n\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Call the postDispatch hook for the mapped type\n\t\tif ( special.postDispatch ) {\n\t\t\tspecial.postDispatch.call( this, event );\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\thandlers: function( event, handlers ) {\n\t\tvar i, handleObj, sel, matchedHandlers, matchedSelectors,\n\t\t\thandlerQueue = [],\n\t\t\tdelegateCount = handlers.delegateCount,\n\t\t\tcur = event.target;\n\n\t\t// Find delegate handlers\n\t\tif ( delegateCount &&\n\n\t\t\t// Support: IE <=9\n\t\t\t// Black-hole SVG <use> instance trees (trac-13180)\n\t\t\tcur.nodeType &&\n\n\t\t\t// Support: Firefox <=42\n\t\t\t// Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861)\n\t\t\t// https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click\n\t\t\t// Support: IE 11 only\n\t\t\t// ...but not arrow key \"clicks\" of radio inputs, which can have `button` -1 (gh-2343)\n\t\t\t!( event.type === \"click\" && event.button >= 1 ) ) {\n\n\t\t\tfor ( ; cur !== this; cur = cur.parentNode || this ) {\n\n\t\t\t\t// Don't check non-elements (#13208)\n\t\t\t\t// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)\n\t\t\t\tif ( cur.nodeType === 1 && !( event.type === \"click\" && cur.disabled === true ) ) {\n\t\t\t\t\tmatchedHandlers = [];\n\t\t\t\t\tmatchedSelectors = {};\n\t\t\t\t\tfor ( i = 0; i < delegateCount; i++ ) {\n\t\t\t\t\t\thandleObj = handlers[ i ];\n\n\t\t\t\t\t\t// Don't conflict with Object.prototype properties (#13203)\n\t\t\t\t\t\tsel = handleObj.selector + \" \";\n\n\t\t\t\t\t\tif ( matchedSelectors[ sel ] === undefined ) {\n\t\t\t\t\t\t\tmatchedSelectors[ sel ] = handleObj.needsContext ?\n\t\t\t\t\t\t\t\tjQuery( sel, this ).index( cur ) > -1 :\n\t\t\t\t\t\t\t\tjQuery.find( sel, this, null, [ cur ] ).length;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ( matchedSelectors[ sel ] ) {\n\t\t\t\t\t\t\tmatchedHandlers.push( handleObj );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( matchedHandlers.length ) {\n\t\t\t\t\t\thandlerQueue.push( { elem: cur, handlers: matchedHandlers } );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add the remaining (directly-bound) handlers\n\t\tcur = this;\n\t\tif ( delegateCount < handlers.length ) {\n\t\t\thandlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } );\n\t\t}\n\n\t\treturn handlerQueue;\n\t},\n\n\taddProp: function( name, hook ) {\n\t\tObject.defineProperty( jQuery.Event.prototype, name, {\n\t\t\tenumerable: true,\n\t\t\tconfigurable: true,\n\n\t\t\tget: isFunction( hook ) ?\n\t\t\t\tfunction() {\n\t\t\t\t\tif ( this.originalEvent ) {\n\t\t\t\t\t\t\treturn hook( this.originalEvent );\n\t\t\t\t\t}\n\t\t\t\t} :\n\t\t\t\tfunction() {\n\t\t\t\t\tif ( this.originalEvent ) {\n\t\t\t\t\t\t\treturn this.originalEvent[ name ];\n\t\t\t\t\t}\n\t\t\t\t},\n\n\t\t\tset: function( value ) {\n\t\t\t\tObject.defineProperty( this, name, {\n\t\t\t\t\tenumerable: true,\n\t\t\t\t\tconfigurable: true,\n\t\t\t\t\twritable: true,\n\t\t\t\t\tvalue: value\n\t\t\t\t} );\n\t\t\t}\n\t\t} );\n\t},\n\n\tfix: function( originalEvent ) {\n\t\treturn originalEvent[ jQuery.expando ] ?\n\t\t\toriginalEvent :\n\t\t\tnew jQuery.Event( originalEvent );\n\t},\n\n\tspecial: {\n\t\tload: {\n\n\t\t\t// Prevent triggered image.load events from bubbling to window.load\n\t\t\tnoBubble: true\n\t\t},\n\t\tfocus: {\n\n\t\t\t// Fire native event if possible so blur/focus sequence is correct\n\t\t\ttrigger: function() {\n\t\t\t\tif ( this !== safeActiveElement() && this.focus ) {\n\t\t\t\t\tthis.focus();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t},\n\t\t\tdelegateType: \"focusin\"\n\t\t},\n\t\tblur: {\n\t\t\ttrigger: function() {\n\t\t\t\tif ( this === safeActiveElement() && this.blur ) {\n\t\t\t\t\tthis.blur();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t},\n\t\t\tdelegateType: \"focusout\"\n\t\t},\n\t\tclick: {\n\n\t\t\t// For checkbox, fire native event so checked state will be right\n\t\t\ttrigger: function() {\n\t\t\t\tif ( this.type === \"checkbox\" && this.click && nodeName( this, \"input\" ) ) {\n\t\t\t\t\tthis.click();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t},\n\n\t\t\t// For cross-browser consistency, don't fire native .click() on links\n\t\t\t_default: function( event ) {\n\t\t\t\treturn nodeName( event.target, \"a\" );\n\t\t\t}\n\t\t},\n\n\t\tbeforeunload: {\n\t\t\tpostDispatch: function( event ) {\n\n\t\t\t\t// Support: Firefox 20+\n\t\t\t\t// Firefox doesn't alert if the returnValue field is not set.\n\t\t\t\tif ( event.result !== undefined && event.originalEvent ) {\n\t\t\t\t\tevent.originalEvent.returnValue = event.result;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n};\n\njQuery.removeEvent = function( elem, type, handle ) {\n\n\t// This \"if\" is needed for plain objects\n\tif ( elem.removeEventListener ) {\n\t\telem.removeEventListener( type, handle );\n\t}\n};\n\njQuery.Event = function( src, props ) {\n\n\t// Allow instantiation without the 'new' keyword\n\tif ( !( this instanceof jQuery.Event ) ) {\n\t\treturn new jQuery.Event( src, props );\n\t}\n\n\t// Event object\n\tif ( src && src.type ) {\n\t\tthis.originalEvent = src;\n\t\tthis.type = src.type;\n\n\t\t// Events bubbling up the document may have been marked as prevented\n\t\t// by a handler lower down the tree; reflect the correct value.\n\t\tthis.isDefaultPrevented = src.defaultPrevented ||\n\t\t\t\tsrc.defaultPrevented === undefined &&\n\n\t\t\t\t// Support: Android <=2.3 only\n\t\t\t\tsrc.returnValue === false ?\n\t\t\treturnTrue :\n\t\t\treturnFalse;\n\n\t\t// Create target properties\n\t\t// Support: Safari <=6 - 7 only\n\t\t// Target should not be a text node (#504, #13143)\n\t\tthis.target = ( src.target && src.target.nodeType === 3 ) ?\n\t\t\tsrc.target.parentNode :\n\t\t\tsrc.target;\n\n\t\tthis.currentTarget = src.currentTarget;\n\t\tthis.relatedTarget = src.relatedTarget;\n\n\t// Event type\n\t} else {\n\t\tthis.type = src;\n\t}\n\n\t// Put explicitly provided properties onto the event object\n\tif ( props ) {\n\t\tjQuery.extend( this, props );\n\t}\n\n\t// Create a timestamp if incoming event doesn't have one\n\tthis.timeStamp = src && src.timeStamp || Date.now();\n\n\t// Mark it as fixed\n\tthis[ jQuery.expando ] = true;\n};\n\n// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding\n// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html\njQuery.Event.prototype = {\n\tconstructor: jQuery.Event,\n\tisDefaultPrevented: returnFalse,\n\tisPropagationStopped: returnFalse,\n\tisImmediatePropagationStopped: returnFalse,\n\tisSimulated: false,\n\n\tpreventDefault: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isDefaultPrevented = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.preventDefault();\n\t\t}\n\t},\n\tstopPropagation: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isPropagationStopped = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.stopPropagation();\n\t\t}\n\t},\n\tstopImmediatePropagation: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isImmediatePropagationStopped = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.stopImmediatePropagation();\n\t\t}\n\n\t\tthis.stopPropagation();\n\t}\n};\n\n// Includes all common event props including KeyEvent and MouseEvent specific props\njQuery.each( {\n\taltKey: true,\n\tbubbles: true,\n\tcancelable: true,\n\tchangedTouches: true,\n\tctrlKey: true,\n\tdetail: true,\n\teventPhase: true,\n\tmetaKey: true,\n\tpageX: true,\n\tpageY: true,\n\tshiftKey: true,\n\tview: true,\n\t\"char\": true,\n\tcharCode: true,\n\tkey: true,\n\tkeyCode: true,\n\tbutton: true,\n\tbuttons: true,\n\tclientX: true,\n\tclientY: true,\n\toffsetX: true,\n\toffsetY: true,\n\tpointerId: true,\n\tpointerType: true,\n\tscreenX: true,\n\tscreenY: true,\n\ttargetTouches: true,\n\ttoElement: true,\n\ttouches: true,\n\n\twhich: function( event ) {\n\t\tvar button = event.button;\n\n\t\t// Add which for key events\n\t\tif ( event.which == null && rkeyEvent.test( event.type ) ) {\n\t\t\treturn event.charCode != null ? event.charCode : event.keyCode;\n\t\t}\n\n\t\t// Add which for click: 1 === left; 2 === middle; 3 === right\n\t\tif ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) {\n\t\t\tif ( button & 1 ) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\tif ( button & 2 ) {\n\t\t\t\treturn 3;\n\t\t\t}\n\n\t\t\tif ( button & 4 ) {\n\t\t\t\treturn 2;\n\t\t\t}\n\n\t\t\treturn 0;\n\t\t}\n\n\t\treturn event.which;\n\t}\n}, jQuery.event.addProp );\n\n// Create mouseenter/leave events using mouseover/out and event-time checks\n// so that event delegation works in jQuery.\n// Do the same for pointerenter/pointerleave and pointerover/pointerout\n//\n// Support: Safari 7 only\n// Safari sends mouseenter too often; see:\n// https://bugs.chromium.org/p/chromium/issues/detail?id=470258\n// for the description of the bug (it existed in older Chrome versions as well).\njQuery.each( {\n\tmouseenter: \"mouseover\",\n\tmouseleave: \"mouseout\",\n\tpointerenter: \"pointerover\",\n\tpointerleave: \"pointerout\"\n}, function( orig, fix ) {\n\tjQuery.event.special[ orig ] = {\n\t\tdelegateType: fix,\n\t\tbindType: fix,\n\n\t\thandle: function( event ) {\n\t\t\tvar ret,\n\t\t\t\ttarget = this,\n\t\t\t\trelated = event.relatedTarget,\n\t\t\t\thandleObj = event.handleObj;\n\n\t\t\t// For mouseenter/leave call the handler if related is outside the target.\n\t\t\t// NB: No relatedTarget if the mouse left/entered the browser window\n\t\t\tif ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) {\n\t\t\t\tevent.type = handleObj.origType;\n\t\t\t\tret = handleObj.handler.apply( this, arguments );\n\t\t\t\tevent.type = fix;\n\t\t\t}\n\t\t\treturn ret;\n\t\t}\n\t};\n} );\n\njQuery.fn.extend( {\n\n\ton: function( types, selector, data, fn ) {\n\t\treturn on( this, types, selector, data, fn );\n\t},\n\tone: function( types, selector, data, fn ) {\n\t\treturn on( this, types, selector, data, fn, 1 );\n\t},\n\toff: function( types, selector, fn ) {\n\t\tvar handleObj, type;\n\t\tif ( types && types.preventDefault && types.handleObj ) {\n\n\t\t\t// ( event )  dispatched jQuery.Event\n\t\t\thandleObj = types.handleObj;\n\t\t\tjQuery( types.delegateTarget ).off(\n\t\t\t\thandleObj.namespace ?\n\t\t\t\t\thandleObj.origType + \".\" + handleObj.namespace :\n\t\t\t\t\thandleObj.origType,\n\t\t\t\thandleObj.selector,\n\t\t\t\thandleObj.handler\n\t\t\t);\n\t\t\treturn this;\n\t\t}\n\t\tif ( typeof types === \"object\" ) {\n\n\t\t\t// ( types-object [, selector] )\n\t\t\tfor ( type in types ) {\n\t\t\t\tthis.off( type, selector, types[ type ] );\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\t\tif ( selector === false || typeof selector === \"function\" ) {\n\n\t\t\t// ( types [, fn] )\n\t\t\tfn = selector;\n\t\t\tselector = undefined;\n\t\t}\n\t\tif ( fn === false ) {\n\t\t\tfn = returnFalse;\n\t\t}\n\t\treturn this.each( function() {\n\t\t\tjQuery.event.remove( this, types, fn, selector );\n\t\t} );\n\t}\n} );\n\n\nvar\n\n\t/* eslint-disable max-len */\n\n\t// See https://github.com/eslint/eslint/issues/3229\n\trxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\\/\\0>\\x20\\t\\r\\n\\f]*)[^>]*)\\/>/gi,\n\n\t/* eslint-enable */\n\n\t// Support: IE <=10 - 11, Edge 12 - 13 only\n\t// In IE/Edge using regex groups here causes severe slowdowns.\n\t// See https://connect.microsoft.com/IE/feedback/details/1736512/\n\trnoInnerhtml = /<script|<style|<link/i,\n\n\t// checked=\"checked\" or checked\n\trchecked = /checked\\s*(?:[^=]|=\\s*.checked.)/i,\n\trcleanScript = /^\\s*<!(?:\\[CDATA\\[|--)|(?:\\]\\]|--)>\\s*$/g;\n\n// Prefer a tbody over its parent table for containing new rows\nfunction manipulationTarget( elem, content ) {\n\tif ( nodeName( elem, \"table\" ) &&\n\t\tnodeName( content.nodeType !== 11 ? content : content.firstChild, \"tr\" ) ) {\n\n\t\treturn jQuery( elem ).children( \"tbody\" )[ 0 ] || elem;\n\t}\n\n\treturn elem;\n}\n\n// Replace/restore the type attribute of script elements for safe DOM manipulation\nfunction disableScript( elem ) {\n\telem.type = ( elem.getAttribute( \"type\" ) !== null ) + \"/\" + elem.type;\n\treturn elem;\n}\nfunction restoreScript( elem ) {\n\tif ( ( elem.type || \"\" ).slice( 0, 5 ) === \"true/\" ) {\n\t\telem.type = elem.type.slice( 5 );\n\t} else {\n\t\telem.removeAttribute( \"type\" );\n\t}\n\n\treturn elem;\n}\n\nfunction cloneCopyEvent( src, dest ) {\n\tvar i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;\n\n\tif ( dest.nodeType !== 1 ) {\n\t\treturn;\n\t}\n\n\t// 1. Copy private data: events, handlers, etc.\n\tif ( dataPriv.hasData( src ) ) {\n\t\tpdataOld = dataPriv.access( src );\n\t\tpdataCur = dataPriv.set( dest, pdataOld );\n\t\tevents = pdataOld.events;\n\n\t\tif ( events ) {\n\t\t\tdelete pdataCur.handle;\n\t\t\tpdataCur.events = {};\n\n\t\t\tfor ( type in events ) {\n\t\t\t\tfor ( i = 0, l = events[ type ].length; i < l; i++ ) {\n\t\t\t\t\tjQuery.event.add( dest, type, events[ type ][ i ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// 2. Copy user data\n\tif ( dataUser.hasData( src ) ) {\n\t\tudataOld = dataUser.access( src );\n\t\tudataCur = jQuery.extend( {}, udataOld );\n\n\t\tdataUser.set( dest, udataCur );\n\t}\n}\n\n// Fix IE bugs, see support tests\nfunction fixInput( src, dest ) {\n\tvar nodeName = dest.nodeName.toLowerCase();\n\n\t// Fails to persist the checked state of a cloned checkbox or radio button.\n\tif ( nodeName === \"input\" && rcheckableType.test( src.type ) ) {\n\t\tdest.checked = src.checked;\n\n\t// Fails to return the selected option to the default selected state when cloning options\n\t} else if ( nodeName === \"input\" || nodeName === \"textarea\" ) {\n\t\tdest.defaultValue = src.defaultValue;\n\t}\n}\n\nfunction domManip( collection, args, callback, ignored ) {\n\n\t// Flatten any nested arrays\n\targs = concat.apply( [], args );\n\n\tvar fragment, first, scripts, hasScripts, node, doc,\n\t\ti = 0,\n\t\tl = collection.length,\n\t\tiNoClone = l - 1,\n\t\tvalue = args[ 0 ],\n\t\tvalueIsFunction = isFunction( value );\n\n\t// We can't cloneNode fragments that contain checked, in WebKit\n\tif ( valueIsFunction ||\n\t\t\t( l > 1 && typeof value === \"string\" &&\n\t\t\t\t!support.checkClone && rchecked.test( value ) ) ) {\n\t\treturn collection.each( function( index ) {\n\t\t\tvar self = collection.eq( index );\n\t\t\tif ( valueIsFunction ) {\n\t\t\t\targs[ 0 ] = value.call( this, index, self.html() );\n\t\t\t}\n\t\t\tdomManip( self, args, callback, ignored );\n\t\t} );\n\t}\n\n\tif ( l ) {\n\t\tfragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored );\n\t\tfirst = fragment.firstChild;\n\n\t\tif ( fragment.childNodes.length === 1 ) {\n\t\t\tfragment = first;\n\t\t}\n\n\t\t// Require either new content or an interest in ignored elements to invoke the callback\n\t\tif ( first || ignored ) {\n\t\t\tscripts = jQuery.map( getAll( fragment, \"script\" ), disableScript );\n\t\t\thasScripts = scripts.length;\n\n\t\t\t// Use the original fragment for the last item\n\t\t\t// instead of the first because it can end up\n\t\t\t// being emptied incorrectly in certain situations (#8070).\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tnode = fragment;\n\n\t\t\t\tif ( i !== iNoClone ) {\n\t\t\t\t\tnode = jQuery.clone( node, true, true );\n\n\t\t\t\t\t// Keep references to cloned scripts for later restoration\n\t\t\t\t\tif ( hasScripts ) {\n\n\t\t\t\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t\t\t\t// push.apply(_, arraylike) throws on ancient WebKit\n\t\t\t\t\t\tjQuery.merge( scripts, getAll( node, \"script\" ) );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcallback.call( collection[ i ], node, i );\n\t\t\t}\n\n\t\t\tif ( hasScripts ) {\n\t\t\t\tdoc = scripts[ scripts.length - 1 ].ownerDocument;\n\n\t\t\t\t// Reenable scripts\n\t\t\t\tjQuery.map( scripts, restoreScript );\n\n\t\t\t\t// Evaluate executable scripts on first document insertion\n\t\t\t\tfor ( i = 0; i < hasScripts; i++ ) {\n\t\t\t\t\tnode = scripts[ i ];\n\t\t\t\t\tif ( rscriptType.test( node.type || \"\" ) &&\n\t\t\t\t\t\t!dataPriv.access( node, \"globalEval\" ) &&\n\t\t\t\t\t\tjQuery.contains( doc, node ) ) {\n\n\t\t\t\t\t\tif ( node.src && ( node.type || \"\" ).toLowerCase()  !== \"module\" ) {\n\n\t\t\t\t\t\t\t// Optional AJAX dependency, but won't run scripts if not present\n\t\t\t\t\t\t\tif ( jQuery._evalUrl ) {\n\t\t\t\t\t\t\t\tjQuery._evalUrl( node.src );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tDOMEval( node.textContent.replace( rcleanScript, \"\" ), doc, node );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn collection;\n}\n\nfunction remove( elem, selector, keepData ) {\n\tvar node,\n\t\tnodes = selector ? jQuery.filter( selector, elem ) : elem,\n\t\ti = 0;\n\n\tfor ( ; ( node = nodes[ i ] ) != null; i++ ) {\n\t\tif ( !keepData && node.nodeType === 1 ) {\n\t\t\tjQuery.cleanData( getAll( node ) );\n\t\t}\n\n\t\tif ( node.parentNode ) {\n\t\t\tif ( keepData && jQuery.contains( node.ownerDocument, node ) ) {\n\t\t\t\tsetGlobalEval( getAll( node, \"script\" ) );\n\t\t\t}\n\t\t\tnode.parentNode.removeChild( node );\n\t\t}\n\t}\n\n\treturn elem;\n}\n\njQuery.extend( {\n\thtmlPrefilter: function( html ) {\n\t\treturn html.replace( rxhtmlTag, \"<$1></$2>\" );\n\t},\n\n\tclone: function( elem, dataAndEvents, deepDataAndEvents ) {\n\t\tvar i, l, srcElements, destElements,\n\t\t\tclone = elem.cloneNode( true ),\n\t\t\tinPage = jQuery.contains( elem.ownerDocument, elem );\n\n\t\t// Fix IE cloning issues\n\t\tif ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&\n\t\t\t\t!jQuery.isXMLDoc( elem ) ) {\n\n\t\t\t// We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2\n\t\t\tdestElements = getAll( clone );\n\t\t\tsrcElements = getAll( elem );\n\n\t\t\tfor ( i = 0, l = srcElements.length; i < l; i++ ) {\n\t\t\t\tfixInput( srcElements[ i ], destElements[ i ] );\n\t\t\t}\n\t\t}\n\n\t\t// Copy the events from the original to the clone\n\t\tif ( dataAndEvents ) {\n\t\t\tif ( deepDataAndEvents ) {\n\t\t\t\tsrcElements = srcElements || getAll( elem );\n\t\t\t\tdestElements = destElements || getAll( clone );\n\n\t\t\t\tfor ( i = 0, l = srcElements.length; i < l; i++ ) {\n\t\t\t\t\tcloneCopyEvent( srcElements[ i ], destElements[ i ] );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcloneCopyEvent( elem, clone );\n\t\t\t}\n\t\t}\n\n\t\t// Preserve script evaluation history\n\t\tdestElements = getAll( clone, \"script\" );\n\t\tif ( destElements.length > 0 ) {\n\t\t\tsetGlobalEval( destElements, !inPage && getAll( elem, \"script\" ) );\n\t\t}\n\n\t\t// Return the cloned set\n\t\treturn clone;\n\t},\n\n\tcleanData: function( elems ) {\n\t\tvar data, elem, type,\n\t\t\tspecial = jQuery.event.special,\n\t\t\ti = 0;\n\n\t\tfor ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {\n\t\t\tif ( acceptData( elem ) ) {\n\t\t\t\tif ( ( data = elem[ dataPriv.expando ] ) ) {\n\t\t\t\t\tif ( data.events ) {\n\t\t\t\t\t\tfor ( type in data.events ) {\n\t\t\t\t\t\t\tif ( special[ type ] ) {\n\t\t\t\t\t\t\t\tjQuery.event.remove( elem, type );\n\n\t\t\t\t\t\t\t// This is a shortcut to avoid jQuery.event.remove's overhead\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tjQuery.removeEvent( elem, type, data.handle );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Support: Chrome <=35 - 45+\n\t\t\t\t\t// Assign undefined instead of using delete, see Data#remove\n\t\t\t\t\telem[ dataPriv.expando ] = undefined;\n\t\t\t\t}\n\t\t\t\tif ( elem[ dataUser.expando ] ) {\n\n\t\t\t\t\t// Support: Chrome <=35 - 45+\n\t\t\t\t\t// Assign undefined instead of using delete, see Data#remove\n\t\t\t\t\telem[ dataUser.expando ] = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n} );\n\njQuery.fn.extend( {\n\tdetach: function( selector ) {\n\t\treturn remove( this, selector, true );\n\t},\n\n\tremove: function( selector ) {\n\t\treturn remove( this, selector );\n\t},\n\n\ttext: function( value ) {\n\t\treturn access( this, function( value ) {\n\t\t\treturn value === undefined ?\n\t\t\t\tjQuery.text( this ) :\n\t\t\t\tthis.empty().each( function() {\n\t\t\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\t\t\tthis.textContent = value;\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t}, null, value, arguments.length );\n\t},\n\n\tappend: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\tvar target = manipulationTarget( this, elem );\n\t\t\t\ttarget.appendChild( elem );\n\t\t\t}\n\t\t} );\n\t},\n\n\tprepend: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\tvar target = manipulationTarget( this, elem );\n\t\t\t\ttarget.insertBefore( elem, target.firstChild );\n\t\t\t}\n\t\t} );\n\t},\n\n\tbefore: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.parentNode ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this );\n\t\t\t}\n\t\t} );\n\t},\n\n\tafter: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.parentNode ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this.nextSibling );\n\t\t\t}\n\t\t} );\n\t},\n\n\tempty: function() {\n\t\tvar elem,\n\t\t\ti = 0;\n\n\t\tfor ( ; ( elem = this[ i ] ) != null; i++ ) {\n\t\t\tif ( elem.nodeType === 1 ) {\n\n\t\t\t\t// Prevent memory leaks\n\t\t\t\tjQuery.cleanData( getAll( elem, false ) );\n\n\t\t\t\t// Remove any remaining nodes\n\t\t\t\telem.textContent = \"\";\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tclone: function( dataAndEvents, deepDataAndEvents ) {\n\t\tdataAndEvents = dataAndEvents == null ? false : dataAndEvents;\n\t\tdeepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;\n\n\t\treturn this.map( function() {\n\t\t\treturn jQuery.clone( this, dataAndEvents, deepDataAndEvents );\n\t\t} );\n\t},\n\n\thtml: function( value ) {\n\t\treturn access( this, function( value ) {\n\t\t\tvar elem = this[ 0 ] || {},\n\t\t\t\ti = 0,\n\t\t\t\tl = this.length;\n\n\t\t\tif ( value === undefined && elem.nodeType === 1 ) {\n\t\t\t\treturn elem.innerHTML;\n\t\t\t}\n\n\t\t\t// See if we can take a shortcut and just use innerHTML\n\t\t\tif ( typeof value === \"string\" && !rnoInnerhtml.test( value ) &&\n\t\t\t\t!wrapMap[ ( rtagName.exec( value ) || [ \"\", \"\" ] )[ 1 ].toLowerCase() ] ) {\n\n\t\t\t\tvalue = jQuery.htmlPrefilter( value );\n\n\t\t\t\ttry {\n\t\t\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\t\t\telem = this[ i ] || {};\n\n\t\t\t\t\t\t// Remove element nodes and prevent memory leaks\n\t\t\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\t\t\tjQuery.cleanData( getAll( elem, false ) );\n\t\t\t\t\t\t\telem.innerHTML = value;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\telem = 0;\n\n\t\t\t\t// If using innerHTML throws an exception, use the fallback method\n\t\t\t\t} catch ( e ) {}\n\t\t\t}\n\n\t\t\tif ( elem ) {\n\t\t\t\tthis.empty().append( value );\n\t\t\t}\n\t\t}, null, value, arguments.length );\n\t},\n\n\treplaceWith: function() {\n\t\tvar ignored = [];\n\n\t\t// Make the changes, replacing each non-ignored context element with the new content\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tvar parent = this.parentNode;\n\n\t\t\tif ( jQuery.inArray( this, ignored ) < 0 ) {\n\t\t\t\tjQuery.cleanData( getAll( this ) );\n\t\t\t\tif ( parent ) {\n\t\t\t\t\tparent.replaceChild( elem, this );\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Force callback invocation\n\t\t}, ignored );\n\t}\n} );\n\njQuery.each( {\n\tappendTo: \"append\",\n\tprependTo: \"prepend\",\n\tinsertBefore: \"before\",\n\tinsertAfter: \"after\",\n\treplaceAll: \"replaceWith\"\n}, function( name, original ) {\n\tjQuery.fn[ name ] = function( selector ) {\n\t\tvar elems,\n\t\t\tret = [],\n\t\t\tinsert = jQuery( selector ),\n\t\t\tlast = insert.length - 1,\n\t\t\ti = 0;\n\n\t\tfor ( ; i <= last; i++ ) {\n\t\t\telems = i === last ? this : this.clone( true );\n\t\t\tjQuery( insert[ i ] )[ original ]( elems );\n\n\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t// .get() because push.apply(_, arraylike) throws on ancient WebKit\n\t\t\tpush.apply( ret, elems.get() );\n\t\t}\n\n\t\treturn this.pushStack( ret );\n\t};\n} );\nvar rnumnonpx = new RegExp( \"^(\" + pnum + \")(?!px)[a-z%]+$\", \"i\" );\n\nvar getStyles = function( elem ) {\n\n\t\t// Support: IE <=11 only, Firefox <=30 (#15098, #14150)\n\t\t// IE throws on elements created in popups\n\t\t// FF meanwhile throws on frame elements through \"defaultView.getComputedStyle\"\n\t\tvar view = elem.ownerDocument.defaultView;\n\n\t\tif ( !view || !view.opener ) {\n\t\t\tview = window;\n\t\t}\n\n\t\treturn view.getComputedStyle( elem );\n\t};\n\nvar rboxStyle = new RegExp( cssExpand.join( \"|\" ), \"i\" );\n\n\n\n( function() {\n\n\t// Executing both pixelPosition & boxSizingReliable tests require only one layout\n\t// so they're executed at the same time to save the second computation.\n\tfunction computeStyleTests() {\n\n\t\t// This is a singleton, we need to execute it only once\n\t\tif ( !div ) {\n\t\t\treturn;\n\t\t}\n\n\t\tcontainer.style.cssText = \"position:absolute;left:-11111px;width:60px;\" +\n\t\t\t\"margin-top:1px;padding:0;border:0\";\n\t\tdiv.style.cssText =\n\t\t\t\"position:relative;display:block;box-sizing:border-box;overflow:scroll;\" +\n\t\t\t\"margin:auto;border:1px;padding:1px;\" +\n\t\t\t\"width:60%;top:1%\";\n\t\tdocumentElement.appendChild( container ).appendChild( div );\n\n\t\tvar divStyle = window.getComputedStyle( div );\n\t\tpixelPositionVal = divStyle.top !== \"1%\";\n\n\t\t// Support: Android 4.0 - 4.3 only, Firefox <=3 - 44\n\t\treliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12;\n\n\t\t// Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3\n\t\t// Some styles come back with percentage values, even though they shouldn't\n\t\tdiv.style.right = \"60%\";\n\t\tpixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36;\n\n\t\t// Support: IE 9 - 11 only\n\t\t// Detect misreporting of content dimensions for box-sizing:border-box elements\n\t\tboxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36;\n\n\t\t// Support: IE 9 only\n\t\t// Detect overflow:scroll screwiness (gh-3699)\n\t\tdiv.style.position = \"absolute\";\n\t\tscrollboxSizeVal = div.offsetWidth === 36 || \"absolute\";\n\n\t\tdocumentElement.removeChild( container );\n\n\t\t// Nullify the div so it wouldn't be stored in the memory and\n\t\t// it will also be a sign that checks already performed\n\t\tdiv = null;\n\t}\n\n\tfunction roundPixelMeasures( measure ) {\n\t\treturn Math.round( parseFloat( measure ) );\n\t}\n\n\tvar pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal,\n\t\treliableMarginLeftVal,\n\t\tcontainer = document.createElement( \"div\" ),\n\t\tdiv = document.createElement( \"div\" );\n\n\t// Finish early in limited (non-browser) environments\n\tif ( !div.style ) {\n\t\treturn;\n\t}\n\n\t// Support: IE <=9 - 11 only\n\t// Style of cloned element affects source element cloned (#8908)\n\tdiv.style.backgroundClip = \"content-box\";\n\tdiv.cloneNode( true ).style.backgroundClip = \"\";\n\tsupport.clearCloneStyle = div.style.backgroundClip === \"content-box\";\n\n\tjQuery.extend( support, {\n\t\tboxSizingReliable: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn boxSizingReliableVal;\n\t\t},\n\t\tpixelBoxStyles: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn pixelBoxStylesVal;\n\t\t},\n\t\tpixelPosition: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn pixelPositionVal;\n\t\t},\n\t\treliableMarginLeft: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn reliableMarginLeftVal;\n\t\t},\n\t\tscrollboxSize: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn scrollboxSizeVal;\n\t\t}\n\t} );\n} )();\n\n\nfunction curCSS( elem, name, computed ) {\n\tvar width, minWidth, maxWidth, ret,\n\n\t\t// Support: Firefox 51+\n\t\t// Retrieving style before computed somehow\n\t\t// fixes an issue with getting wrong values\n\t\t// on detached elements\n\t\tstyle = elem.style;\n\n\tcomputed = computed || getStyles( elem );\n\n\t// getPropertyValue is needed for:\n\t//   .css('filter') (IE 9 only, #12537)\n\t//   .css('--customProperty) (#3144)\n\tif ( computed ) {\n\t\tret = computed.getPropertyValue( name ) || computed[ name ];\n\n\t\tif ( ret === \"\" && !jQuery.contains( elem.ownerDocument, elem ) ) {\n\t\t\tret = jQuery.style( elem, name );\n\t\t}\n\n\t\t// A tribute to the \"awesome hack by Dean Edwards\"\n\t\t// Android Browser returns percentage for some values,\n\t\t// but width seems to be reliably pixels.\n\t\t// This is against the CSSOM draft spec:\n\t\t// https://drafts.csswg.org/cssom/#resolved-values\n\t\tif ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) {\n\n\t\t\t// Remember the original values\n\t\t\twidth = style.width;\n\t\t\tminWidth = style.minWidth;\n\t\t\tmaxWidth = style.maxWidth;\n\n\t\t\t// Put in the new values to get a computed value out\n\t\t\tstyle.minWidth = style.maxWidth = style.width = ret;\n\t\t\tret = computed.width;\n\n\t\t\t// Revert the changed values\n\t\t\tstyle.width = width;\n\t\t\tstyle.minWidth = minWidth;\n\t\t\tstyle.maxWidth = maxWidth;\n\t\t}\n\t}\n\n\treturn ret !== undefined ?\n\n\t\t// Support: IE <=9 - 11 only\n\t\t// IE returns zIndex value as an integer.\n\t\tret + \"\" :\n\t\tret;\n}\n\n\nfunction addGetHookIf( conditionFn, hookFn ) {\n\n\t// Define the hook, we'll check on the first run if it's really needed.\n\treturn {\n\t\tget: function() {\n\t\t\tif ( conditionFn() ) {\n\n\t\t\t\t// Hook not needed (or it's not possible to use it due\n\t\t\t\t// to missing dependency), remove it.\n\t\t\t\tdelete this.get;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Hook needed; redefine it so that the support test is not executed again.\n\t\t\treturn ( this.get = hookFn ).apply( this, arguments );\n\t\t}\n\t};\n}\n\n\nvar\n\n\t// Swappable if display is none or starts with table\n\t// except \"table\", \"table-cell\", or \"table-caption\"\n\t// See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display\n\trdisplayswap = /^(none|table(?!-c[ea]).+)/,\n\trcustomProp = /^--/,\n\tcssShow = { position: \"absolute\", visibility: \"hidden\", display: \"block\" },\n\tcssNormalTransform = {\n\t\tletterSpacing: \"0\",\n\t\tfontWeight: \"400\"\n\t},\n\n\tcssPrefixes = [ \"Webkit\", \"Moz\", \"ms\" ],\n\temptyStyle = document.createElement( \"div\" ).style;\n\n// Return a css property mapped to a potentially vendor prefixed property\nfunction vendorPropName( name ) {\n\n\t// Shortcut for names that are not vendor prefixed\n\tif ( name in emptyStyle ) {\n\t\treturn name;\n\t}\n\n\t// Check for vendor prefixed names\n\tvar capName = name[ 0 ].toUpperCase() + name.slice( 1 ),\n\t\ti = cssPrefixes.length;\n\n\twhile ( i-- ) {\n\t\tname = cssPrefixes[ i ] + capName;\n\t\tif ( name in emptyStyle ) {\n\t\t\treturn name;\n\t\t}\n\t}\n}\n\n// Return a property mapped along what jQuery.cssProps suggests or to\n// a vendor prefixed property.\nfunction finalPropName( name ) {\n\tvar ret = jQuery.cssProps[ name ];\n\tif ( !ret ) {\n\t\tret = jQuery.cssProps[ name ] = vendorPropName( name ) || name;\n\t}\n\treturn ret;\n}\n\nfunction setPositiveNumber( elem, value, subtract ) {\n\n\t// Any relative (+/-) values have already been\n\t// normalized at this point\n\tvar matches = rcssNum.exec( value );\n\treturn matches ?\n\n\t\t// Guard against undefined \"subtract\", e.g., when used as in cssHooks\n\t\tMath.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || \"px\" ) :\n\t\tvalue;\n}\n\nfunction boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) {\n\tvar i = dimension === \"width\" ? 1 : 0,\n\t\textra = 0,\n\t\tdelta = 0;\n\n\t// Adjustment may not be necessary\n\tif ( box === ( isBorderBox ? \"border\" : \"content\" ) ) {\n\t\treturn 0;\n\t}\n\n\tfor ( ; i < 4; i += 2 ) {\n\n\t\t// Both box models exclude margin\n\t\tif ( box === \"margin\" ) {\n\t\t\tdelta += jQuery.css( elem, box + cssExpand[ i ], true, styles );\n\t\t}\n\n\t\t// If we get here with a content-box, we're seeking \"padding\" or \"border\" or \"margin\"\n\t\tif ( !isBorderBox ) {\n\n\t\t\t// Add padding\n\t\t\tdelta += jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\n\t\t\t// For \"border\" or \"margin\", add border\n\t\t\tif ( box !== \"padding\" ) {\n\t\t\t\tdelta += jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\n\t\t\t// But still keep track of it otherwise\n\t\t\t} else {\n\t\t\t\textra += jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\t\t\t}\n\n\t\t// If we get here with a border-box (content + padding + border), we're seeking \"content\" or\n\t\t// \"padding\" or \"margin\"\n\t\t} else {\n\n\t\t\t// For \"content\", subtract padding\n\t\t\tif ( box === \"content\" ) {\n\t\t\t\tdelta -= jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\t\t\t}\n\n\t\t\t// For \"content\" or \"padding\", subtract border\n\t\t\tif ( box !== \"margin\" ) {\n\t\t\t\tdelta -= jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\t\t\t}\n\t\t}\n\t}\n\n\t// Account for positive content-box scroll gutter when requested by providing computedVal\n\tif ( !isBorderBox && computedVal >= 0 ) {\n\n\t\t// offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border\n\t\t// Assuming integer scroll gutter, subtract the rest and round down\n\t\tdelta += Math.max( 0, Math.ceil(\n\t\t\telem[ \"offset\" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -\n\t\t\tcomputedVal -\n\t\t\tdelta -\n\t\t\textra -\n\t\t\t0.5\n\t\t) );\n\t}\n\n\treturn delta;\n}\n\nfunction getWidthOrHeight( elem, dimension, extra ) {\n\n\t// Start with computed style\n\tvar styles = getStyles( elem ),\n\t\tval = curCSS( elem, dimension, styles ),\n\t\tisBorderBox = jQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\",\n\t\tvalueIsBorderBox = isBorderBox;\n\n\t// Support: Firefox <=54\n\t// Return a confounding non-pixel value or feign ignorance, as appropriate.\n\tif ( rnumnonpx.test( val ) ) {\n\t\tif ( !extra ) {\n\t\t\treturn val;\n\t\t}\n\t\tval = \"auto\";\n\t}\n\n\t// Check for style in case a browser which returns unreliable values\n\t// for getComputedStyle silently falls back to the reliable elem.style\n\tvalueIsBorderBox = valueIsBorderBox &&\n\t\t( support.boxSizingReliable() || val === elem.style[ dimension ] );\n\n\t// Fall back to offsetWidth/offsetHeight when value is \"auto\"\n\t// This happens for inline elements with no explicit setting (gh-3571)\n\t// Support: Android <=4.1 - 4.3 only\n\t// Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602)\n\tif ( val === \"auto\" ||\n\t\t!parseFloat( val ) && jQuery.css( elem, \"display\", false, styles ) === \"inline\" ) {\n\n\t\tval = elem[ \"offset\" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ];\n\n\t\t// offsetWidth/offsetHeight provide border-box values\n\t\tvalueIsBorderBox = true;\n\t}\n\n\t// Normalize \"\" and auto\n\tval = parseFloat( val ) || 0;\n\n\t// Adjust for the element's box model\n\treturn ( val +\n\t\tboxModelAdjustment(\n\t\t\telem,\n\t\t\tdimension,\n\t\t\textra || ( isBorderBox ? \"border\" : \"content\" ),\n\t\t\tvalueIsBorderBox,\n\t\t\tstyles,\n\n\t\t\t// Provide the current computed size to request scroll gutter calculation (gh-3589)\n\t\t\tval\n\t\t)\n\t) + \"px\";\n}\n\njQuery.extend( {\n\n\t// Add in style property hooks for overriding the default\n\t// behavior of getting and setting a style property\n\tcssHooks: {\n\t\topacity: {\n\t\t\tget: function( elem, computed ) {\n\t\t\t\tif ( computed ) {\n\n\t\t\t\t\t// We should always get a number back from opacity\n\t\t\t\t\tvar ret = curCSS( elem, \"opacity\" );\n\t\t\t\t\treturn ret === \"\" ? \"1\" : ret;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\t// Don't automatically add \"px\" to these possibly-unitless properties\n\tcssNumber: {\n\t\t\"animationIterationCount\": true,\n\t\t\"columnCount\": true,\n\t\t\"fillOpacity\": true,\n\t\t\"flexGrow\": true,\n\t\t\"flexShrink\": true,\n\t\t\"fontWeight\": true,\n\t\t\"lineHeight\": true,\n\t\t\"opacity\": true,\n\t\t\"order\": true,\n\t\t\"orphans\": true,\n\t\t\"widows\": true,\n\t\t\"zIndex\": true,\n\t\t\"zoom\": true\n\t},\n\n\t// Add in properties whose names you wish to fix before\n\t// setting or getting the value\n\tcssProps: {},\n\n\t// Get and set the style property on a DOM Node\n\tstyle: function( elem, name, value, extra ) {\n\n\t\t// Don't set styles on text and comment nodes\n\t\tif ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Make sure that we're working with the right name\n\t\tvar ret, type, hooks,\n\t\t\torigName = camelCase( name ),\n\t\t\tisCustomProp = rcustomProp.test( name ),\n\t\t\tstyle = elem.style;\n\n\t\t// Make sure that we're working with the right name. We don't\n\t\t// want to query the value if it is a CSS custom property\n\t\t// since they are user-defined.\n\t\tif ( !isCustomProp ) {\n\t\t\tname = finalPropName( origName );\n\t\t}\n\n\t\t// Gets hook for the prefixed version, then unprefixed version\n\t\thooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n\t\t// Check if we're setting a value\n\t\tif ( value !== undefined ) {\n\t\t\ttype = typeof value;\n\n\t\t\t// Convert \"+=\" or \"-=\" to relative numbers (#7345)\n\t\t\tif ( type === \"string\" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {\n\t\t\t\tvalue = adjustCSS( elem, name, ret );\n\n\t\t\t\t// Fixes bug #9237\n\t\t\t\ttype = \"number\";\n\t\t\t}\n\n\t\t\t// Make sure that null and NaN values aren't set (#7116)\n\t\t\tif ( value == null || value !== value ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// If a number was passed in, add the unit (except for certain CSS properties)\n\t\t\tif ( type === \"number\" ) {\n\t\t\t\tvalue += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? \"\" : \"px\" );\n\t\t\t}\n\n\t\t\t// background-* props affect original clone's values\n\t\t\tif ( !support.clearCloneStyle && value === \"\" && name.indexOf( \"background\" ) === 0 ) {\n\t\t\t\tstyle[ name ] = \"inherit\";\n\t\t\t}\n\n\t\t\t// If a hook was provided, use that value, otherwise just set the specified value\n\t\t\tif ( !hooks || !( \"set\" in hooks ) ||\n\t\t\t\t( value = hooks.set( elem, value, extra ) ) !== undefined ) {\n\n\t\t\t\tif ( isCustomProp ) {\n\t\t\t\t\tstyle.setProperty( name, value );\n\t\t\t\t} else {\n\t\t\t\t\tstyle[ name ] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t} else {\n\n\t\t\t// If a hook was provided get the non-computed value from there\n\t\t\tif ( hooks && \"get\" in hooks &&\n\t\t\t\t( ret = hooks.get( elem, false, extra ) ) !== undefined ) {\n\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\t// Otherwise just get the value from the style object\n\t\t\treturn style[ name ];\n\t\t}\n\t},\n\n\tcss: function( elem, name, extra, styles ) {\n\t\tvar val, num, hooks,\n\t\t\torigName = camelCase( name ),\n\t\t\tisCustomProp = rcustomProp.test( name );\n\n\t\t// Make sure that we're working with the right name. We don't\n\t\t// want to modify the value if it is a CSS custom property\n\t\t// since they are user-defined.\n\t\tif ( !isCustomProp ) {\n\t\t\tname = finalPropName( origName );\n\t\t}\n\n\t\t// Try prefixed name followed by the unprefixed name\n\t\thooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n\t\t// If a hook was provided get the computed value from there\n\t\tif ( hooks && \"get\" in hooks ) {\n\t\t\tval = hooks.get( elem, true, extra );\n\t\t}\n\n\t\t// Otherwise, if a way to get the computed value exists, use that\n\t\tif ( val === undefined ) {\n\t\t\tval = curCSS( elem, name, styles );\n\t\t}\n\n\t\t// Convert \"normal\" to computed value\n\t\tif ( val === \"normal\" && name in cssNormalTransform ) {\n\t\t\tval = cssNormalTransform[ name ];\n\t\t}\n\n\t\t// Make numeric if forced or a qualifier was provided and val looks numeric\n\t\tif ( extra === \"\" || extra ) {\n\t\t\tnum = parseFloat( val );\n\t\t\treturn extra === true || isFinite( num ) ? num || 0 : val;\n\t\t}\n\n\t\treturn val;\n\t}\n} );\n\njQuery.each( [ \"height\", \"width\" ], function( i, dimension ) {\n\tjQuery.cssHooks[ dimension ] = {\n\t\tget: function( elem, computed, extra ) {\n\t\t\tif ( computed ) {\n\n\t\t\t\t// Certain elements can have dimension info if we invisibly show them\n\t\t\t\t// but it must have a current display style that would benefit\n\t\t\t\treturn rdisplayswap.test( jQuery.css( elem, \"display\" ) ) &&\n\n\t\t\t\t\t// Support: Safari 8+\n\t\t\t\t\t// Table columns in Safari have non-zero offsetWidth & zero\n\t\t\t\t\t// getBoundingClientRect().width unless display is changed.\n\t\t\t\t\t// Support: IE <=11 only\n\t\t\t\t\t// Running getBoundingClientRect on a disconnected node\n\t\t\t\t\t// in IE throws an error.\n\t\t\t\t\t( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ?\n\t\t\t\t\t\tswap( elem, cssShow, function() {\n\t\t\t\t\t\t\treturn getWidthOrHeight( elem, dimension, extra );\n\t\t\t\t\t\t} ) :\n\t\t\t\t\t\tgetWidthOrHeight( elem, dimension, extra );\n\t\t\t}\n\t\t},\n\n\t\tset: function( elem, value, extra ) {\n\t\t\tvar matches,\n\t\t\t\tstyles = getStyles( elem ),\n\t\t\t\tisBorderBox = jQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\",\n\t\t\t\tsubtract = extra && boxModelAdjustment(\n\t\t\t\t\telem,\n\t\t\t\t\tdimension,\n\t\t\t\t\textra,\n\t\t\t\t\tisBorderBox,\n\t\t\t\t\tstyles\n\t\t\t\t);\n\n\t\t\t// Account for unreliable border-box dimensions by comparing offset* to computed and\n\t\t\t// faking a content-box to get border and padding (gh-3699)\n\t\t\tif ( isBorderBox && support.scrollboxSize() === styles.position ) {\n\t\t\t\tsubtract -= Math.ceil(\n\t\t\t\t\telem[ \"offset\" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -\n\t\t\t\t\tparseFloat( styles[ dimension ] ) -\n\t\t\t\t\tboxModelAdjustment( elem, dimension, \"border\", false, styles ) -\n\t\t\t\t\t0.5\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Convert to pixels if value adjustment is needed\n\t\t\tif ( subtract && ( matches = rcssNum.exec( value ) ) &&\n\t\t\t\t( matches[ 3 ] || \"px\" ) !== \"px\" ) {\n\n\t\t\t\telem.style[ dimension ] = value;\n\t\t\t\tvalue = jQuery.css( elem, dimension );\n\t\t\t}\n\n\t\t\treturn setPositiveNumber( elem, value, subtract );\n\t\t}\n\t};\n} );\n\njQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,\n\tfunction( elem, computed ) {\n\t\tif ( computed ) {\n\t\t\treturn ( parseFloat( curCSS( elem, \"marginLeft\" ) ) ||\n\t\t\t\telem.getBoundingClientRect().left -\n\t\t\t\t\tswap( elem, { marginLeft: 0 }, function() {\n\t\t\t\t\t\treturn elem.getBoundingClientRect().left;\n\t\t\t\t\t} )\n\t\t\t\t) + \"px\";\n\t\t}\n\t}\n);\n\n// These hooks are used by animate to expand properties\njQuery.each( {\n\tmargin: \"\",\n\tpadding: \"\",\n\tborder: \"Width\"\n}, function( prefix, suffix ) {\n\tjQuery.cssHooks[ prefix + suffix ] = {\n\t\texpand: function( value ) {\n\t\t\tvar i = 0,\n\t\t\t\texpanded = {},\n\n\t\t\t\t// Assumes a single number if not a string\n\t\t\t\tparts = typeof value === \"string\" ? value.split( \" \" ) : [ value ];\n\n\t\t\tfor ( ; i < 4; i++ ) {\n\t\t\t\texpanded[ prefix + cssExpand[ i ] + suffix ] =\n\t\t\t\t\tparts[ i ] || parts[ i - 2 ] || parts[ 0 ];\n\t\t\t}\n\n\t\t\treturn expanded;\n\t\t}\n\t};\n\n\tif ( prefix !== \"margin\" ) {\n\t\tjQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;\n\t}\n} );\n\njQuery.fn.extend( {\n\tcss: function( name, value ) {\n\t\treturn access( this, function( elem, name, value ) {\n\t\t\tvar styles, len,\n\t\t\t\tmap = {},\n\t\t\t\ti = 0;\n\n\t\t\tif ( Array.isArray( name ) ) {\n\t\t\t\tstyles = getStyles( elem );\n\t\t\t\tlen = name.length;\n\n\t\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\t\tmap[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );\n\t\t\t\t}\n\n\t\t\t\treturn map;\n\t\t\t}\n\n\t\t\treturn value !== undefined ?\n\t\t\t\tjQuery.style( elem, name, value ) :\n\t\t\t\tjQuery.css( elem, name );\n\t\t}, name, value, arguments.length > 1 );\n\t}\n} );\n\n\nfunction Tween( elem, options, prop, end, easing ) {\n\treturn new Tween.prototype.init( elem, options, prop, end, easing );\n}\njQuery.Tween = Tween;\n\nTween.prototype = {\n\tconstructor: Tween,\n\tinit: function( elem, options, prop, end, easing, unit ) {\n\t\tthis.elem = elem;\n\t\tthis.prop = prop;\n\t\tthis.easing = easing || jQuery.easing._default;\n\t\tthis.options = options;\n\t\tthis.start = this.now = this.cur();\n\t\tthis.end = end;\n\t\tthis.unit = unit || ( jQuery.cssNumber[ prop ] ? \"\" : \"px\" );\n\t},\n\tcur: function() {\n\t\tvar hooks = Tween.propHooks[ this.prop ];\n\n\t\treturn hooks && hooks.get ?\n\t\t\thooks.get( this ) :\n\t\t\tTween.propHooks._default.get( this );\n\t},\n\trun: function( percent ) {\n\t\tvar eased,\n\t\t\thooks = Tween.propHooks[ this.prop ];\n\n\t\tif ( this.options.duration ) {\n\t\t\tthis.pos = eased = jQuery.easing[ this.easing ](\n\t\t\t\tpercent, this.options.duration * percent, 0, 1, this.options.duration\n\t\t\t);\n\t\t} else {\n\t\t\tthis.pos = eased = percent;\n\t\t}\n\t\tthis.now = ( this.end - this.start ) * eased + this.start;\n\n\t\tif ( this.options.step ) {\n\t\t\tthis.options.step.call( this.elem, this.now, this );\n\t\t}\n\n\t\tif ( hooks && hooks.set ) {\n\t\t\thooks.set( this );\n\t\t} else {\n\t\t\tTween.propHooks._default.set( this );\n\t\t}\n\t\treturn this;\n\t}\n};\n\nTween.prototype.init.prototype = Tween.prototype;\n\nTween.propHooks = {\n\t_default: {\n\t\tget: function( tween ) {\n\t\t\tvar result;\n\n\t\t\t// Use a property on the element directly when it is not a DOM element,\n\t\t\t// or when there is no matching style property that exists.\n\t\t\tif ( tween.elem.nodeType !== 1 ||\n\t\t\t\ttween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) {\n\t\t\t\treturn tween.elem[ tween.prop ];\n\t\t\t}\n\n\t\t\t// Passing an empty string as a 3rd parameter to .css will automatically\n\t\t\t// attempt a parseFloat and fallback to a string if the parse fails.\n\t\t\t// Simple values such as \"10px\" are parsed to Float;\n\t\t\t// complex values such as \"rotate(1rad)\" are returned as-is.\n\t\t\tresult = jQuery.css( tween.elem, tween.prop, \"\" );\n\n\t\t\t// Empty strings, null, undefined and \"auto\" are converted to 0.\n\t\t\treturn !result || result === \"auto\" ? 0 : result;\n\t\t},\n\t\tset: function( tween ) {\n\n\t\t\t// Use step hook for back compat.\n\t\t\t// Use cssHook if its there.\n\t\t\t// Use .style if available and use plain properties where available.\n\t\t\tif ( jQuery.fx.step[ tween.prop ] ) {\n\t\t\t\tjQuery.fx.step[ tween.prop ]( tween );\n\t\t\t} else if ( tween.elem.nodeType === 1 &&\n\t\t\t\t( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null ||\n\t\t\t\t\tjQuery.cssHooks[ tween.prop ] ) ) {\n\t\t\t\tjQuery.style( tween.elem, tween.prop, tween.now + tween.unit );\n\t\t\t} else {\n\t\t\t\ttween.elem[ tween.prop ] = tween.now;\n\t\t\t}\n\t\t}\n\t}\n};\n\n// Support: IE <=9 only\n// Panic based approach to setting things on disconnected nodes\nTween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {\n\tset: function( tween ) {\n\t\tif ( tween.elem.nodeType && tween.elem.parentNode ) {\n\t\t\ttween.elem[ tween.prop ] = tween.now;\n\t\t}\n\t}\n};\n\njQuery.easing = {\n\tlinear: function( p ) {\n\t\treturn p;\n\t},\n\tswing: function( p ) {\n\t\treturn 0.5 - Math.cos( p * Math.PI ) / 2;\n\t},\n\t_default: \"swing\"\n};\n\njQuery.fx = Tween.prototype.init;\n\n// Back compat <1.8 extension point\njQuery.fx.step = {};\n\n\n\n\nvar\n\tfxNow, inProgress,\n\trfxtypes = /^(?:toggle|show|hide)$/,\n\trrun = /queueHooks$/;\n\nfunction schedule() {\n\tif ( inProgress ) {\n\t\tif ( document.hidden === false && window.requestAnimationFrame ) {\n\t\t\twindow.requestAnimationFrame( schedule );\n\t\t} else {\n\t\t\twindow.setTimeout( schedule, jQuery.fx.interval );\n\t\t}\n\n\t\tjQuery.fx.tick();\n\t}\n}\n\n// Animations created synchronously will run synchronously\nfunction createFxNow() {\n\twindow.setTimeout( function() {\n\t\tfxNow = undefined;\n\t} );\n\treturn ( fxNow = Date.now() );\n}\n\n// Generate parameters to create a standard animation\nfunction genFx( type, includeWidth ) {\n\tvar which,\n\t\ti = 0,\n\t\tattrs = { height: type };\n\n\t// If we include width, step value is 1 to do all cssExpand values,\n\t// otherwise step value is 2 to skip over Left and Right\n\tincludeWidth = includeWidth ? 1 : 0;\n\tfor ( ; i < 4; i += 2 - includeWidth ) {\n\t\twhich = cssExpand[ i ];\n\t\tattrs[ \"margin\" + which ] = attrs[ \"padding\" + which ] = type;\n\t}\n\n\tif ( includeWidth ) {\n\t\tattrs.opacity = attrs.width = type;\n\t}\n\n\treturn attrs;\n}\n\nfunction createTween( value, prop, animation ) {\n\tvar tween,\n\t\tcollection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ \"*\" ] ),\n\t\tindex = 0,\n\t\tlength = collection.length;\n\tfor ( ; index < length; index++ ) {\n\t\tif ( ( tween = collection[ index ].call( animation, prop, value ) ) ) {\n\n\t\t\t// We're done with this property\n\t\t\treturn tween;\n\t\t}\n\t}\n}\n\nfunction defaultPrefilter( elem, props, opts ) {\n\tvar prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display,\n\t\tisBox = \"width\" in props || \"height\" in props,\n\t\tanim = this,\n\t\torig = {},\n\t\tstyle = elem.style,\n\t\thidden = elem.nodeType && isHiddenWithinTree( elem ),\n\t\tdataShow = dataPriv.get( elem, \"fxshow\" );\n\n\t// Queue-skipping animations hijack the fx hooks\n\tif ( !opts.queue ) {\n\t\thooks = jQuery._queueHooks( elem, \"fx\" );\n\t\tif ( hooks.unqueued == null ) {\n\t\t\thooks.unqueued = 0;\n\t\t\toldfire = hooks.empty.fire;\n\t\t\thooks.empty.fire = function() {\n\t\t\t\tif ( !hooks.unqueued ) {\n\t\t\t\t\toldfire();\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t\thooks.unqueued++;\n\n\t\tanim.always( function() {\n\n\t\t\t// Ensure the complete handler is called before this completes\n\t\t\tanim.always( function() {\n\t\t\t\thooks.unqueued--;\n\t\t\t\tif ( !jQuery.queue( elem, \"fx\" ).length ) {\n\t\t\t\t\thooks.empty.fire();\n\t\t\t\t}\n\t\t\t} );\n\t\t} );\n\t}\n\n\t// Detect show/hide animations\n\tfor ( prop in props ) {\n\t\tvalue = props[ prop ];\n\t\tif ( rfxtypes.test( value ) ) {\n\t\t\tdelete props[ prop ];\n\t\t\ttoggle = toggle || value === \"toggle\";\n\t\t\tif ( value === ( hidden ? \"hide\" : \"show\" ) ) {\n\n\t\t\t\t// Pretend to be hidden if this is a \"show\" and\n\t\t\t\t// there is still data from a stopped show/hide\n\t\t\t\tif ( value === \"show\" && dataShow && dataShow[ prop ] !== undefined ) {\n\t\t\t\t\thidden = true;\n\n\t\t\t\t// Ignore all other no-op show/hide data\n\t\t\t\t} else {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\torig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );\n\t\t}\n\t}\n\n\t// Bail out if this is a no-op like .hide().hide()\n\tpropTween = !jQuery.isEmptyObject( props );\n\tif ( !propTween && jQuery.isEmptyObject( orig ) ) {\n\t\treturn;\n\t}\n\n\t// Restrict \"overflow\" and \"display\" styles during box animations\n\tif ( isBox && elem.nodeType === 1 ) {\n\n\t\t// Support: IE <=9 - 11, Edge 12 - 15\n\t\t// Record all 3 overflow attributes because IE does not infer the shorthand\n\t\t// from identically-valued overflowX and overflowY and Edge just mirrors\n\t\t// the overflowX value there.\n\t\topts.overflow = [ style.overflow, style.overflowX, style.overflowY ];\n\n\t\t// Identify a display type, preferring old show/hide data over the CSS cascade\n\t\trestoreDisplay = dataShow && dataShow.display;\n\t\tif ( restoreDisplay == null ) {\n\t\t\trestoreDisplay = dataPriv.get( elem, \"display\" );\n\t\t}\n\t\tdisplay = jQuery.css( elem, \"display\" );\n\t\tif ( display === \"none\" ) {\n\t\t\tif ( restoreDisplay ) {\n\t\t\t\tdisplay = restoreDisplay;\n\t\t\t} else {\n\n\t\t\t\t// Get nonempty value(s) by temporarily forcing visibility\n\t\t\t\tshowHide( [ elem ], true );\n\t\t\t\trestoreDisplay = elem.style.display || restoreDisplay;\n\t\t\t\tdisplay = jQuery.css( elem, \"display\" );\n\t\t\t\tshowHide( [ elem ] );\n\t\t\t}\n\t\t}\n\n\t\t// Animate inline elements as inline-block\n\t\tif ( display === \"inline\" || display === \"inline-block\" && restoreDisplay != null ) {\n\t\t\tif ( jQuery.css( elem, \"float\" ) === \"none\" ) {\n\n\t\t\t\t// Restore the original display value at the end of pure show/hide animations\n\t\t\t\tif ( !propTween ) {\n\t\t\t\t\tanim.done( function() {\n\t\t\t\t\t\tstyle.display = restoreDisplay;\n\t\t\t\t\t} );\n\t\t\t\t\tif ( restoreDisplay == null ) {\n\t\t\t\t\t\tdisplay = style.display;\n\t\t\t\t\t\trestoreDisplay = display === \"none\" ? \"\" : display;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstyle.display = \"inline-block\";\n\t\t\t}\n\t\t}\n\t}\n\n\tif ( opts.overflow ) {\n\t\tstyle.overflow = \"hidden\";\n\t\tanim.always( function() {\n\t\t\tstyle.overflow = opts.overflow[ 0 ];\n\t\t\tstyle.overflowX = opts.overflow[ 1 ];\n\t\t\tstyle.overflowY = opts.overflow[ 2 ];\n\t\t} );\n\t}\n\n\t// Implement show/hide animations\n\tpropTween = false;\n\tfor ( prop in orig ) {\n\n\t\t// General show/hide setup for this element animation\n\t\tif ( !propTween ) {\n\t\t\tif ( dataShow ) {\n\t\t\t\tif ( \"hidden\" in dataShow ) {\n\t\t\t\t\thidden = dataShow.hidden;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdataShow = dataPriv.access( elem, \"fxshow\", { display: restoreDisplay } );\n\t\t\t}\n\n\t\t\t// Store hidden/visible for toggle so `.stop().toggle()` \"reverses\"\n\t\t\tif ( toggle ) {\n\t\t\t\tdataShow.hidden = !hidden;\n\t\t\t}\n\n\t\t\t// Show elements before animating them\n\t\t\tif ( hidden ) {\n\t\t\t\tshowHide( [ elem ], true );\n\t\t\t}\n\n\t\t\t/* eslint-disable no-loop-func */\n\n\t\t\tanim.done( function() {\n\n\t\t\t/* eslint-enable no-loop-func */\n\n\t\t\t\t// The final step of a \"hide\" animation is actually hiding the element\n\t\t\t\tif ( !hidden ) {\n\t\t\t\t\tshowHide( [ elem ] );\n\t\t\t\t}\n\t\t\t\tdataPriv.remove( elem, \"fxshow\" );\n\t\t\t\tfor ( prop in orig ) {\n\t\t\t\t\tjQuery.style( elem, prop, orig[ prop ] );\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\n\t\t// Per-property setup\n\t\tpropTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );\n\t\tif ( !( prop in dataShow ) ) {\n\t\t\tdataShow[ prop ] = propTween.start;\n\t\t\tif ( hidden ) {\n\t\t\t\tpropTween.end = propTween.start;\n\t\t\t\tpropTween.start = 0;\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction propFilter( props, specialEasing ) {\n\tvar index, name, easing, value, hooks;\n\n\t// camelCase, specialEasing and expand cssHook pass\n\tfor ( index in props ) {\n\t\tname = camelCase( index );\n\t\teasing = specialEasing[ name ];\n\t\tvalue = props[ index ];\n\t\tif ( Array.isArray( value ) ) {\n\t\t\teasing = value[ 1 ];\n\t\t\tvalue = props[ index ] = value[ 0 ];\n\t\t}\n\n\t\tif ( index !== name ) {\n\t\t\tprops[ name ] = value;\n\t\t\tdelete props[ index ];\n\t\t}\n\n\t\thooks = jQuery.cssHooks[ name ];\n\t\tif ( hooks && \"expand\" in hooks ) {\n\t\t\tvalue = hooks.expand( value );\n\t\t\tdelete props[ name ];\n\n\t\t\t// Not quite $.extend, this won't overwrite existing keys.\n\t\t\t// Reusing 'index' because we have the correct \"name\"\n\t\t\tfor ( index in value ) {\n\t\t\t\tif ( !( index in props ) ) {\n\t\t\t\t\tprops[ index ] = value[ index ];\n\t\t\t\t\tspecialEasing[ index ] = easing;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tspecialEasing[ name ] = easing;\n\t\t}\n\t}\n}\n\nfunction Animation( elem, properties, options ) {\n\tvar result,\n\t\tstopped,\n\t\tindex = 0,\n\t\tlength = Animation.prefilters.length,\n\t\tdeferred = jQuery.Deferred().always( function() {\n\n\t\t\t// Don't match elem in the :animated selector\n\t\t\tdelete tick.elem;\n\t\t} ),\n\t\ttick = function() {\n\t\t\tif ( stopped ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tvar currentTime = fxNow || createFxNow(),\n\t\t\t\tremaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),\n\n\t\t\t\t// Support: Android 2.3 only\n\t\t\t\t// Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497)\n\t\t\t\ttemp = remaining / animation.duration || 0,\n\t\t\t\tpercent = 1 - temp,\n\t\t\t\tindex = 0,\n\t\t\t\tlength = animation.tweens.length;\n\n\t\t\tfor ( ; index < length; index++ ) {\n\t\t\t\tanimation.tweens[ index ].run( percent );\n\t\t\t}\n\n\t\t\tdeferred.notifyWith( elem, [ animation, percent, remaining ] );\n\n\t\t\t// If there's more to do, yield\n\t\t\tif ( percent < 1 && length ) {\n\t\t\t\treturn remaining;\n\t\t\t}\n\n\t\t\t// If this was an empty animation, synthesize a final progress notification\n\t\t\tif ( !length ) {\n\t\t\t\tdeferred.notifyWith( elem, [ animation, 1, 0 ] );\n\t\t\t}\n\n\t\t\t// Resolve the animation and report its conclusion\n\t\t\tdeferred.resolveWith( elem, [ animation ] );\n\t\t\treturn false;\n\t\t},\n\t\tanimation = deferred.promise( {\n\t\t\telem: elem,\n\t\t\tprops: jQuery.extend( {}, properties ),\n\t\t\topts: jQuery.extend( true, {\n\t\t\t\tspecialEasing: {},\n\t\t\t\teasing: jQuery.easing._default\n\t\t\t}, options ),\n\t\t\toriginalProperties: properties,\n\t\t\toriginalOptions: options,\n\t\t\tstartTime: fxNow || createFxNow(),\n\t\t\tduration: options.duration,\n\t\t\ttweens: [],\n\t\t\tcreateTween: function( prop, end ) {\n\t\t\t\tvar tween = jQuery.Tween( elem, animation.opts, prop, end,\n\t\t\t\t\t\tanimation.opts.specialEasing[ prop ] || animation.opts.easing );\n\t\t\t\tanimation.tweens.push( tween );\n\t\t\t\treturn tween;\n\t\t\t},\n\t\t\tstop: function( gotoEnd ) {\n\t\t\t\tvar index = 0,\n\n\t\t\t\t\t// If we are going to the end, we want to run all the tweens\n\t\t\t\t\t// otherwise we skip this part\n\t\t\t\t\tlength = gotoEnd ? animation.tweens.length : 0;\n\t\t\t\tif ( stopped ) {\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t\tstopped = true;\n\t\t\t\tfor ( ; index < length; index++ ) {\n\t\t\t\t\tanimation.tweens[ index ].run( 1 );\n\t\t\t\t}\n\n\t\t\t\t// Resolve when we played the last frame; otherwise, reject\n\t\t\t\tif ( gotoEnd ) {\n\t\t\t\t\tdeferred.notifyWith( elem, [ animation, 1, 0 ] );\n\t\t\t\t\tdeferred.resolveWith( elem, [ animation, gotoEnd ] );\n\t\t\t\t} else {\n\t\t\t\t\tdeferred.rejectWith( elem, [ animation, gotoEnd ] );\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t}\n\t\t} ),\n\t\tprops = animation.props;\n\n\tpropFilter( props, animation.opts.specialEasing );\n\n\tfor ( ; index < length; index++ ) {\n\t\tresult = Animation.prefilters[ index ].call( animation, elem, props, animation.opts );\n\t\tif ( result ) {\n\t\t\tif ( isFunction( result.stop ) ) {\n\t\t\t\tjQuery._queueHooks( animation.elem, animation.opts.queue ).stop =\n\t\t\t\t\tresult.stop.bind( result );\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\t}\n\n\tjQuery.map( props, createTween, animation );\n\n\tif ( isFunction( animation.opts.start ) ) {\n\t\tanimation.opts.start.call( elem, animation );\n\t}\n\n\t// Attach callbacks from options\n\tanimation\n\t\t.progress( animation.opts.progress )\n\t\t.done( animation.opts.done, animation.opts.complete )\n\t\t.fail( animation.opts.fail )\n\t\t.always( animation.opts.always );\n\n\tjQuery.fx.timer(\n\t\tjQuery.extend( tick, {\n\t\t\telem: elem,\n\t\t\tanim: animation,\n\t\t\tqueue: animation.opts.queue\n\t\t} )\n\t);\n\n\treturn animation;\n}\n\njQuery.Animation = jQuery.extend( Animation, {\n\n\ttweeners: {\n\t\t\"*\": [ function( prop, value ) {\n\t\t\tvar tween = this.createTween( prop, value );\n\t\t\tadjustCSS( tween.elem, prop, rcssNum.exec( value ), tween );\n\t\t\treturn tween;\n\t\t} ]\n\t},\n\n\ttweener: function( props, callback ) {\n\t\tif ( isFunction( props ) ) {\n\t\t\tcallback = props;\n\t\t\tprops = [ \"*\" ];\n\t\t} else {\n\t\t\tprops = props.match( rnothtmlwhite );\n\t\t}\n\n\t\tvar prop,\n\t\t\tindex = 0,\n\t\t\tlength = props.length;\n\n\t\tfor ( ; index < length; index++ ) {\n\t\t\tprop = props[ index ];\n\t\t\tAnimation.tweeners[ prop ] = Animation.tweeners[ prop ] || [];\n\t\t\tAnimation.tweeners[ prop ].unshift( callback );\n\t\t}\n\t},\n\n\tprefilters: [ defaultPrefilter ],\n\n\tprefilter: function( callback, prepend ) {\n\t\tif ( prepend ) {\n\t\t\tAnimation.prefilters.unshift( callback );\n\t\t} else {\n\t\t\tAnimation.prefilters.push( callback );\n\t\t}\n\t}\n} );\n\njQuery.speed = function( speed, easing, fn ) {\n\tvar opt = speed && typeof speed === \"object\" ? jQuery.extend( {}, speed ) : {\n\t\tcomplete: fn || !fn && easing ||\n\t\t\tisFunction( speed ) && speed,\n\t\tduration: speed,\n\t\teasing: fn && easing || easing && !isFunction( easing ) && easing\n\t};\n\n\t// Go to the end state if fx are off\n\tif ( jQuery.fx.off ) {\n\t\topt.duration = 0;\n\n\t} else {\n\t\tif ( typeof opt.duration !== \"number\" ) {\n\t\t\tif ( opt.duration in jQuery.fx.speeds ) {\n\t\t\t\topt.duration = jQuery.fx.speeds[ opt.duration ];\n\n\t\t\t} else {\n\t\t\t\topt.duration = jQuery.fx.speeds._default;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Normalize opt.queue - true/undefined/null -> \"fx\"\n\tif ( opt.queue == null || opt.queue === true ) {\n\t\topt.queue = \"fx\";\n\t}\n\n\t// Queueing\n\topt.old = opt.complete;\n\n\topt.complete = function() {\n\t\tif ( isFunction( opt.old ) ) {\n\t\t\topt.old.call( this );\n\t\t}\n\n\t\tif ( opt.queue ) {\n\t\t\tjQuery.dequeue( this, opt.queue );\n\t\t}\n\t};\n\n\treturn opt;\n};\n\njQuery.fn.extend( {\n\tfadeTo: function( speed, to, easing, callback ) {\n\n\t\t// Show any hidden elements after setting opacity to 0\n\t\treturn this.filter( isHiddenWithinTree ).css( \"opacity\", 0 ).show()\n\n\t\t\t// Animate to the value specified\n\t\t\t.end().animate( { opacity: to }, speed, easing, callback );\n\t},\n\tanimate: function( prop, speed, easing, callback ) {\n\t\tvar empty = jQuery.isEmptyObject( prop ),\n\t\t\toptall = jQuery.speed( speed, easing, callback ),\n\t\t\tdoAnimation = function() {\n\n\t\t\t\t// Operate on a copy of prop so per-property easing won't be lost\n\t\t\t\tvar anim = Animation( this, jQuery.extend( {}, prop ), optall );\n\n\t\t\t\t// Empty animations, or finishing resolves immediately\n\t\t\t\tif ( empty || dataPriv.get( this, \"finish\" ) ) {\n\t\t\t\t\tanim.stop( true );\n\t\t\t\t}\n\t\t\t};\n\t\t\tdoAnimation.finish = doAnimation;\n\n\t\treturn empty || optall.queue === false ?\n\t\t\tthis.each( doAnimation ) :\n\t\t\tthis.queue( optall.queue, doAnimation );\n\t},\n\tstop: function( type, clearQueue, gotoEnd ) {\n\t\tvar stopQueue = function( hooks ) {\n\t\t\tvar stop = hooks.stop;\n\t\t\tdelete hooks.stop;\n\t\t\tstop( gotoEnd );\n\t\t};\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tgotoEnd = clearQueue;\n\t\t\tclearQueue = type;\n\t\t\ttype = undefined;\n\t\t}\n\t\tif ( clearQueue && type !== false ) {\n\t\t\tthis.queue( type || \"fx\", [] );\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tvar dequeue = true,\n\t\t\t\tindex = type != null && type + \"queueHooks\",\n\t\t\t\ttimers = jQuery.timers,\n\t\t\t\tdata = dataPriv.get( this );\n\n\t\t\tif ( index ) {\n\t\t\t\tif ( data[ index ] && data[ index ].stop ) {\n\t\t\t\t\tstopQueue( data[ index ] );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor ( index in data ) {\n\t\t\t\t\tif ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {\n\t\t\t\t\t\tstopQueue( data[ index ] );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor ( index = timers.length; index--; ) {\n\t\t\t\tif ( timers[ index ].elem === this &&\n\t\t\t\t\t( type == null || timers[ index ].queue === type ) ) {\n\n\t\t\t\t\ttimers[ index ].anim.stop( gotoEnd );\n\t\t\t\t\tdequeue = false;\n\t\t\t\t\ttimers.splice( index, 1 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Start the next in the queue if the last step wasn't forced.\n\t\t\t// Timers currently will call their complete callbacks, which\n\t\t\t// will dequeue but only if they were gotoEnd.\n\t\t\tif ( dequeue || !gotoEnd ) {\n\t\t\t\tjQuery.dequeue( this, type );\n\t\t\t}\n\t\t} );\n\t},\n\tfinish: function( type ) {\n\t\tif ( type !== false ) {\n\t\t\ttype = type || \"fx\";\n\t\t}\n\t\treturn this.each( function() {\n\t\t\tvar index,\n\t\t\t\tdata = dataPriv.get( this ),\n\t\t\t\tqueue = data[ type + \"queue\" ],\n\t\t\t\thooks = data[ type + \"queueHooks\" ],\n\t\t\t\ttimers = jQuery.timers,\n\t\t\t\tlength = queue ? queue.length : 0;\n\n\t\t\t// Enable finishing flag on private data\n\t\t\tdata.finish = true;\n\n\t\t\t// Empty the queue first\n\t\t\tjQuery.queue( this, type, [] );\n\n\t\t\tif ( hooks && hooks.stop ) {\n\t\t\t\thooks.stop.call( this, true );\n\t\t\t}\n\n\t\t\t// Look for any active animations, and finish them\n\t\t\tfor ( index = timers.length; index--; ) {\n\t\t\t\tif ( timers[ index ].elem === this && timers[ index ].queue === type ) {\n\t\t\t\t\ttimers[ index ].anim.stop( true );\n\t\t\t\t\ttimers.splice( index, 1 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Look for any animations in the old queue and finish them\n\t\t\tfor ( index = 0; index < length; index++ ) {\n\t\t\t\tif ( queue[ index ] && queue[ index ].finish ) {\n\t\t\t\t\tqueue[ index ].finish.call( this );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Turn off finishing flag\n\t\t\tdelete data.finish;\n\t\t} );\n\t}\n} );\n\njQuery.each( [ \"toggle\", \"show\", \"hide\" ], function( i, name ) {\n\tvar cssFn = jQuery.fn[ name ];\n\tjQuery.fn[ name ] = function( speed, easing, callback ) {\n\t\treturn speed == null || typeof speed === \"boolean\" ?\n\t\t\tcssFn.apply( this, arguments ) :\n\t\t\tthis.animate( genFx( name, true ), speed, easing, callback );\n\t};\n} );\n\n// Generate shortcuts for custom animations\njQuery.each( {\n\tslideDown: genFx( \"show\" ),\n\tslideUp: genFx( \"hide\" ),\n\tslideToggle: genFx( \"toggle\" ),\n\tfadeIn: { opacity: \"show\" },\n\tfadeOut: { opacity: \"hide\" },\n\tfadeToggle: { opacity: \"toggle\" }\n}, function( name, props ) {\n\tjQuery.fn[ name ] = function( speed, easing, callback ) {\n\t\treturn this.animate( props, speed, easing, callback );\n\t};\n} );\n\njQuery.timers = [];\njQuery.fx.tick = function() {\n\tvar timer,\n\t\ti = 0,\n\t\ttimers = jQuery.timers;\n\n\tfxNow = Date.now();\n\n\tfor ( ; i < timers.length; i++ ) {\n\t\ttimer = timers[ i ];\n\n\t\t// Run the timer and safely remove it when done (allowing for external removal)\n\t\tif ( !timer() && timers[ i ] === timer ) {\n\t\t\ttimers.splice( i--, 1 );\n\t\t}\n\t}\n\n\tif ( !timers.length ) {\n\t\tjQuery.fx.stop();\n\t}\n\tfxNow = undefined;\n};\n\njQuery.fx.timer = function( timer ) {\n\tjQuery.timers.push( timer );\n\tjQuery.fx.start();\n};\n\njQuery.fx.interval = 13;\njQuery.fx.start = function() {\n\tif ( inProgress ) {\n\t\treturn;\n\t}\n\n\tinProgress = true;\n\tschedule();\n};\n\njQuery.fx.stop = function() {\n\tinProgress = null;\n};\n\njQuery.fx.speeds = {\n\tslow: 600,\n\tfast: 200,\n\n\t// Default speed\n\t_default: 400\n};\n\n\n// Based off of the plugin by Clint Helfers, with permission.\n// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/\njQuery.fn.delay = function( time, type ) {\n\ttime = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;\n\ttype = type || \"fx\";\n\n\treturn this.queue( type, function( next, hooks ) {\n\t\tvar timeout = window.setTimeout( next, time );\n\t\thooks.stop = function() {\n\t\t\twindow.clearTimeout( timeout );\n\t\t};\n\t} );\n};\n\n\n( function() {\n\tvar input = document.createElement( \"input\" ),\n\t\tselect = document.createElement( \"select\" ),\n\t\topt = select.appendChild( document.createElement( \"option\" ) );\n\n\tinput.type = \"checkbox\";\n\n\t// Support: Android <=4.3 only\n\t// Default value for a checkbox should be \"on\"\n\tsupport.checkOn = input.value !== \"\";\n\n\t// Support: IE <=11 only\n\t// Must access selectedIndex to make default options select\n\tsupport.optSelected = opt.selected;\n\n\t// Support: IE <=11 only\n\t// An input loses its value after becoming a radio\n\tinput = document.createElement( \"input\" );\n\tinput.value = \"t\";\n\tinput.type = \"radio\";\n\tsupport.radioValue = input.value === \"t\";\n} )();\n\n\nvar boolHook,\n\tattrHandle = jQuery.expr.attrHandle;\n\njQuery.fn.extend( {\n\tattr: function( name, value ) {\n\t\treturn access( this, jQuery.attr, name, value, arguments.length > 1 );\n\t},\n\n\tremoveAttr: function( name ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.removeAttr( this, name );\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tattr: function( elem, name, value ) {\n\t\tvar ret, hooks,\n\t\t\tnType = elem.nodeType;\n\n\t\t// Don't get/set attributes on text, comment and attribute nodes\n\t\tif ( nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Fallback to prop when attributes are not supported\n\t\tif ( typeof elem.getAttribute === \"undefined\" ) {\n\t\t\treturn jQuery.prop( elem, name, value );\n\t\t}\n\n\t\t// Attribute hooks are determined by the lowercase version\n\t\t// Grab necessary hook if one is defined\n\t\tif ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {\n\t\t\thooks = jQuery.attrHooks[ name.toLowerCase() ] ||\n\t\t\t\t( jQuery.expr.match.bool.test( name ) ? boolHook : undefined );\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\t\t\tif ( value === null ) {\n\t\t\t\tjQuery.removeAttr( elem, name );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( hooks && \"set\" in hooks &&\n\t\t\t\t( ret = hooks.set( elem, value, name ) ) !== undefined ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\telem.setAttribute( name, value + \"\" );\n\t\t\treturn value;\n\t\t}\n\n\t\tif ( hooks && \"get\" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {\n\t\t\treturn ret;\n\t\t}\n\n\t\tret = jQuery.find.attr( elem, name );\n\n\t\t// Non-existent attributes return null, we normalize to undefined\n\t\treturn ret == null ? undefined : ret;\n\t},\n\n\tattrHooks: {\n\t\ttype: {\n\t\t\tset: function( elem, value ) {\n\t\t\t\tif ( !support.radioValue && value === \"radio\" &&\n\t\t\t\t\tnodeName( elem, \"input\" ) ) {\n\t\t\t\t\tvar val = elem.value;\n\t\t\t\t\telem.setAttribute( \"type\", value );\n\t\t\t\t\tif ( val ) {\n\t\t\t\t\t\telem.value = val;\n\t\t\t\t\t}\n\t\t\t\t\treturn value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\tremoveAttr: function( elem, value ) {\n\t\tvar name,\n\t\t\ti = 0,\n\n\t\t\t// Attribute names can contain non-HTML whitespace characters\n\t\t\t// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2\n\t\t\tattrNames = value && value.match( rnothtmlwhite );\n\n\t\tif ( attrNames && elem.nodeType === 1 ) {\n\t\t\twhile ( ( name = attrNames[ i++ ] ) ) {\n\t\t\t\telem.removeAttribute( name );\n\t\t\t}\n\t\t}\n\t}\n} );\n\n// Hooks for boolean attributes\nboolHook = {\n\tset: function( elem, value, name ) {\n\t\tif ( value === false ) {\n\n\t\t\t// Remove boolean attributes when set to false\n\t\t\tjQuery.removeAttr( elem, name );\n\t\t} else {\n\t\t\telem.setAttribute( name, name );\n\t\t}\n\t\treturn name;\n\t}\n};\n\njQuery.each( jQuery.expr.match.bool.source.match( /\\w+/g ), function( i, name ) {\n\tvar getter = attrHandle[ name ] || jQuery.find.attr;\n\n\tattrHandle[ name ] = function( elem, name, isXML ) {\n\t\tvar ret, handle,\n\t\t\tlowercaseName = name.toLowerCase();\n\n\t\tif ( !isXML ) {\n\n\t\t\t// Avoid an infinite loop by temporarily removing this function from the getter\n\t\t\thandle = attrHandle[ lowercaseName ];\n\t\t\tattrHandle[ lowercaseName ] = ret;\n\t\t\tret = getter( elem, name, isXML ) != null ?\n\t\t\t\tlowercaseName :\n\t\t\t\tnull;\n\t\t\tattrHandle[ lowercaseName ] = handle;\n\t\t}\n\t\treturn ret;\n\t};\n} );\n\n\n\n\nvar rfocusable = /^(?:input|select|textarea|button)$/i,\n\trclickable = /^(?:a|area)$/i;\n\njQuery.fn.extend( {\n\tprop: function( name, value ) {\n\t\treturn access( this, jQuery.prop, name, value, arguments.length > 1 );\n\t},\n\n\tremoveProp: function( name ) {\n\t\treturn this.each( function() {\n\t\t\tdelete this[ jQuery.propFix[ name ] || name ];\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tprop: function( elem, name, value ) {\n\t\tvar ret, hooks,\n\t\t\tnType = elem.nodeType;\n\n\t\t// Don't get/set properties on text, comment and attribute nodes\n\t\tif ( nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {\n\n\t\t\t// Fix name and attach hooks\n\t\t\tname = jQuery.propFix[ name ] || name;\n\t\t\thooks = jQuery.propHooks[ name ];\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\t\t\tif ( hooks && \"set\" in hooks &&\n\t\t\t\t( ret = hooks.set( elem, value, name ) ) !== undefined ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\treturn ( elem[ name ] = value );\n\t\t}\n\n\t\tif ( hooks && \"get\" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {\n\t\t\treturn ret;\n\t\t}\n\n\t\treturn elem[ name ];\n\t},\n\n\tpropHooks: {\n\t\ttabIndex: {\n\t\t\tget: function( elem ) {\n\n\t\t\t\t// Support: IE <=9 - 11 only\n\t\t\t\t// elem.tabIndex doesn't always return the\n\t\t\t\t// correct value when it hasn't been explicitly set\n\t\t\t\t// https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/\n\t\t\t\t// Use proper attribute retrieval(#12072)\n\t\t\t\tvar tabindex = jQuery.find.attr( elem, \"tabindex\" );\n\n\t\t\t\tif ( tabindex ) {\n\t\t\t\t\treturn parseInt( tabindex, 10 );\n\t\t\t\t}\n\n\t\t\t\tif (\n\t\t\t\t\trfocusable.test( elem.nodeName ) ||\n\t\t\t\t\trclickable.test( elem.nodeName ) &&\n\t\t\t\t\telem.href\n\t\t\t\t) {\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t},\n\n\tpropFix: {\n\t\t\"for\": \"htmlFor\",\n\t\t\"class\": \"className\"\n\t}\n} );\n\n// Support: IE <=11 only\n// Accessing the selectedIndex property\n// forces the browser to respect setting selected\n// on the option\n// The getter ensures a default option is selected\n// when in an optgroup\n// eslint rule \"no-unused-expressions\" is disabled for this code\n// since it considers such accessions noop\nif ( !support.optSelected ) {\n\tjQuery.propHooks.selected = {\n\t\tget: function( elem ) {\n\n\t\t\t/* eslint no-unused-expressions: \"off\" */\n\n\t\t\tvar parent = elem.parentNode;\n\t\t\tif ( parent && parent.parentNode ) {\n\t\t\t\tparent.parentNode.selectedIndex;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t\tset: function( elem ) {\n\n\t\t\t/* eslint no-unused-expressions: \"off\" */\n\n\t\t\tvar parent = elem.parentNode;\n\t\t\tif ( parent ) {\n\t\t\t\tparent.selectedIndex;\n\n\t\t\t\tif ( parent.parentNode ) {\n\t\t\t\t\tparent.parentNode.selectedIndex;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n}\n\njQuery.each( [\n\t\"tabIndex\",\n\t\"readOnly\",\n\t\"maxLength\",\n\t\"cellSpacing\",\n\t\"cellPadding\",\n\t\"rowSpan\",\n\t\"colSpan\",\n\t\"useMap\",\n\t\"frameBorder\",\n\t\"contentEditable\"\n], function() {\n\tjQuery.propFix[ this.toLowerCase() ] = this;\n} );\n\n\n\n\n\t// Strip and collapse whitespace according to HTML spec\n\t// https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace\n\tfunction stripAndCollapse( value ) {\n\t\tvar tokens = value.match( rnothtmlwhite ) || [];\n\t\treturn tokens.join( \" \" );\n\t}\n\n\nfunction getClass( elem ) {\n\treturn elem.getAttribute && elem.getAttribute( \"class\" ) || \"\";\n}\n\nfunction classesToArray( value ) {\n\tif ( Array.isArray( value ) ) {\n\t\treturn value;\n\t}\n\tif ( typeof value === \"string\" ) {\n\t\treturn value.match( rnothtmlwhite ) || [];\n\t}\n\treturn [];\n}\n\njQuery.fn.extend( {\n\taddClass: function( value ) {\n\t\tvar classes, elem, cur, curValue, clazz, j, finalValue,\n\t\t\ti = 0;\n\n\t\tif ( isFunction( value ) ) {\n\t\t\treturn this.each( function( j ) {\n\t\t\t\tjQuery( this ).addClass( value.call( this, j, getClass( this ) ) );\n\t\t\t} );\n\t\t}\n\n\t\tclasses = classesToArray( value );\n\n\t\tif ( classes.length ) {\n\t\t\twhile ( ( elem = this[ i++ ] ) ) {\n\t\t\t\tcurValue = getClass( elem );\n\t\t\t\tcur = elem.nodeType === 1 && ( \" \" + stripAndCollapse( curValue ) + \" \" );\n\n\t\t\t\tif ( cur ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\twhile ( ( clazz = classes[ j++ ] ) ) {\n\t\t\t\t\t\tif ( cur.indexOf( \" \" + clazz + \" \" ) < 0 ) {\n\t\t\t\t\t\t\tcur += clazz + \" \";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Only assign if different to avoid unneeded rendering.\n\t\t\t\t\tfinalValue = stripAndCollapse( cur );\n\t\t\t\t\tif ( curValue !== finalValue ) {\n\t\t\t\t\t\telem.setAttribute( \"class\", finalValue );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tremoveClass: function( value ) {\n\t\tvar classes, elem, cur, curValue, clazz, j, finalValue,\n\t\t\ti = 0;\n\n\t\tif ( isFunction( value ) ) {\n\t\t\treturn this.each( function( j ) {\n\t\t\t\tjQuery( this ).removeClass( value.call( this, j, getClass( this ) ) );\n\t\t\t} );\n\t\t}\n\n\t\tif ( !arguments.length ) {\n\t\t\treturn this.attr( \"class\", \"\" );\n\t\t}\n\n\t\tclasses = classesToArray( value );\n\n\t\tif ( classes.length ) {\n\t\t\twhile ( ( elem = this[ i++ ] ) ) {\n\t\t\t\tcurValue = getClass( elem );\n\n\t\t\t\t// This expression is here for better compressibility (see addClass)\n\t\t\t\tcur = elem.nodeType === 1 && ( \" \" + stripAndCollapse( curValue ) + \" \" );\n\n\t\t\t\tif ( cur ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\twhile ( ( clazz = classes[ j++ ] ) ) {\n\n\t\t\t\t\t\t// Remove *all* instances\n\t\t\t\t\t\twhile ( cur.indexOf( \" \" + clazz + \" \" ) > -1 ) {\n\t\t\t\t\t\t\tcur = cur.replace( \" \" + clazz + \" \", \" \" );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Only assign if different to avoid unneeded rendering.\n\t\t\t\t\tfinalValue = stripAndCollapse( cur );\n\t\t\t\t\tif ( curValue !== finalValue ) {\n\t\t\t\t\t\telem.setAttribute( \"class\", finalValue );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\ttoggleClass: function( value, stateVal ) {\n\t\tvar type = typeof value,\n\t\t\tisValidValue = type === \"string\" || Array.isArray( value );\n\n\t\tif ( typeof stateVal === \"boolean\" && isValidValue ) {\n\t\t\treturn stateVal ? this.addClass( value ) : this.removeClass( value );\n\t\t}\n\n\t\tif ( isFunction( value ) ) {\n\t\t\treturn this.each( function( i ) {\n\t\t\t\tjQuery( this ).toggleClass(\n\t\t\t\t\tvalue.call( this, i, getClass( this ), stateVal ),\n\t\t\t\t\tstateVal\n\t\t\t\t);\n\t\t\t} );\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tvar className, i, self, classNames;\n\n\t\t\tif ( isValidValue ) {\n\n\t\t\t\t// Toggle individual class names\n\t\t\t\ti = 0;\n\t\t\t\tself = jQuery( this );\n\t\t\t\tclassNames = classesToArray( value );\n\n\t\t\t\twhile ( ( className = classNames[ i++ ] ) ) {\n\n\t\t\t\t\t// Check each className given, space separated list\n\t\t\t\t\tif ( self.hasClass( className ) ) {\n\t\t\t\t\t\tself.removeClass( className );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tself.addClass( className );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t// Toggle whole class name\n\t\t\t} else if ( value === undefined || type === \"boolean\" ) {\n\t\t\t\tclassName = getClass( this );\n\t\t\t\tif ( className ) {\n\n\t\t\t\t\t// Store className if set\n\t\t\t\t\tdataPriv.set( this, \"__className__\", className );\n\t\t\t\t}\n\n\t\t\t\t// If the element has a class name or if we're passed `false`,\n\t\t\t\t// then remove the whole classname (if there was one, the above saved it).\n\t\t\t\t// Otherwise bring back whatever was previously saved (if anything),\n\t\t\t\t// falling back to the empty string if nothing was stored.\n\t\t\t\tif ( this.setAttribute ) {\n\t\t\t\t\tthis.setAttribute( \"class\",\n\t\t\t\t\t\tclassName || value === false ?\n\t\t\t\t\t\t\"\" :\n\t\t\t\t\t\tdataPriv.get( this, \"__className__\" ) || \"\"\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t},\n\n\thasClass: function( selector ) {\n\t\tvar className, elem,\n\t\t\ti = 0;\n\n\t\tclassName = \" \" + selector + \" \";\n\t\twhile ( ( elem = this[ i++ ] ) ) {\n\t\t\tif ( elem.nodeType === 1 &&\n\t\t\t\t( \" \" + stripAndCollapse( getClass( elem ) ) + \" \" ).indexOf( className ) > -1 ) {\n\t\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n} );\n\n\n\n\nvar rreturn = /\\r/g;\n\njQuery.fn.extend( {\n\tval: function( value ) {\n\t\tvar hooks, ret, valueIsFunction,\n\t\t\telem = this[ 0 ];\n\n\t\tif ( !arguments.length ) {\n\t\t\tif ( elem ) {\n\t\t\t\thooks = jQuery.valHooks[ elem.type ] ||\n\t\t\t\t\tjQuery.valHooks[ elem.nodeName.toLowerCase() ];\n\n\t\t\t\tif ( hooks &&\n\t\t\t\t\t\"get\" in hooks &&\n\t\t\t\t\t( ret = hooks.get( elem, \"value\" ) ) !== undefined\n\t\t\t\t) {\n\t\t\t\t\treturn ret;\n\t\t\t\t}\n\n\t\t\t\tret = elem.value;\n\n\t\t\t\t// Handle most common string cases\n\t\t\t\tif ( typeof ret === \"string\" ) {\n\t\t\t\t\treturn ret.replace( rreturn, \"\" );\n\t\t\t\t}\n\n\t\t\t\t// Handle cases where value is null/undef or number\n\t\t\t\treturn ret == null ? \"\" : ret;\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tvalueIsFunction = isFunction( value );\n\n\t\treturn this.each( function( i ) {\n\t\t\tvar val;\n\n\t\t\tif ( this.nodeType !== 1 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( valueIsFunction ) {\n\t\t\t\tval = value.call( this, i, jQuery( this ).val() );\n\t\t\t} else {\n\t\t\t\tval = value;\n\t\t\t}\n\n\t\t\t// Treat null/undefined as \"\"; convert numbers to string\n\t\t\tif ( val == null ) {\n\t\t\t\tval = \"\";\n\n\t\t\t} else if ( typeof val === \"number\" ) {\n\t\t\t\tval += \"\";\n\n\t\t\t} else if ( Array.isArray( val ) ) {\n\t\t\t\tval = jQuery.map( val, function( value ) {\n\t\t\t\t\treturn value == null ? \"\" : value + \"\";\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\thooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];\n\n\t\t\t// If set returns undefined, fall back to normal setting\n\t\t\tif ( !hooks || !( \"set\" in hooks ) || hooks.set( this, val, \"value\" ) === undefined ) {\n\t\t\t\tthis.value = val;\n\t\t\t}\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tvalHooks: {\n\t\toption: {\n\t\t\tget: function( elem ) {\n\n\t\t\t\tvar val = jQuery.find.attr( elem, \"value\" );\n\t\t\t\treturn val != null ?\n\t\t\t\t\tval :\n\n\t\t\t\t\t// Support: IE <=10 - 11 only\n\t\t\t\t\t// option.text throws exceptions (#14686, #14858)\n\t\t\t\t\t// Strip and collapse whitespace\n\t\t\t\t\t// https://html.spec.whatwg.org/#strip-and-collapse-whitespace\n\t\t\t\t\tstripAndCollapse( jQuery.text( elem ) );\n\t\t\t}\n\t\t},\n\t\tselect: {\n\t\t\tget: function( elem ) {\n\t\t\t\tvar value, option, i,\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tindex = elem.selectedIndex,\n\t\t\t\t\tone = elem.type === \"select-one\",\n\t\t\t\t\tvalues = one ? null : [],\n\t\t\t\t\tmax = one ? index + 1 : options.length;\n\n\t\t\t\tif ( index < 0 ) {\n\t\t\t\t\ti = max;\n\n\t\t\t\t} else {\n\t\t\t\t\ti = one ? index : 0;\n\t\t\t\t}\n\n\t\t\t\t// Loop through all the selected options\n\t\t\t\tfor ( ; i < max; i++ ) {\n\t\t\t\t\toption = options[ i ];\n\n\t\t\t\t\t// Support: IE <=9 only\n\t\t\t\t\t// IE8-9 doesn't update selected after form reset (#2551)\n\t\t\t\t\tif ( ( option.selected || i === index ) &&\n\n\t\t\t\t\t\t\t// Don't return options that are disabled or in a disabled optgroup\n\t\t\t\t\t\t\t!option.disabled &&\n\t\t\t\t\t\t\t( !option.parentNode.disabled ||\n\t\t\t\t\t\t\t\t!nodeName( option.parentNode, \"optgroup\" ) ) ) {\n\n\t\t\t\t\t\t// Get the specific value for the option\n\t\t\t\t\t\tvalue = jQuery( option ).val();\n\n\t\t\t\t\t\t// We don't need an array for one selects\n\t\t\t\t\t\tif ( one ) {\n\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Multi-Selects return an array\n\t\t\t\t\t\tvalues.push( value );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn values;\n\t\t\t},\n\n\t\t\tset: function( elem, value ) {\n\t\t\t\tvar optionSet, option,\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tvalues = jQuery.makeArray( value ),\n\t\t\t\t\ti = options.length;\n\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\toption = options[ i ];\n\n\t\t\t\t\t/* eslint-disable no-cond-assign */\n\n\t\t\t\t\tif ( option.selected =\n\t\t\t\t\t\tjQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1\n\t\t\t\t\t) {\n\t\t\t\t\t\toptionSet = true;\n\t\t\t\t\t}\n\n\t\t\t\t\t/* eslint-enable no-cond-assign */\n\t\t\t\t}\n\n\t\t\t\t// Force browsers to behave consistently when non-matching value is set\n\t\t\t\tif ( !optionSet ) {\n\t\t\t\t\telem.selectedIndex = -1;\n\t\t\t\t}\n\t\t\t\treturn values;\n\t\t\t}\n\t\t}\n\t}\n} );\n\n// Radios and checkboxes getter/setter\njQuery.each( [ \"radio\", \"checkbox\" ], function() {\n\tjQuery.valHooks[ this ] = {\n\t\tset: function( elem, value ) {\n\t\t\tif ( Array.isArray( value ) ) {\n\t\t\t\treturn ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 );\n\t\t\t}\n\t\t}\n\t};\n\tif ( !support.checkOn ) {\n\t\tjQuery.valHooks[ this ].get = function( elem ) {\n\t\t\treturn elem.getAttribute( \"value\" ) === null ? \"on\" : elem.value;\n\t\t};\n\t}\n} );\n\n\n\n\n// Return jQuery for attributes-only inclusion\n\n\nsupport.focusin = \"onfocusin\" in window;\n\n\nvar rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,\n\tstopPropagationCallback = function( e ) {\n\t\te.stopPropagation();\n\t};\n\njQuery.extend( jQuery.event, {\n\n\ttrigger: function( event, data, elem, onlyHandlers ) {\n\n\t\tvar i, cur, tmp, bubbleType, ontype, handle, special, lastElement,\n\t\t\teventPath = [ elem || document ],\n\t\t\ttype = hasOwn.call( event, \"type\" ) ? event.type : event,\n\t\t\tnamespaces = hasOwn.call( event, \"namespace\" ) ? event.namespace.split( \".\" ) : [];\n\n\t\tcur = lastElement = tmp = elem = elem || document;\n\n\t\t// Don't do events on text and comment nodes\n\t\tif ( elem.nodeType === 3 || elem.nodeType === 8 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// focus/blur morphs to focusin/out; ensure we're not firing them right now\n\t\tif ( rfocusMorph.test( type + jQuery.event.triggered ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( type.indexOf( \".\" ) > -1 ) {\n\n\t\t\t// Namespaced trigger; create a regexp to match event type in handle()\n\t\t\tnamespaces = type.split( \".\" );\n\t\t\ttype = namespaces.shift();\n\t\t\tnamespaces.sort();\n\t\t}\n\t\tontype = type.indexOf( \":\" ) < 0 && \"on\" + type;\n\n\t\t// Caller can pass in a jQuery.Event object, Object, or just an event type string\n\t\tevent = event[ jQuery.expando ] ?\n\t\t\tevent :\n\t\t\tnew jQuery.Event( type, typeof event === \"object\" && event );\n\n\t\t// Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)\n\t\tevent.isTrigger = onlyHandlers ? 2 : 3;\n\t\tevent.namespace = namespaces.join( \".\" );\n\t\tevent.rnamespace = event.namespace ?\n\t\t\tnew RegExp( \"(^|\\\\.)\" + namespaces.join( \"\\\\.(?:.*\\\\.|)\" ) + \"(\\\\.|$)\" ) :\n\t\t\tnull;\n\n\t\t// Clean up the event in case it is being reused\n\t\tevent.result = undefined;\n\t\tif ( !event.target ) {\n\t\t\tevent.target = elem;\n\t\t}\n\n\t\t// Clone any incoming data and prepend the event, creating the handler arg list\n\t\tdata = data == null ?\n\t\t\t[ event ] :\n\t\t\tjQuery.makeArray( data, [ event ] );\n\n\t\t// Allow special events to draw outside the lines\n\t\tspecial = jQuery.event.special[ type ] || {};\n\t\tif ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine event propagation path in advance, per W3C events spec (#9951)\n\t\t// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)\n\t\tif ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) {\n\n\t\t\tbubbleType = special.delegateType || type;\n\t\t\tif ( !rfocusMorph.test( bubbleType + type ) ) {\n\t\t\t\tcur = cur.parentNode;\n\t\t\t}\n\t\t\tfor ( ; cur; cur = cur.parentNode ) {\n\t\t\t\teventPath.push( cur );\n\t\t\t\ttmp = cur;\n\t\t\t}\n\n\t\t\t// Only add window if we got to document (e.g., not plain obj or detached DOM)\n\t\t\tif ( tmp === ( elem.ownerDocument || document ) ) {\n\t\t\t\teventPath.push( tmp.defaultView || tmp.parentWindow || window );\n\t\t\t}\n\t\t}\n\n\t\t// Fire handlers on the event path\n\t\ti = 0;\n\t\twhile ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) {\n\t\t\tlastElement = cur;\n\t\t\tevent.type = i > 1 ?\n\t\t\t\tbubbleType :\n\t\t\t\tspecial.bindType || type;\n\n\t\t\t// jQuery handler\n\t\t\thandle = ( dataPriv.get( cur, \"events\" ) || {} )[ event.type ] &&\n\t\t\t\tdataPriv.get( cur, \"handle\" );\n\t\t\tif ( handle ) {\n\t\t\t\thandle.apply( cur, data );\n\t\t\t}\n\n\t\t\t// Native handler\n\t\t\thandle = ontype && cur[ ontype ];\n\t\t\tif ( handle && handle.apply && acceptData( cur ) ) {\n\t\t\t\tevent.result = handle.apply( cur, data );\n\t\t\t\tif ( event.result === false ) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tevent.type = type;\n\n\t\t// If nobody prevented the default action, do it now\n\t\tif ( !onlyHandlers && !event.isDefaultPrevented() ) {\n\n\t\t\tif ( ( !special._default ||\n\t\t\t\tspecial._default.apply( eventPath.pop(), data ) === false ) &&\n\t\t\t\tacceptData( elem ) ) {\n\n\t\t\t\t// Call a native DOM method on the target with the same name as the event.\n\t\t\t\t// Don't do default actions on window, that's where global variables be (#6170)\n\t\t\t\tif ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) {\n\n\t\t\t\t\t// Don't re-trigger an onFOO event when we call its FOO() method\n\t\t\t\t\ttmp = elem[ ontype ];\n\n\t\t\t\t\tif ( tmp ) {\n\t\t\t\t\t\telem[ ontype ] = null;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Prevent re-triggering of the same event, since we already bubbled it above\n\t\t\t\t\tjQuery.event.triggered = type;\n\n\t\t\t\t\tif ( event.isPropagationStopped() ) {\n\t\t\t\t\t\tlastElement.addEventListener( type, stopPropagationCallback );\n\t\t\t\t\t}\n\n\t\t\t\t\telem[ type ]();\n\n\t\t\t\t\tif ( event.isPropagationStopped() ) {\n\t\t\t\t\t\tlastElement.removeEventListener( type, stopPropagationCallback );\n\t\t\t\t\t}\n\n\t\t\t\t\tjQuery.event.triggered = undefined;\n\n\t\t\t\t\tif ( tmp ) {\n\t\t\t\t\t\telem[ ontype ] = tmp;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\t// Piggyback on a donor event to simulate a different one\n\t// Used only for `focus(in | out)` events\n\tsimulate: function( type, elem, event ) {\n\t\tvar e = jQuery.extend(\n\t\t\tnew jQuery.Event(),\n\t\t\tevent,\n\t\t\t{\n\t\t\t\ttype: type,\n\t\t\t\tisSimulated: true\n\t\t\t}\n\t\t);\n\n\t\tjQuery.event.trigger( e, null, elem );\n\t}\n\n} );\n\njQuery.fn.extend( {\n\n\ttrigger: function( type, data ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.event.trigger( type, data, this );\n\t\t} );\n\t},\n\ttriggerHandler: function( type, data ) {\n\t\tvar elem = this[ 0 ];\n\t\tif ( elem ) {\n\t\t\treturn jQuery.event.trigger( type, data, elem, true );\n\t\t}\n\t}\n} );\n\n\n// Support: Firefox <=44\n// Firefox doesn't have focus(in | out) events\n// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787\n//\n// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1\n// focus(in | out) events fire after focus & blur events,\n// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order\n// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857\nif ( !support.focusin ) {\n\tjQuery.each( { focus: \"focusin\", blur: \"focusout\" }, function( orig, fix ) {\n\n\t\t// Attach a single capturing handler on the document while someone wants focusin/focusout\n\t\tvar handler = function( event ) {\n\t\t\tjQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) );\n\t\t};\n\n\t\tjQuery.event.special[ fix ] = {\n\t\t\tsetup: function() {\n\t\t\t\tvar doc = this.ownerDocument || this,\n\t\t\t\t\tattaches = dataPriv.access( doc, fix );\n\n\t\t\t\tif ( !attaches ) {\n\t\t\t\t\tdoc.addEventListener( orig, handler, true );\n\t\t\t\t}\n\t\t\t\tdataPriv.access( doc, fix, ( attaches || 0 ) + 1 );\n\t\t\t},\n\t\t\tteardown: function() {\n\t\t\t\tvar doc = this.ownerDocument || this,\n\t\t\t\t\tattaches = dataPriv.access( doc, fix ) - 1;\n\n\t\t\t\tif ( !attaches ) {\n\t\t\t\t\tdoc.removeEventListener( orig, handler, true );\n\t\t\t\t\tdataPriv.remove( doc, fix );\n\n\t\t\t\t} else {\n\t\t\t\t\tdataPriv.access( doc, fix, attaches );\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t} );\n}\nvar location = window.location;\n\nvar nonce = Date.now();\n\nvar rquery = ( /\\?/ );\n\n\n\n// Cross-browser xml parsing\njQuery.parseXML = function( data ) {\n\tvar xml;\n\tif ( !data || typeof data !== \"string\" ) {\n\t\treturn null;\n\t}\n\n\t// Support: IE 9 - 11 only\n\t// IE throws on parseFromString with invalid input.\n\ttry {\n\t\txml = ( new window.DOMParser() ).parseFromString( data, \"text/xml\" );\n\t} catch ( e ) {\n\t\txml = undefined;\n\t}\n\n\tif ( !xml || xml.getElementsByTagName( \"parsererror\" ).length ) {\n\t\tjQuery.error( \"Invalid XML: \" + data );\n\t}\n\treturn xml;\n};\n\n\nvar\n\trbracket = /\\[\\]$/,\n\trCRLF = /\\r?\\n/g,\n\trsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,\n\trsubmittable = /^(?:input|select|textarea|keygen)/i;\n\nfunction buildParams( prefix, obj, traditional, add ) {\n\tvar name;\n\n\tif ( Array.isArray( obj ) ) {\n\n\t\t// Serialize array item.\n\t\tjQuery.each( obj, function( i, v ) {\n\t\t\tif ( traditional || rbracket.test( prefix ) ) {\n\n\t\t\t\t// Treat each array item as a scalar.\n\t\t\t\tadd( prefix, v );\n\n\t\t\t} else {\n\n\t\t\t\t// Item is non-scalar (array or object), encode its numeric index.\n\t\t\t\tbuildParams(\n\t\t\t\t\tprefix + \"[\" + ( typeof v === \"object\" && v != null ? i : \"\" ) + \"]\",\n\t\t\t\t\tv,\n\t\t\t\t\ttraditional,\n\t\t\t\t\tadd\n\t\t\t\t);\n\t\t\t}\n\t\t} );\n\n\t} else if ( !traditional && toType( obj ) === \"object\" ) {\n\n\t\t// Serialize object item.\n\t\tfor ( name in obj ) {\n\t\t\tbuildParams( prefix + \"[\" + name + \"]\", obj[ name ], traditional, add );\n\t\t}\n\n\t} else {\n\n\t\t// Serialize scalar item.\n\t\tadd( prefix, obj );\n\t}\n}\n\n// Serialize an array of form elements or a set of\n// key/values into a query string\njQuery.param = function( a, traditional ) {\n\tvar prefix,\n\t\ts = [],\n\t\tadd = function( key, valueOrFunction ) {\n\n\t\t\t// If value is a function, invoke it and use its return value\n\t\t\tvar value = isFunction( valueOrFunction ) ?\n\t\t\t\tvalueOrFunction() :\n\t\t\t\tvalueOrFunction;\n\n\t\t\ts[ s.length ] = encodeURIComponent( key ) + \"=\" +\n\t\t\t\tencodeURIComponent( value == null ? \"\" : value );\n\t\t};\n\n\t// If an array was passed in, assume that it is an array of form elements.\n\tif ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {\n\n\t\t// Serialize the form elements\n\t\tjQuery.each( a, function() {\n\t\t\tadd( this.name, this.value );\n\t\t} );\n\n\t} else {\n\n\t\t// If traditional, encode the \"old\" way (the way 1.3.2 or older\n\t\t// did it), otherwise encode params recursively.\n\t\tfor ( prefix in a ) {\n\t\t\tbuildParams( prefix, a[ prefix ], traditional, add );\n\t\t}\n\t}\n\n\t// Return the resulting serialization\n\treturn s.join( \"&\" );\n};\n\njQuery.fn.extend( {\n\tserialize: function() {\n\t\treturn jQuery.param( this.serializeArray() );\n\t},\n\tserializeArray: function() {\n\t\treturn this.map( function() {\n\n\t\t\t// Can add propHook for \"elements\" to filter or add form elements\n\t\t\tvar elements = jQuery.prop( this, \"elements\" );\n\t\t\treturn elements ? jQuery.makeArray( elements ) : this;\n\t\t} )\n\t\t.filter( function() {\n\t\t\tvar type = this.type;\n\n\t\t\t// Use .is( \":disabled\" ) so that fieldset[disabled] works\n\t\t\treturn this.name && !jQuery( this ).is( \":disabled\" ) &&\n\t\t\t\trsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&\n\t\t\t\t( this.checked || !rcheckableType.test( type ) );\n\t\t} )\n\t\t.map( function( i, elem ) {\n\t\t\tvar val = jQuery( this ).val();\n\n\t\t\tif ( val == null ) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tif ( Array.isArray( val ) ) {\n\t\t\t\treturn jQuery.map( val, function( val ) {\n\t\t\t\t\treturn { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\treturn { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t} ).get();\n\t}\n} );\n\n\nvar\n\tr20 = /%20/g,\n\trhash = /#.*$/,\n\trantiCache = /([?&])_=[^&]*/,\n\trheaders = /^(.*?):[ \\t]*([^\\r\\n]*)$/mg,\n\n\t// #7653, #8125, #8152: local protocol detection\n\trlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,\n\trnoContent = /^(?:GET|HEAD)$/,\n\trprotocol = /^\\/\\//,\n\n\t/* Prefilters\n\t * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)\n\t * 2) These are called:\n\t *    - BEFORE asking for a transport\n\t *    - AFTER param serialization (s.data is a string if s.processData is true)\n\t * 3) key is the dataType\n\t * 4) the catchall symbol \"*\" can be used\n\t * 5) execution will start with transport dataType and THEN continue down to \"*\" if needed\n\t */\n\tprefilters = {},\n\n\t/* Transports bindings\n\t * 1) key is the dataType\n\t * 2) the catchall symbol \"*\" can be used\n\t * 3) selection will start with transport dataType and THEN go to \"*\" if needed\n\t */\n\ttransports = {},\n\n\t// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression\n\tallTypes = \"*/\".concat( \"*\" ),\n\n\t// Anchor tag for parsing the document origin\n\toriginAnchor = document.createElement( \"a\" );\n\toriginAnchor.href = location.href;\n\n// Base \"constructor\" for jQuery.ajaxPrefilter and jQuery.ajaxTransport\nfunction addToPrefiltersOrTransports( structure ) {\n\n\t// dataTypeExpression is optional and defaults to \"*\"\n\treturn function( dataTypeExpression, func ) {\n\n\t\tif ( typeof dataTypeExpression !== \"string\" ) {\n\t\t\tfunc = dataTypeExpression;\n\t\t\tdataTypeExpression = \"*\";\n\t\t}\n\n\t\tvar dataType,\n\t\t\ti = 0,\n\t\t\tdataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || [];\n\n\t\tif ( isFunction( func ) ) {\n\n\t\t\t// For each dataType in the dataTypeExpression\n\t\t\twhile ( ( dataType = dataTypes[ i++ ] ) ) {\n\n\t\t\t\t// Prepend if requested\n\t\t\t\tif ( dataType[ 0 ] === \"+\" ) {\n\t\t\t\t\tdataType = dataType.slice( 1 ) || \"*\";\n\t\t\t\t\t( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func );\n\n\t\t\t\t// Otherwise append\n\t\t\t\t} else {\n\t\t\t\t\t( structure[ dataType ] = structure[ dataType ] || [] ).push( func );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n}\n\n// Base inspection function for prefilters and transports\nfunction inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {\n\n\tvar inspected = {},\n\t\tseekingTransport = ( structure === transports );\n\n\tfunction inspect( dataType ) {\n\t\tvar selected;\n\t\tinspected[ dataType ] = true;\n\t\tjQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {\n\t\t\tvar dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );\n\t\t\tif ( typeof dataTypeOrTransport === \"string\" &&\n\t\t\t\t!seekingTransport && !inspected[ dataTypeOrTransport ] ) {\n\n\t\t\t\toptions.dataTypes.unshift( dataTypeOrTransport );\n\t\t\t\tinspect( dataTypeOrTransport );\n\t\t\t\treturn false;\n\t\t\t} else if ( seekingTransport ) {\n\t\t\t\treturn !( selected = dataTypeOrTransport );\n\t\t\t}\n\t\t} );\n\t\treturn selected;\n\t}\n\n\treturn inspect( options.dataTypes[ 0 ] ) || !inspected[ \"*\" ] && inspect( \"*\" );\n}\n\n// A special extend for ajax options\n// that takes \"flat\" options (not to be deep extended)\n// Fixes #9887\nfunction ajaxExtend( target, src ) {\n\tvar key, deep,\n\t\tflatOptions = jQuery.ajaxSettings.flatOptions || {};\n\n\tfor ( key in src ) {\n\t\tif ( src[ key ] !== undefined ) {\n\t\t\t( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];\n\t\t}\n\t}\n\tif ( deep ) {\n\t\tjQuery.extend( true, target, deep );\n\t}\n\n\treturn target;\n}\n\n/* Handles responses to an ajax request:\n * - finds the right dataType (mediates between content-type and expected dataType)\n * - returns the corresponding response\n */\nfunction ajaxHandleResponses( s, jqXHR, responses ) {\n\n\tvar ct, type, finalDataType, firstDataType,\n\t\tcontents = s.contents,\n\t\tdataTypes = s.dataTypes;\n\n\t// Remove auto dataType and get content-type in the process\n\twhile ( dataTypes[ 0 ] === \"*\" ) {\n\t\tdataTypes.shift();\n\t\tif ( ct === undefined ) {\n\t\t\tct = s.mimeType || jqXHR.getResponseHeader( \"Content-Type\" );\n\t\t}\n\t}\n\n\t// Check if we're dealing with a known content-type\n\tif ( ct ) {\n\t\tfor ( type in contents ) {\n\t\t\tif ( contents[ type ] && contents[ type ].test( ct ) ) {\n\t\t\t\tdataTypes.unshift( type );\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check to see if we have a response for the expected dataType\n\tif ( dataTypes[ 0 ] in responses ) {\n\t\tfinalDataType = dataTypes[ 0 ];\n\t} else {\n\n\t\t// Try convertible dataTypes\n\t\tfor ( type in responses ) {\n\t\t\tif ( !dataTypes[ 0 ] || s.converters[ type + \" \" + dataTypes[ 0 ] ] ) {\n\t\t\t\tfinalDataType = type;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( !firstDataType ) {\n\t\t\t\tfirstDataType = type;\n\t\t\t}\n\t\t}\n\n\t\t// Or just use first one\n\t\tfinalDataType = finalDataType || firstDataType;\n\t}\n\n\t// If we found a dataType\n\t// We add the dataType to the list if needed\n\t// and return the corresponding response\n\tif ( finalDataType ) {\n\t\tif ( finalDataType !== dataTypes[ 0 ] ) {\n\t\t\tdataTypes.unshift( finalDataType );\n\t\t}\n\t\treturn responses[ finalDataType ];\n\t}\n}\n\n/* Chain conversions given the request and the original response\n * Also sets the responseXXX fields on the jqXHR instance\n */\nfunction ajaxConvert( s, response, jqXHR, isSuccess ) {\n\tvar conv2, current, conv, tmp, prev,\n\t\tconverters = {},\n\n\t\t// Work with a copy of dataTypes in case we need to modify it for conversion\n\t\tdataTypes = s.dataTypes.slice();\n\n\t// Create converters map with lowercased keys\n\tif ( dataTypes[ 1 ] ) {\n\t\tfor ( conv in s.converters ) {\n\t\t\tconverters[ conv.toLowerCase() ] = s.converters[ conv ];\n\t\t}\n\t}\n\n\tcurrent = dataTypes.shift();\n\n\t// Convert to each sequential dataType\n\twhile ( current ) {\n\n\t\tif ( s.responseFields[ current ] ) {\n\t\t\tjqXHR[ s.responseFields[ current ] ] = response;\n\t\t}\n\n\t\t// Apply the dataFilter if provided\n\t\tif ( !prev && isSuccess && s.dataFilter ) {\n\t\t\tresponse = s.dataFilter( response, s.dataType );\n\t\t}\n\n\t\tprev = current;\n\t\tcurrent = dataTypes.shift();\n\n\t\tif ( current ) {\n\n\t\t\t// There's only work to do if current dataType is non-auto\n\t\t\tif ( current === \"*\" ) {\n\n\t\t\t\tcurrent = prev;\n\n\t\t\t// Convert response if prev dataType is non-auto and differs from current\n\t\t\t} else if ( prev !== \"*\" && prev !== current ) {\n\n\t\t\t\t// Seek a direct converter\n\t\t\t\tconv = converters[ prev + \" \" + current ] || converters[ \"* \" + current ];\n\n\t\t\t\t// If none found, seek a pair\n\t\t\t\tif ( !conv ) {\n\t\t\t\t\tfor ( conv2 in converters ) {\n\n\t\t\t\t\t\t// If conv2 outputs current\n\t\t\t\t\t\ttmp = conv2.split( \" \" );\n\t\t\t\t\t\tif ( tmp[ 1 ] === current ) {\n\n\t\t\t\t\t\t\t// If prev can be converted to accepted input\n\t\t\t\t\t\t\tconv = converters[ prev + \" \" + tmp[ 0 ] ] ||\n\t\t\t\t\t\t\t\tconverters[ \"* \" + tmp[ 0 ] ];\n\t\t\t\t\t\t\tif ( conv ) {\n\n\t\t\t\t\t\t\t\t// Condense equivalence converters\n\t\t\t\t\t\t\t\tif ( conv === true ) {\n\t\t\t\t\t\t\t\t\tconv = converters[ conv2 ];\n\n\t\t\t\t\t\t\t\t// Otherwise, insert the intermediate dataType\n\t\t\t\t\t\t\t\t} else if ( converters[ conv2 ] !== true ) {\n\t\t\t\t\t\t\t\t\tcurrent = tmp[ 0 ];\n\t\t\t\t\t\t\t\t\tdataTypes.unshift( tmp[ 1 ] );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Apply converter (if not an equivalence)\n\t\t\t\tif ( conv !== true ) {\n\n\t\t\t\t\t// Unless errors are allowed to bubble, catch and return them\n\t\t\t\t\tif ( conv && s.throws ) {\n\t\t\t\t\t\tresponse = conv( response );\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tresponse = conv( response );\n\t\t\t\t\t\t} catch ( e ) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tstate: \"parsererror\",\n\t\t\t\t\t\t\t\terror: conv ? e : \"No conversion from \" + prev + \" to \" + current\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { state: \"success\", data: response };\n}\n\njQuery.extend( {\n\n\t// Counter for holding the number of active queries\n\tactive: 0,\n\n\t// Last-Modified header cache for next request\n\tlastModified: {},\n\tetag: {},\n\n\tajaxSettings: {\n\t\turl: location.href,\n\t\ttype: \"GET\",\n\t\tisLocal: rlocalProtocol.test( location.protocol ),\n\t\tglobal: true,\n\t\tprocessData: true,\n\t\tasync: true,\n\t\tcontentType: \"application/x-www-form-urlencoded; charset=UTF-8\",\n\n\t\t/*\n\t\ttimeout: 0,\n\t\tdata: null,\n\t\tdataType: null,\n\t\tusername: null,\n\t\tpassword: null,\n\t\tcache: null,\n\t\tthrows: false,\n\t\ttraditional: false,\n\t\theaders: {},\n\t\t*/\n\n\t\taccepts: {\n\t\t\t\"*\": allTypes,\n\t\t\ttext: \"text/plain\",\n\t\t\thtml: \"text/html\",\n\t\t\txml: \"application/xml, text/xml\",\n\t\t\tjson: \"application/json, text/javascript\"\n\t\t},\n\n\t\tcontents: {\n\t\t\txml: /\\bxml\\b/,\n\t\t\thtml: /\\bhtml/,\n\t\t\tjson: /\\bjson\\b/\n\t\t},\n\n\t\tresponseFields: {\n\t\t\txml: \"responseXML\",\n\t\t\ttext: \"responseText\",\n\t\t\tjson: \"responseJSON\"\n\t\t},\n\n\t\t// Data converters\n\t\t// Keys separate source (or catchall \"*\") and destination types with a single space\n\t\tconverters: {\n\n\t\t\t// Convert anything to text\n\t\t\t\"* text\": String,\n\n\t\t\t// Text to html (true = no transformation)\n\t\t\t\"text html\": true,\n\n\t\t\t// Evaluate text as a json expression\n\t\t\t\"text json\": JSON.parse,\n\n\t\t\t// Parse text as xml\n\t\t\t\"text xml\": jQuery.parseXML\n\t\t},\n\n\t\t// For options that shouldn't be deep extended:\n\t\t// you can add your own custom options here if\n\t\t// and when you create one that shouldn't be\n\t\t// deep extended (see ajaxExtend)\n\t\tflatOptions: {\n\t\t\turl: true,\n\t\t\tcontext: true\n\t\t}\n\t},\n\n\t// Creates a full fledged settings object into target\n\t// with both ajaxSettings and settings fields.\n\t// If target is omitted, writes into ajaxSettings.\n\tajaxSetup: function( target, settings ) {\n\t\treturn settings ?\n\n\t\t\t// Building a settings object\n\t\t\tajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :\n\n\t\t\t// Extending ajaxSettings\n\t\t\tajaxExtend( jQuery.ajaxSettings, target );\n\t},\n\n\tajaxPrefilter: addToPrefiltersOrTransports( prefilters ),\n\tajaxTransport: addToPrefiltersOrTransports( transports ),\n\n\t// Main method\n\tajax: function( url, options ) {\n\n\t\t// If url is an object, simulate pre-1.5 signature\n\t\tif ( typeof url === \"object\" ) {\n\t\t\toptions = url;\n\t\t\turl = undefined;\n\t\t}\n\n\t\t// Force options to be an object\n\t\toptions = options || {};\n\n\t\tvar transport,\n\n\t\t\t// URL without anti-cache param\n\t\t\tcacheURL,\n\n\t\t\t// Response headers\n\t\t\tresponseHeadersString,\n\t\t\tresponseHeaders,\n\n\t\t\t// timeout handle\n\t\t\ttimeoutTimer,\n\n\t\t\t// Url cleanup var\n\t\t\turlAnchor,\n\n\t\t\t// Request state (becomes false upon send and true upon completion)\n\t\t\tcompleted,\n\n\t\t\t// To know if global events are to be dispatched\n\t\t\tfireGlobals,\n\n\t\t\t// Loop variable\n\t\t\ti,\n\n\t\t\t// uncached part of the url\n\t\t\tuncached,\n\n\t\t\t// Create the final options object\n\t\t\ts = jQuery.ajaxSetup( {}, options ),\n\n\t\t\t// Callbacks context\n\t\t\tcallbackContext = s.context || s,\n\n\t\t\t// Context for global events is callbackContext if it is a DOM node or jQuery collection\n\t\t\tglobalEventContext = s.context &&\n\t\t\t\t( callbackContext.nodeType || callbackContext.jquery ) ?\n\t\t\t\t\tjQuery( callbackContext ) :\n\t\t\t\t\tjQuery.event,\n\n\t\t\t// Deferreds\n\t\t\tdeferred = jQuery.Deferred(),\n\t\t\tcompleteDeferred = jQuery.Callbacks( \"once memory\" ),\n\n\t\t\t// Status-dependent callbacks\n\t\t\tstatusCode = s.statusCode || {},\n\n\t\t\t// Headers (they are sent all at once)\n\t\t\trequestHeaders = {},\n\t\t\trequestHeadersNames = {},\n\n\t\t\t// Default abort message\n\t\t\tstrAbort = \"canceled\",\n\n\t\t\t// Fake xhr\n\t\t\tjqXHR = {\n\t\t\t\treadyState: 0,\n\n\t\t\t\t// Builds headers hashtable if needed\n\t\t\t\tgetResponseHeader: function( key ) {\n\t\t\t\t\tvar match;\n\t\t\t\t\tif ( completed ) {\n\t\t\t\t\t\tif ( !responseHeaders ) {\n\t\t\t\t\t\t\tresponseHeaders = {};\n\t\t\t\t\t\t\twhile ( ( match = rheaders.exec( responseHeadersString ) ) ) {\n\t\t\t\t\t\t\t\tresponseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmatch = responseHeaders[ key.toLowerCase() ];\n\t\t\t\t\t}\n\t\t\t\t\treturn match == null ? null : match;\n\t\t\t\t},\n\n\t\t\t\t// Raw string\n\t\t\t\tgetAllResponseHeaders: function() {\n\t\t\t\t\treturn completed ? responseHeadersString : null;\n\t\t\t\t},\n\n\t\t\t\t// Caches the header\n\t\t\t\tsetRequestHeader: function( name, value ) {\n\t\t\t\t\tif ( completed == null ) {\n\t\t\t\t\t\tname = requestHeadersNames[ name.toLowerCase() ] =\n\t\t\t\t\t\t\trequestHeadersNames[ name.toLowerCase() ] || name;\n\t\t\t\t\t\trequestHeaders[ name ] = value;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Overrides response content-type header\n\t\t\t\toverrideMimeType: function( type ) {\n\t\t\t\t\tif ( completed == null ) {\n\t\t\t\t\t\ts.mimeType = type;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Status-dependent callbacks\n\t\t\t\tstatusCode: function( map ) {\n\t\t\t\t\tvar code;\n\t\t\t\t\tif ( map ) {\n\t\t\t\t\t\tif ( completed ) {\n\n\t\t\t\t\t\t\t// Execute the appropriate callbacks\n\t\t\t\t\t\t\tjqXHR.always( map[ jqXHR.status ] );\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t// Lazy-add the new callbacks in a way that preserves old ones\n\t\t\t\t\t\t\tfor ( code in map ) {\n\t\t\t\t\t\t\t\tstatusCode[ code ] = [ statusCode[ code ], map[ code ] ];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Cancel the request\n\t\t\t\tabort: function( statusText ) {\n\t\t\t\t\tvar finalText = statusText || strAbort;\n\t\t\t\t\tif ( transport ) {\n\t\t\t\t\t\ttransport.abort( finalText );\n\t\t\t\t\t}\n\t\t\t\t\tdone( 0, finalText );\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t};\n\n\t\t// Attach deferreds\n\t\tdeferred.promise( jqXHR );\n\n\t\t// Add protocol if not provided (prefilters might expect it)\n\t\t// Handle falsy url in the settings object (#10093: consistency with old signature)\n\t\t// We also use the url parameter if available\n\t\ts.url = ( ( url || s.url || location.href ) + \"\" )\n\t\t\t.replace( rprotocol, location.protocol + \"//\" );\n\n\t\t// Alias method option to type as per ticket #12004\n\t\ts.type = options.method || options.type || s.method || s.type;\n\n\t\t// Extract dataTypes list\n\t\ts.dataTypes = ( s.dataType || \"*\" ).toLowerCase().match( rnothtmlwhite ) || [ \"\" ];\n\n\t\t// A cross-domain request is in order when the origin doesn't match the current origin.\n\t\tif ( s.crossDomain == null ) {\n\t\t\turlAnchor = document.createElement( \"a\" );\n\n\t\t\t// Support: IE <=8 - 11, Edge 12 - 15\n\t\t\t// IE throws exception on accessing the href property if url is malformed,\n\t\t\t// e.g. http://example.com:80x/\n\t\t\ttry {\n\t\t\t\turlAnchor.href = s.url;\n\n\t\t\t\t// Support: IE <=8 - 11 only\n\t\t\t\t// Anchor's host property isn't correctly set when s.url is relative\n\t\t\t\turlAnchor.href = urlAnchor.href;\n\t\t\t\ts.crossDomain = originAnchor.protocol + \"//\" + originAnchor.host !==\n\t\t\t\t\turlAnchor.protocol + \"//\" + urlAnchor.host;\n\t\t\t} catch ( e ) {\n\n\t\t\t\t// If there is an error parsing the URL, assume it is crossDomain,\n\t\t\t\t// it can be rejected by the transport if it is invalid\n\t\t\t\ts.crossDomain = true;\n\t\t\t}\n\t\t}\n\n\t\t// Convert data if not already a string\n\t\tif ( s.data && s.processData && typeof s.data !== \"string\" ) {\n\t\t\ts.data = jQuery.param( s.data, s.traditional );\n\t\t}\n\n\t\t// Apply prefilters\n\t\tinspectPrefiltersOrTransports( prefilters, s, options, jqXHR );\n\n\t\t// If request was aborted inside a prefilter, stop there\n\t\tif ( completed ) {\n\t\t\treturn jqXHR;\n\t\t}\n\n\t\t// We can fire global events as of now if asked to\n\t\t// Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118)\n\t\tfireGlobals = jQuery.event && s.global;\n\n\t\t// Watch for a new set of requests\n\t\tif ( fireGlobals && jQuery.active++ === 0 ) {\n\t\t\tjQuery.event.trigger( \"ajaxStart\" );\n\t\t}\n\n\t\t// Uppercase the type\n\t\ts.type = s.type.toUpperCase();\n\n\t\t// Determine if request has content\n\t\ts.hasContent = !rnoContent.test( s.type );\n\n\t\t// Save the URL in case we're toying with the If-Modified-Since\n\t\t// and/or If-None-Match header later on\n\t\t// Remove hash to simplify url manipulation\n\t\tcacheURL = s.url.replace( rhash, \"\" );\n\n\t\t// More options handling for requests with no content\n\t\tif ( !s.hasContent ) {\n\n\t\t\t// Remember the hash so we can put it back\n\t\t\tuncached = s.url.slice( cacheURL.length );\n\n\t\t\t// If data is available and should be processed, append data to url\n\t\t\tif ( s.data && ( s.processData || typeof s.data === \"string\" ) ) {\n\t\t\t\tcacheURL += ( rquery.test( cacheURL ) ? \"&\" : \"?\" ) + s.data;\n\n\t\t\t\t// #9682: remove data so that it's not used in an eventual retry\n\t\t\t\tdelete s.data;\n\t\t\t}\n\n\t\t\t// Add or update anti-cache param if needed\n\t\t\tif ( s.cache === false ) {\n\t\t\t\tcacheURL = cacheURL.replace( rantiCache, \"$1\" );\n\t\t\t\tuncached = ( rquery.test( cacheURL ) ? \"&\" : \"?\" ) + \"_=\" + ( nonce++ ) + uncached;\n\t\t\t}\n\n\t\t\t// Put hash and anti-cache on the URL that will be requested (gh-1732)\n\t\t\ts.url = cacheURL + uncached;\n\n\t\t// Change '%20' to '+' if this is encoded form body content (gh-2658)\n\t\t} else if ( s.data && s.processData &&\n\t\t\t( s.contentType || \"\" ).indexOf( \"application/x-www-form-urlencoded\" ) === 0 ) {\n\t\t\ts.data = s.data.replace( r20, \"+\" );\n\t\t}\n\n\t\t// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n\t\tif ( s.ifModified ) {\n\t\t\tif ( jQuery.lastModified[ cacheURL ] ) {\n\t\t\t\tjqXHR.setRequestHeader( \"If-Modified-Since\", jQuery.lastModified[ cacheURL ] );\n\t\t\t}\n\t\t\tif ( jQuery.etag[ cacheURL ] ) {\n\t\t\t\tjqXHR.setRequestHeader( \"If-None-Match\", jQuery.etag[ cacheURL ] );\n\t\t\t}\n\t\t}\n\n\t\t// Set the correct header, if data is being sent\n\t\tif ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {\n\t\t\tjqXHR.setRequestHeader( \"Content-Type\", s.contentType );\n\t\t}\n\n\t\t// Set the Accepts header for the server, depending on the dataType\n\t\tjqXHR.setRequestHeader(\n\t\t\t\"Accept\",\n\t\t\ts.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ?\n\t\t\t\ts.accepts[ s.dataTypes[ 0 ] ] +\n\t\t\t\t\t( s.dataTypes[ 0 ] !== \"*\" ? \", \" + allTypes + \"; q=0.01\" : \"\" ) :\n\t\t\t\ts.accepts[ \"*\" ]\n\t\t);\n\n\t\t// Check for headers option\n\t\tfor ( i in s.headers ) {\n\t\t\tjqXHR.setRequestHeader( i, s.headers[ i ] );\n\t\t}\n\n\t\t// Allow custom headers/mimetypes and early abort\n\t\tif ( s.beforeSend &&\n\t\t\t( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) {\n\n\t\t\t// Abort if not done already and return\n\t\t\treturn jqXHR.abort();\n\t\t}\n\n\t\t// Aborting is no longer a cancellation\n\t\tstrAbort = \"abort\";\n\n\t\t// Install callbacks on deferreds\n\t\tcompleteDeferred.add( s.complete );\n\t\tjqXHR.done( s.success );\n\t\tjqXHR.fail( s.error );\n\n\t\t// Get transport\n\t\ttransport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );\n\n\t\t// If no transport, we auto-abort\n\t\tif ( !transport ) {\n\t\t\tdone( -1, \"No Transport\" );\n\t\t} else {\n\t\t\tjqXHR.readyState = 1;\n\n\t\t\t// Send global event\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajaxSend\", [ jqXHR, s ] );\n\t\t\t}\n\n\t\t\t// If request was aborted inside ajaxSend, stop there\n\t\t\tif ( completed ) {\n\t\t\t\treturn jqXHR;\n\t\t\t}\n\n\t\t\t// Timeout\n\t\t\tif ( s.async && s.timeout > 0 ) {\n\t\t\t\ttimeoutTimer = window.setTimeout( function() {\n\t\t\t\t\tjqXHR.abort( \"timeout\" );\n\t\t\t\t}, s.timeout );\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tcompleted = false;\n\t\t\t\ttransport.send( requestHeaders, done );\n\t\t\t} catch ( e ) {\n\n\t\t\t\t// Rethrow post-completion exceptions\n\t\t\t\tif ( completed ) {\n\t\t\t\t\tthrow e;\n\t\t\t\t}\n\n\t\t\t\t// Propagate others as results\n\t\t\t\tdone( -1, e );\n\t\t\t}\n\t\t}\n\n\t\t// Callback for when everything is done\n\t\tfunction done( status, nativeStatusText, responses, headers ) {\n\t\t\tvar isSuccess, success, error, response, modified,\n\t\t\t\tstatusText = nativeStatusText;\n\n\t\t\t// Ignore repeat invocations\n\t\t\tif ( completed ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcompleted = true;\n\n\t\t\t// Clear timeout if it exists\n\t\t\tif ( timeoutTimer ) {\n\t\t\t\twindow.clearTimeout( timeoutTimer );\n\t\t\t}\n\n\t\t\t// Dereference transport for early garbage collection\n\t\t\t// (no matter how long the jqXHR object will be used)\n\t\t\ttransport = undefined;\n\n\t\t\t// Cache response headers\n\t\t\tresponseHeadersString = headers || \"\";\n\n\t\t\t// Set readyState\n\t\t\tjqXHR.readyState = status > 0 ? 4 : 0;\n\n\t\t\t// Determine if successful\n\t\t\tisSuccess = status >= 200 && status < 300 || status === 304;\n\n\t\t\t// Get response data\n\t\t\tif ( responses ) {\n\t\t\t\tresponse = ajaxHandleResponses( s, jqXHR, responses );\n\t\t\t}\n\n\t\t\t// Convert no matter what (that way responseXXX fields are always set)\n\t\t\tresponse = ajaxConvert( s, response, jqXHR, isSuccess );\n\n\t\t\t// If successful, handle type chaining\n\t\t\tif ( isSuccess ) {\n\n\t\t\t\t// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n\t\t\t\tif ( s.ifModified ) {\n\t\t\t\t\tmodified = jqXHR.getResponseHeader( \"Last-Modified\" );\n\t\t\t\t\tif ( modified ) {\n\t\t\t\t\t\tjQuery.lastModified[ cacheURL ] = modified;\n\t\t\t\t\t}\n\t\t\t\t\tmodified = jqXHR.getResponseHeader( \"etag\" );\n\t\t\t\t\tif ( modified ) {\n\t\t\t\t\t\tjQuery.etag[ cacheURL ] = modified;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// if no content\n\t\t\t\tif ( status === 204 || s.type === \"HEAD\" ) {\n\t\t\t\t\tstatusText = \"nocontent\";\n\n\t\t\t\t// if not modified\n\t\t\t\t} else if ( status === 304 ) {\n\t\t\t\t\tstatusText = \"notmodified\";\n\n\t\t\t\t// If we have data, let's convert it\n\t\t\t\t} else {\n\t\t\t\t\tstatusText = response.state;\n\t\t\t\t\tsuccess = response.data;\n\t\t\t\t\terror = response.error;\n\t\t\t\t\tisSuccess = !error;\n\t\t\t\t}\n\t\t\t} else {\n\n\t\t\t\t// Extract error from statusText and normalize for non-aborts\n\t\t\t\terror = statusText;\n\t\t\t\tif ( status || !statusText ) {\n\t\t\t\t\tstatusText = \"error\";\n\t\t\t\t\tif ( status < 0 ) {\n\t\t\t\t\t\tstatus = 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set data for the fake xhr object\n\t\t\tjqXHR.status = status;\n\t\t\tjqXHR.statusText = ( nativeStatusText || statusText ) + \"\";\n\n\t\t\t// Success/Error\n\t\t\tif ( isSuccess ) {\n\t\t\t\tdeferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );\n\t\t\t} else {\n\t\t\t\tdeferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );\n\t\t\t}\n\n\t\t\t// Status-dependent callbacks\n\t\t\tjqXHR.statusCode( statusCode );\n\t\t\tstatusCode = undefined;\n\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( isSuccess ? \"ajaxSuccess\" : \"ajaxError\",\n\t\t\t\t\t[ jqXHR, s, isSuccess ? success : error ] );\n\t\t\t}\n\n\t\t\t// Complete\n\t\t\tcompleteDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );\n\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajaxComplete\", [ jqXHR, s ] );\n\n\t\t\t\t// Handle the global AJAX counter\n\t\t\t\tif ( !( --jQuery.active ) ) {\n\t\t\t\t\tjQuery.event.trigger( \"ajaxStop\" );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn jqXHR;\n\t},\n\n\tgetJSON: function( url, data, callback ) {\n\t\treturn jQuery.get( url, data, callback, \"json\" );\n\t},\n\n\tgetScript: function( url, callback ) {\n\t\treturn jQuery.get( url, undefined, callback, \"script\" );\n\t}\n} );\n\njQuery.each( [ \"get\", \"post\" ], function( i, method ) {\n\tjQuery[ method ] = function( url, data, callback, type ) {\n\n\t\t// Shift arguments if data argument was omitted\n\t\tif ( isFunction( data ) ) {\n\t\t\ttype = type || callback;\n\t\t\tcallback = data;\n\t\t\tdata = undefined;\n\t\t}\n\n\t\t// The url can be an options object (which then must have .url)\n\t\treturn jQuery.ajax( jQuery.extend( {\n\t\t\turl: url,\n\t\t\ttype: method,\n\t\t\tdataType: type,\n\t\t\tdata: data,\n\t\t\tsuccess: callback\n\t\t}, jQuery.isPlainObject( url ) && url ) );\n\t};\n} );\n\n\njQuery._evalUrl = function( url ) {\n\treturn jQuery.ajax( {\n\t\turl: url,\n\n\t\t// Make this explicit, since user can override this through ajaxSetup (#11264)\n\t\ttype: \"GET\",\n\t\tdataType: \"script\",\n\t\tcache: true,\n\t\tasync: false,\n\t\tglobal: false,\n\t\t\"throws\": true\n\t} );\n};\n\n\njQuery.fn.extend( {\n\twrapAll: function( html ) {\n\t\tvar wrap;\n\n\t\tif ( this[ 0 ] ) {\n\t\t\tif ( isFunction( html ) ) {\n\t\t\t\thtml = html.call( this[ 0 ] );\n\t\t\t}\n\n\t\t\t// The elements to wrap the target around\n\t\t\twrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );\n\n\t\t\tif ( this[ 0 ].parentNode ) {\n\t\t\t\twrap.insertBefore( this[ 0 ] );\n\t\t\t}\n\n\t\t\twrap.map( function() {\n\t\t\t\tvar elem = this;\n\n\t\t\t\twhile ( elem.firstElementChild ) {\n\t\t\t\t\telem = elem.firstElementChild;\n\t\t\t\t}\n\n\t\t\t\treturn elem;\n\t\t\t} ).append( this );\n\t\t}\n\n\t\treturn this;\n\t},\n\n\twrapInner: function( html ) {\n\t\tif ( isFunction( html ) ) {\n\t\t\treturn this.each( function( i ) {\n\t\t\t\tjQuery( this ).wrapInner( html.call( this, i ) );\n\t\t\t} );\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tvar self = jQuery( this ),\n\t\t\t\tcontents = self.contents();\n\n\t\t\tif ( contents.length ) {\n\t\t\t\tcontents.wrapAll( html );\n\n\t\t\t} else {\n\t\t\t\tself.append( html );\n\t\t\t}\n\t\t} );\n\t},\n\n\twrap: function( html ) {\n\t\tvar htmlIsFunction = isFunction( html );\n\n\t\treturn this.each( function( i ) {\n\t\t\tjQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html );\n\t\t} );\n\t},\n\n\tunwrap: function( selector ) {\n\t\tthis.parent( selector ).not( \"body\" ).each( function() {\n\t\t\tjQuery( this ).replaceWith( this.childNodes );\n\t\t} );\n\t\treturn this;\n\t}\n} );\n\n\njQuery.expr.pseudos.hidden = function( elem ) {\n\treturn !jQuery.expr.pseudos.visible( elem );\n};\njQuery.expr.pseudos.visible = function( elem ) {\n\treturn !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );\n};\n\n\n\n\njQuery.ajaxSettings.xhr = function() {\n\ttry {\n\t\treturn new window.XMLHttpRequest();\n\t} catch ( e ) {}\n};\n\nvar xhrSuccessStatus = {\n\n\t\t// File protocol always yields status code 0, assume 200\n\t\t0: 200,\n\n\t\t// Support: IE <=9 only\n\t\t// #1450: sometimes IE returns 1223 when it should be 204\n\t\t1223: 204\n\t},\n\txhrSupported = jQuery.ajaxSettings.xhr();\n\nsupport.cors = !!xhrSupported && ( \"withCredentials\" in xhrSupported );\nsupport.ajax = xhrSupported = !!xhrSupported;\n\njQuery.ajaxTransport( function( options ) {\n\tvar callback, errorCallback;\n\n\t// Cross domain only allowed if supported through XMLHttpRequest\n\tif ( support.cors || xhrSupported && !options.crossDomain ) {\n\t\treturn {\n\t\t\tsend: function( headers, complete ) {\n\t\t\t\tvar i,\n\t\t\t\t\txhr = options.xhr();\n\n\t\t\t\txhr.open(\n\t\t\t\t\toptions.type,\n\t\t\t\t\toptions.url,\n\t\t\t\t\toptions.async,\n\t\t\t\t\toptions.username,\n\t\t\t\t\toptions.password\n\t\t\t\t);\n\n\t\t\t\t// Apply custom fields if provided\n\t\t\t\tif ( options.xhrFields ) {\n\t\t\t\t\tfor ( i in options.xhrFields ) {\n\t\t\t\t\t\txhr[ i ] = options.xhrFields[ i ];\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Override mime type if needed\n\t\t\t\tif ( options.mimeType && xhr.overrideMimeType ) {\n\t\t\t\t\txhr.overrideMimeType( options.mimeType );\n\t\t\t\t}\n\n\t\t\t\t// X-Requested-With header\n\t\t\t\t// For cross-domain requests, seeing as conditions for a preflight are\n\t\t\t\t// akin to a jigsaw puzzle, we simply never set it to be sure.\n\t\t\t\t// (it can always be set on a per-request basis or even using ajaxSetup)\n\t\t\t\t// For same-domain requests, won't change header if already provided.\n\t\t\t\tif ( !options.crossDomain && !headers[ \"X-Requested-With\" ] ) {\n\t\t\t\t\theaders[ \"X-Requested-With\" ] = \"XMLHttpRequest\";\n\t\t\t\t}\n\n\t\t\t\t// Set headers\n\t\t\t\tfor ( i in headers ) {\n\t\t\t\t\txhr.setRequestHeader( i, headers[ i ] );\n\t\t\t\t}\n\n\t\t\t\t// Callback\n\t\t\t\tcallback = function( type ) {\n\t\t\t\t\treturn function() {\n\t\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\t\tcallback = errorCallback = xhr.onload =\n\t\t\t\t\t\t\t\txhr.onerror = xhr.onabort = xhr.ontimeout =\n\t\t\t\t\t\t\t\t\txhr.onreadystatechange = null;\n\n\t\t\t\t\t\t\tif ( type === \"abort\" ) {\n\t\t\t\t\t\t\t\txhr.abort();\n\t\t\t\t\t\t\t} else if ( type === \"error\" ) {\n\n\t\t\t\t\t\t\t\t// Support: IE <=9 only\n\t\t\t\t\t\t\t\t// On a manual native abort, IE9 throws\n\t\t\t\t\t\t\t\t// errors on any property access that is not readyState\n\t\t\t\t\t\t\t\tif ( typeof xhr.status !== \"number\" ) {\n\t\t\t\t\t\t\t\t\tcomplete( 0, \"error\" );\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tcomplete(\n\n\t\t\t\t\t\t\t\t\t\t// File: protocol always yields status 0; see #8605, #14207\n\t\t\t\t\t\t\t\t\t\txhr.status,\n\t\t\t\t\t\t\t\t\t\txhr.statusText\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tcomplete(\n\t\t\t\t\t\t\t\t\txhrSuccessStatus[ xhr.status ] || xhr.status,\n\t\t\t\t\t\t\t\t\txhr.statusText,\n\n\t\t\t\t\t\t\t\t\t// Support: IE <=9 only\n\t\t\t\t\t\t\t\t\t// IE9 has no XHR2 but throws on binary (trac-11426)\n\t\t\t\t\t\t\t\t\t// For XHR2 non-text, let the caller handle it (gh-2498)\n\t\t\t\t\t\t\t\t\t( xhr.responseType || \"text\" ) !== \"text\"  ||\n\t\t\t\t\t\t\t\t\ttypeof xhr.responseText !== \"string\" ?\n\t\t\t\t\t\t\t\t\t\t{ binary: xhr.response } :\n\t\t\t\t\t\t\t\t\t\t{ text: xhr.responseText },\n\t\t\t\t\t\t\t\t\txhr.getAllResponseHeaders()\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t};\n\n\t\t\t\t// Listen to events\n\t\t\t\txhr.onload = callback();\n\t\t\t\terrorCallback = xhr.onerror = xhr.ontimeout = callback( \"error\" );\n\n\t\t\t\t// Support: IE 9 only\n\t\t\t\t// Use onreadystatechange to replace onabort\n\t\t\t\t// to handle uncaught aborts\n\t\t\t\tif ( xhr.onabort !== undefined ) {\n\t\t\t\t\txhr.onabort = errorCallback;\n\t\t\t\t} else {\n\t\t\t\t\txhr.onreadystatechange = function() {\n\n\t\t\t\t\t\t// Check readyState before timeout as it changes\n\t\t\t\t\t\tif ( xhr.readyState === 4 ) {\n\n\t\t\t\t\t\t\t// Allow onerror to be called first,\n\t\t\t\t\t\t\t// but that will not handle a native abort\n\t\t\t\t\t\t\t// Also, save errorCallback to a variable\n\t\t\t\t\t\t\t// as xhr.onerror cannot be accessed\n\t\t\t\t\t\t\twindow.setTimeout( function() {\n\t\t\t\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\t\t\t\terrorCallback();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\t// Create the abort callback\n\t\t\t\tcallback = callback( \"abort\" );\n\n\t\t\t\ttry {\n\n\t\t\t\t\t// Do send the request (this may raise an exception)\n\t\t\t\t\txhr.send( options.hasContent && options.data || null );\n\t\t\t\t} catch ( e ) {\n\n\t\t\t\t\t// #14683: Only rethrow if this hasn't been notified as an error yet\n\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\tthrow e;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tabort: function() {\n\t\t\t\tif ( callback ) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n} );\n\n\n\n\n// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432)\njQuery.ajaxPrefilter( function( s ) {\n\tif ( s.crossDomain ) {\n\t\ts.contents.script = false;\n\t}\n} );\n\n// Install script dataType\njQuery.ajaxSetup( {\n\taccepts: {\n\t\tscript: \"text/javascript, application/javascript, \" +\n\t\t\t\"application/ecmascript, application/x-ecmascript\"\n\t},\n\tcontents: {\n\t\tscript: /\\b(?:java|ecma)script\\b/\n\t},\n\tconverters: {\n\t\t\"text script\": function( text ) {\n\t\t\tjQuery.globalEval( text );\n\t\t\treturn text;\n\t\t}\n\t}\n} );\n\n// Handle cache's special case and crossDomain\njQuery.ajaxPrefilter( \"script\", function( s ) {\n\tif ( s.cache === undefined ) {\n\t\ts.cache = false;\n\t}\n\tif ( s.crossDomain ) {\n\t\ts.type = \"GET\";\n\t}\n} );\n\n// Bind script tag hack transport\njQuery.ajaxTransport( \"script\", function( s ) {\n\n\t// This transport only deals with cross domain requests\n\tif ( s.crossDomain ) {\n\t\tvar script, callback;\n\t\treturn {\n\t\t\tsend: function( _, complete ) {\n\t\t\t\tscript = jQuery( \"<script>\" ).prop( {\n\t\t\t\t\tcharset: s.scriptCharset,\n\t\t\t\t\tsrc: s.url\n\t\t\t\t} ).on(\n\t\t\t\t\t\"load error\",\n\t\t\t\t\tcallback = function( evt ) {\n\t\t\t\t\t\tscript.remove();\n\t\t\t\t\t\tcallback = null;\n\t\t\t\t\t\tif ( evt ) {\n\t\t\t\t\t\t\tcomplete( evt.type === \"error\" ? 404 : 200, evt.type );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t);\n\n\t\t\t\t// Use native DOM manipulation to avoid our domManip AJAX trickery\n\t\t\t\tdocument.head.appendChild( script[ 0 ] );\n\t\t\t},\n\t\t\tabort: function() {\n\t\t\t\tif ( callback ) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n} );\n\n\n\n\nvar oldCallbacks = [],\n\trjsonp = /(=)\\?(?=&|$)|\\?\\?/;\n\n// Default jsonp settings\njQuery.ajaxSetup( {\n\tjsonp: \"callback\",\n\tjsonpCallback: function() {\n\t\tvar callback = oldCallbacks.pop() || ( jQuery.expando + \"_\" + ( nonce++ ) );\n\t\tthis[ callback ] = true;\n\t\treturn callback;\n\t}\n} );\n\n// Detect, normalize options and install callbacks for jsonp requests\njQuery.ajaxPrefilter( \"json jsonp\", function( s, originalSettings, jqXHR ) {\n\n\tvar callbackName, overwritten, responseContainer,\n\t\tjsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?\n\t\t\t\"url\" :\n\t\t\ttypeof s.data === \"string\" &&\n\t\t\t\t( s.contentType || \"\" )\n\t\t\t\t\t.indexOf( \"application/x-www-form-urlencoded\" ) === 0 &&\n\t\t\t\trjsonp.test( s.data ) && \"data\"\n\t\t);\n\n\t// Handle iff the expected data type is \"jsonp\" or we have a parameter to set\n\tif ( jsonProp || s.dataTypes[ 0 ] === \"jsonp\" ) {\n\n\t\t// Get callback name, remembering preexisting value associated with it\n\t\tcallbackName = s.jsonpCallback = isFunction( s.jsonpCallback ) ?\n\t\t\ts.jsonpCallback() :\n\t\t\ts.jsonpCallback;\n\n\t\t// Insert callback into url or form data\n\t\tif ( jsonProp ) {\n\t\t\ts[ jsonProp ] = s[ jsonProp ].replace( rjsonp, \"$1\" + callbackName );\n\t\t} else if ( s.jsonp !== false ) {\n\t\t\ts.url += ( rquery.test( s.url ) ? \"&\" : \"?\" ) + s.jsonp + \"=\" + callbackName;\n\t\t}\n\n\t\t// Use data converter to retrieve json after script execution\n\t\ts.converters[ \"script json\" ] = function() {\n\t\t\tif ( !responseContainer ) {\n\t\t\t\tjQuery.error( callbackName + \" was not called\" );\n\t\t\t}\n\t\t\treturn responseContainer[ 0 ];\n\t\t};\n\n\t\t// Force json dataType\n\t\ts.dataTypes[ 0 ] = \"json\";\n\n\t\t// Install callback\n\t\toverwritten = window[ callbackName ];\n\t\twindow[ callbackName ] = function() {\n\t\t\tresponseContainer = arguments;\n\t\t};\n\n\t\t// Clean-up function (fires after converters)\n\t\tjqXHR.always( function() {\n\n\t\t\t// If previous value didn't exist - remove it\n\t\t\tif ( overwritten === undefined ) {\n\t\t\t\tjQuery( window ).removeProp( callbackName );\n\n\t\t\t// Otherwise restore preexisting value\n\t\t\t} else {\n\t\t\t\twindow[ callbackName ] = overwritten;\n\t\t\t}\n\n\t\t\t// Save back as free\n\t\t\tif ( s[ callbackName ] ) {\n\n\t\t\t\t// Make sure that re-using the options doesn't screw things around\n\t\t\t\ts.jsonpCallback = originalSettings.jsonpCallback;\n\n\t\t\t\t// Save the callback name for future use\n\t\t\t\toldCallbacks.push( callbackName );\n\t\t\t}\n\n\t\t\t// Call if it was a function and we have a response\n\t\t\tif ( responseContainer && isFunction( overwritten ) ) {\n\t\t\t\toverwritten( responseContainer[ 0 ] );\n\t\t\t}\n\n\t\t\tresponseContainer = overwritten = undefined;\n\t\t} );\n\n\t\t// Delegate to script\n\t\treturn \"script\";\n\t}\n} );\n\n\n\n\n// Support: Safari 8 only\n// In Safari 8 documents created via document.implementation.createHTMLDocument\n// collapse sibling forms: the second one becomes a child of the first one.\n// Because of that, this security measure has to be disabled in Safari 8.\n// https://bugs.webkit.org/show_bug.cgi?id=137337\nsupport.createHTMLDocument = ( function() {\n\tvar body = document.implementation.createHTMLDocument( \"\" ).body;\n\tbody.innerHTML = \"<form></form><form></form>\";\n\treturn body.childNodes.length === 2;\n} )();\n\n\n// Argument \"data\" should be string of html\n// context (optional): If specified, the fragment will be created in this context,\n// defaults to document\n// keepScripts (optional): If true, will include scripts passed in the html string\njQuery.parseHTML = function( data, context, keepScripts ) {\n\tif ( typeof data !== \"string\" ) {\n\t\treturn [];\n\t}\n\tif ( typeof context === \"boolean\" ) {\n\t\tkeepScripts = context;\n\t\tcontext = false;\n\t}\n\n\tvar base, parsed, scripts;\n\n\tif ( !context ) {\n\n\t\t// Stop scripts or inline event handlers from being executed immediately\n\t\t// by using document.implementation\n\t\tif ( support.createHTMLDocument ) {\n\t\t\tcontext = document.implementation.createHTMLDocument( \"\" );\n\n\t\t\t// Set the base href for the created document\n\t\t\t// so any parsed elements with URLs\n\t\t\t// are based on the document's URL (gh-2965)\n\t\t\tbase = context.createElement( \"base\" );\n\t\t\tbase.href = document.location.href;\n\t\t\tcontext.head.appendChild( base );\n\t\t} else {\n\t\t\tcontext = document;\n\t\t}\n\t}\n\n\tparsed = rsingleTag.exec( data );\n\tscripts = !keepScripts && [];\n\n\t// Single tag\n\tif ( parsed ) {\n\t\treturn [ context.createElement( parsed[ 1 ] ) ];\n\t}\n\n\tparsed = buildFragment( [ data ], context, scripts );\n\n\tif ( scripts && scripts.length ) {\n\t\tjQuery( scripts ).remove();\n\t}\n\n\treturn jQuery.merge( [], parsed.childNodes );\n};\n\n\n/**\n * Load a url into a page\n */\njQuery.fn.load = function( url, params, callback ) {\n\tvar selector, type, response,\n\t\tself = this,\n\t\toff = url.indexOf( \" \" );\n\n\tif ( off > -1 ) {\n\t\tselector = stripAndCollapse( url.slice( off ) );\n\t\turl = url.slice( 0, off );\n\t}\n\n\t// If it's a function\n\tif ( isFunction( params ) ) {\n\n\t\t// We assume that it's the callback\n\t\tcallback = params;\n\t\tparams = undefined;\n\n\t// Otherwise, build a param string\n\t} else if ( params && typeof params === \"object\" ) {\n\t\ttype = \"POST\";\n\t}\n\n\t// If we have elements to modify, make the request\n\tif ( self.length > 0 ) {\n\t\tjQuery.ajax( {\n\t\t\turl: url,\n\n\t\t\t// If \"type\" variable is undefined, then \"GET\" method will be used.\n\t\t\t// Make value of this field explicit since\n\t\t\t// user can override it through ajaxSetup method\n\t\t\ttype: type || \"GET\",\n\t\t\tdataType: \"html\",\n\t\t\tdata: params\n\t\t} ).done( function( responseText ) {\n\n\t\t\t// Save response for use in complete callback\n\t\t\tresponse = arguments;\n\n\t\t\tself.html( selector ?\n\n\t\t\t\t// If a selector was specified, locate the right elements in a dummy div\n\t\t\t\t// Exclude scripts to avoid IE 'Permission Denied' errors\n\t\t\t\tjQuery( \"<div>\" ).append( jQuery.parseHTML( responseText ) ).find( selector ) :\n\n\t\t\t\t// Otherwise use the full result\n\t\t\t\tresponseText );\n\n\t\t// If the request succeeds, this function gets \"data\", \"status\", \"jqXHR\"\n\t\t// but they are ignored because response was set above.\n\t\t// If it fails, this function gets \"jqXHR\", \"status\", \"error\"\n\t\t} ).always( callback && function( jqXHR, status ) {\n\t\t\tself.each( function() {\n\t\t\t\tcallback.apply( this, response || [ jqXHR.responseText, status, jqXHR ] );\n\t\t\t} );\n\t\t} );\n\t}\n\n\treturn this;\n};\n\n\n\n\n// Attach a bunch of functions for handling common AJAX events\njQuery.each( [\n\t\"ajaxStart\",\n\t\"ajaxStop\",\n\t\"ajaxComplete\",\n\t\"ajaxError\",\n\t\"ajaxSuccess\",\n\t\"ajaxSend\"\n], function( i, type ) {\n\tjQuery.fn[ type ] = function( fn ) {\n\t\treturn this.on( type, fn );\n\t};\n} );\n\n\n\n\njQuery.expr.pseudos.animated = function( elem ) {\n\treturn jQuery.grep( jQuery.timers, function( fn ) {\n\t\treturn elem === fn.elem;\n\t} ).length;\n};\n\n\n\n\njQuery.offset = {\n\tsetOffset: function( elem, options, i ) {\n\t\tvar curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,\n\t\t\tposition = jQuery.css( elem, \"position\" ),\n\t\t\tcurElem = jQuery( elem ),\n\t\t\tprops = {};\n\n\t\t// Set position first, in-case top/left are set even on static elem\n\t\tif ( position === \"static\" ) {\n\t\t\telem.style.position = \"relative\";\n\t\t}\n\n\t\tcurOffset = curElem.offset();\n\t\tcurCSSTop = jQuery.css( elem, \"top\" );\n\t\tcurCSSLeft = jQuery.css( elem, \"left\" );\n\t\tcalculatePosition = ( position === \"absolute\" || position === \"fixed\" ) &&\n\t\t\t( curCSSTop + curCSSLeft ).indexOf( \"auto\" ) > -1;\n\n\t\t// Need to be able to calculate position if either\n\t\t// top or left is auto and position is either absolute or fixed\n\t\tif ( calculatePosition ) {\n\t\t\tcurPosition = curElem.position();\n\t\t\tcurTop = curPosition.top;\n\t\t\tcurLeft = curPosition.left;\n\n\t\t} else {\n\t\t\tcurTop = parseFloat( curCSSTop ) || 0;\n\t\t\tcurLeft = parseFloat( curCSSLeft ) || 0;\n\t\t}\n\n\t\tif ( isFunction( options ) ) {\n\n\t\t\t// Use jQuery.extend here to allow modification of coordinates argument (gh-1848)\n\t\t\toptions = options.call( elem, i, jQuery.extend( {}, curOffset ) );\n\t\t}\n\n\t\tif ( options.top != null ) {\n\t\t\tprops.top = ( options.top - curOffset.top ) + curTop;\n\t\t}\n\t\tif ( options.left != null ) {\n\t\t\tprops.left = ( options.left - curOffset.left ) + curLeft;\n\t\t}\n\n\t\tif ( \"using\" in options ) {\n\t\t\toptions.using.call( elem, props );\n\n\t\t} else {\n\t\t\tcurElem.css( props );\n\t\t}\n\t}\n};\n\njQuery.fn.extend( {\n\n\t// offset() relates an element's border box to the document origin\n\toffset: function( options ) {\n\n\t\t// Preserve chaining for setter\n\t\tif ( arguments.length ) {\n\t\t\treturn options === undefined ?\n\t\t\t\tthis :\n\t\t\t\tthis.each( function( i ) {\n\t\t\t\t\tjQuery.offset.setOffset( this, options, i );\n\t\t\t\t} );\n\t\t}\n\n\t\tvar rect, win,\n\t\t\telem = this[ 0 ];\n\n\t\tif ( !elem ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Return zeros for disconnected and hidden (display: none) elements (gh-2310)\n\t\t// Support: IE <=11 only\n\t\t// Running getBoundingClientRect on a\n\t\t// disconnected node in IE throws an error\n\t\tif ( !elem.getClientRects().length ) {\n\t\t\treturn { top: 0, left: 0 };\n\t\t}\n\n\t\t// Get document-relative position by adding viewport scroll to viewport-relative gBCR\n\t\trect = elem.getBoundingClientRect();\n\t\twin = elem.ownerDocument.defaultView;\n\t\treturn {\n\t\t\ttop: rect.top + win.pageYOffset,\n\t\t\tleft: rect.left + win.pageXOffset\n\t\t};\n\t},\n\n\t// position() relates an element's margin box to its offset parent's padding box\n\t// This corresponds to the behavior of CSS absolute positioning\n\tposition: function() {\n\t\tif ( !this[ 0 ] ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar offsetParent, offset, doc,\n\t\t\telem = this[ 0 ],\n\t\t\tparentOffset = { top: 0, left: 0 };\n\n\t\t// position:fixed elements are offset from the viewport, which itself always has zero offset\n\t\tif ( jQuery.css( elem, \"position\" ) === \"fixed\" ) {\n\n\t\t\t// Assume position:fixed implies availability of getBoundingClientRect\n\t\t\toffset = elem.getBoundingClientRect();\n\n\t\t} else {\n\t\t\toffset = this.offset();\n\n\t\t\t// Account for the *real* offset parent, which can be the document or its root element\n\t\t\t// when a statically positioned element is identified\n\t\t\tdoc = elem.ownerDocument;\n\t\t\toffsetParent = elem.offsetParent || doc.documentElement;\n\t\t\twhile ( offsetParent &&\n\t\t\t\t( offsetParent === doc.body || offsetParent === doc.documentElement ) &&\n\t\t\t\tjQuery.css( offsetParent, \"position\" ) === \"static\" ) {\n\n\t\t\t\toffsetParent = offsetParent.parentNode;\n\t\t\t}\n\t\t\tif ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 ) {\n\n\t\t\t\t// Incorporate borders into its offset, since they are outside its content origin\n\t\t\t\tparentOffset = jQuery( offsetParent ).offset();\n\t\t\t\tparentOffset.top += jQuery.css( offsetParent, \"borderTopWidth\", true );\n\t\t\t\tparentOffset.left += jQuery.css( offsetParent, \"borderLeftWidth\", true );\n\t\t\t}\n\t\t}\n\n\t\t// Subtract parent offsets and element margins\n\t\treturn {\n\t\t\ttop: offset.top - parentOffset.top - jQuery.css( elem, \"marginTop\", true ),\n\t\t\tleft: offset.left - parentOffset.left - jQuery.css( elem, \"marginLeft\", true )\n\t\t};\n\t},\n\n\t// This method will return documentElement in the following cases:\n\t// 1) For the element inside the iframe without offsetParent, this method will return\n\t//    documentElement of the parent window\n\t// 2) For the hidden or detached element\n\t// 3) For body or html element, i.e. in case of the html node - it will return itself\n\t//\n\t// but those exceptions were never presented as a real life use-cases\n\t// and might be considered as more preferable results.\n\t//\n\t// This logic, however, is not guaranteed and can change at any point in the future\n\toffsetParent: function() {\n\t\treturn this.map( function() {\n\t\t\tvar offsetParent = this.offsetParent;\n\n\t\t\twhile ( offsetParent && jQuery.css( offsetParent, \"position\" ) === \"static\" ) {\n\t\t\t\toffsetParent = offsetParent.offsetParent;\n\t\t\t}\n\n\t\t\treturn offsetParent || documentElement;\n\t\t} );\n\t}\n} );\n\n// Create scrollLeft and scrollTop methods\njQuery.each( { scrollLeft: \"pageXOffset\", scrollTop: \"pageYOffset\" }, function( method, prop ) {\n\tvar top = \"pageYOffset\" === prop;\n\n\tjQuery.fn[ method ] = function( val ) {\n\t\treturn access( this, function( elem, method, val ) {\n\n\t\t\t// Coalesce documents and windows\n\t\t\tvar win;\n\t\t\tif ( isWindow( elem ) ) {\n\t\t\t\twin = elem;\n\t\t\t} else if ( elem.nodeType === 9 ) {\n\t\t\t\twin = elem.defaultView;\n\t\t\t}\n\n\t\t\tif ( val === undefined ) {\n\t\t\t\treturn win ? win[ prop ] : elem[ method ];\n\t\t\t}\n\n\t\t\tif ( win ) {\n\t\t\t\twin.scrollTo(\n\t\t\t\t\t!top ? val : win.pageXOffset,\n\t\t\t\t\ttop ? val : win.pageYOffset\n\t\t\t\t);\n\n\t\t\t} else {\n\t\t\t\telem[ method ] = val;\n\t\t\t}\n\t\t}, method, val, arguments.length );\n\t};\n} );\n\n// Support: Safari <=7 - 9.1, Chrome <=37 - 49\n// Add the top/left cssHooks using jQuery.fn.position\n// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084\n// Blink bug: https://bugs.chromium.org/p/chromium/issues/detail?id=589347\n// getComputedStyle returns percent when specified for top/left/bottom/right;\n// rather than make the css module depend on the offset module, just check for it here\njQuery.each( [ \"top\", \"left\" ], function( i, prop ) {\n\tjQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,\n\t\tfunction( elem, computed ) {\n\t\t\tif ( computed ) {\n\t\t\t\tcomputed = curCSS( elem, prop );\n\n\t\t\t\t// If curCSS returns percentage, fallback to offset\n\t\t\t\treturn rnumnonpx.test( computed ) ?\n\t\t\t\t\tjQuery( elem ).position()[ prop ] + \"px\" :\n\t\t\t\t\tcomputed;\n\t\t\t}\n\t\t}\n\t);\n} );\n\n\n// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods\njQuery.each( { Height: \"height\", Width: \"width\" }, function( name, type ) {\n\tjQuery.each( { padding: \"inner\" + name, content: type, \"\": \"outer\" + name },\n\t\tfunction( defaultExtra, funcName ) {\n\n\t\t// Margin is only for outerHeight, outerWidth\n\t\tjQuery.fn[ funcName ] = function( margin, value ) {\n\t\t\tvar chainable = arguments.length && ( defaultExtra || typeof margin !== \"boolean\" ),\n\t\t\t\textra = defaultExtra || ( margin === true || value === true ? \"margin\" : \"border\" );\n\n\t\t\treturn access( this, function( elem, type, value ) {\n\t\t\t\tvar doc;\n\n\t\t\t\tif ( isWindow( elem ) ) {\n\n\t\t\t\t\t// $( window ).outerWidth/Height return w/h including scrollbars (gh-1729)\n\t\t\t\t\treturn funcName.indexOf( \"outer\" ) === 0 ?\n\t\t\t\t\t\telem[ \"inner\" + name ] :\n\t\t\t\t\t\telem.document.documentElement[ \"client\" + name ];\n\t\t\t\t}\n\n\t\t\t\t// Get document width or height\n\t\t\t\tif ( elem.nodeType === 9 ) {\n\t\t\t\t\tdoc = elem.documentElement;\n\n\t\t\t\t\t// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],\n\t\t\t\t\t// whichever is greatest\n\t\t\t\t\treturn Math.max(\n\t\t\t\t\t\telem.body[ \"scroll\" + name ], doc[ \"scroll\" + name ],\n\t\t\t\t\t\telem.body[ \"offset\" + name ], doc[ \"offset\" + name ],\n\t\t\t\t\t\tdoc[ \"client\" + name ]\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\treturn value === undefined ?\n\n\t\t\t\t\t// Get width or height on the element, requesting but not forcing parseFloat\n\t\t\t\t\tjQuery.css( elem, type, extra ) :\n\n\t\t\t\t\t// Set width or height on the element\n\t\t\t\t\tjQuery.style( elem, type, value, extra );\n\t\t\t}, type, chainable ? margin : undefined, chainable );\n\t\t};\n\t} );\n} );\n\n\njQuery.each( ( \"blur focus focusin focusout resize scroll click dblclick \" +\n\t\"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave \" +\n\t\"change select submit keydown keypress keyup contextmenu\" ).split( \" \" ),\n\tfunction( i, name ) {\n\n\t// Handle event binding\n\tjQuery.fn[ name ] = function( data, fn ) {\n\t\treturn arguments.length > 0 ?\n\t\t\tthis.on( name, null, data, fn ) :\n\t\t\tthis.trigger( name );\n\t};\n} );\n\njQuery.fn.extend( {\n\thover: function( fnOver, fnOut ) {\n\t\treturn this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );\n\t}\n} );\n\n\n\n\njQuery.fn.extend( {\n\n\tbind: function( types, data, fn ) {\n\t\treturn this.on( types, null, data, fn );\n\t},\n\tunbind: function( types, fn ) {\n\t\treturn this.off( types, null, fn );\n\t},\n\n\tdelegate: function( selector, types, data, fn ) {\n\t\treturn this.on( types, selector, data, fn );\n\t},\n\tundelegate: function( selector, types, fn ) {\n\n\t\t// ( namespace ) or ( selector, types [, fn] )\n\t\treturn arguments.length === 1 ?\n\t\t\tthis.off( selector, \"**\" ) :\n\t\t\tthis.off( types, selector || \"**\", fn );\n\t}\n} );\n\n// Bind a function to a context, optionally partially applying any\n// arguments.\n// jQuery.proxy is deprecated to promote standards (specifically Function#bind)\n// However, it is not slated for removal any time soon\njQuery.proxy = function( fn, context ) {\n\tvar tmp, args, proxy;\n\n\tif ( typeof context === \"string\" ) {\n\t\ttmp = fn[ context ];\n\t\tcontext = fn;\n\t\tfn = tmp;\n\t}\n\n\t// Quick check to determine if target is callable, in the spec\n\t// this throws a TypeError, but we will just return undefined.\n\tif ( !isFunction( fn ) ) {\n\t\treturn undefined;\n\t}\n\n\t// Simulated bind\n\targs = slice.call( arguments, 2 );\n\tproxy = function() {\n\t\treturn fn.apply( context || this, args.concat( slice.call( arguments ) ) );\n\t};\n\n\t// Set the guid of unique handler to the same of original handler, so it can be removed\n\tproxy.guid = fn.guid = fn.guid || jQuery.guid++;\n\n\treturn proxy;\n};\n\njQuery.holdReady = function( hold ) {\n\tif ( hold ) {\n\t\tjQuery.readyWait++;\n\t} else {\n\t\tjQuery.ready( true );\n\t}\n};\njQuery.isArray = Array.isArray;\njQuery.parseJSON = JSON.parse;\njQuery.nodeName = nodeName;\njQuery.isFunction = isFunction;\njQuery.isWindow = isWindow;\njQuery.camelCase = camelCase;\njQuery.type = toType;\n\njQuery.now = Date.now;\n\njQuery.isNumeric = function( obj ) {\n\n\t// As of jQuery 3.0, isNumeric is limited to\n\t// strings and numbers (primitives or objects)\n\t// that can be coerced to finite numbers (gh-2662)\n\tvar type = jQuery.type( obj );\n\treturn ( type === \"number\" || type === \"string\" ) &&\n\n\t\t// parseFloat NaNs numeric-cast false positives (\"\")\n\t\t// ...but misinterprets leading-number strings, particularly hex literals (\"0x...\")\n\t\t// subtraction forces infinities to NaN\n\t\t!isNaN( obj - parseFloat( obj ) );\n};\n\n\n\n\n// Register as a named AMD module, since jQuery can be concatenated with other\n// files that may use define, but not via a proper concatenation script that\n// understands anonymous AMD modules. A named AMD is safest and most robust\n// way to register. Lowercase jquery is used because AMD module names are\n// derived from file names, and jQuery is normally delivered in a lowercase\n// file name. Do this after creating the global so that if an AMD module wants\n// to call noConflict to hide this version of jQuery, it will work.\n\n// Note that for maximum portability, libraries that are not jQuery should\n// declare themselves as anonymous modules, and avoid setting a global if an\n// AMD loader is present. jQuery is a special case. For more information, see\n// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon\n\nif ( typeof define === \"function\" && define.amd ) {\n\tdefine( \"jquery\", [], function() {\n\t\treturn jQuery;\n\t} );\n}\n\n\n\n\nvar\n\n\t// Map over jQuery in case of overwrite\n\t_jQuery = window.jQuery,\n\n\t// Map over the $ in case of overwrite\n\t_$ = window.$;\n\njQuery.noConflict = function( deep ) {\n\tif ( window.$ === jQuery ) {\n\t\twindow.$ = _$;\n\t}\n\n\tif ( deep && window.jQuery === jQuery ) {\n\t\twindow.jQuery = _jQuery;\n\t}\n\n\treturn jQuery;\n};\n\n// Expose jQuery and $ identifiers, even in AMD\n// (#7102#comment:10, https://github.com/jquery/jquery/pull/557)\n// and CommonJS for browser emulators (#13566)\nif ( !noGlobal ) {\n\twindow.jQuery = window.$ = jQuery;\n}\n\n\n\n\nreturn jQuery;\n} );\n/*# sourceMappingURL=jquery.js.map */"
  },
  {
    "path": "gui/static/js/jquery.tabledit.js",
    "content": "/*!\n * Tabledit v1.2.3 (https://github.com/markcell/jQuery-Tabledit)\n * Copyright (c) 2015 Celso Marques\n * Copyright (c) 2020 dOpenSource\n * Licensed under MIT (https://github.com/markcell/jQuery-Tabledit/blob/master/LICENSE)\n *\n * Modified draw to be public, and plugin can now be accessed via $(table).data('Tabledit') so new rows can be added dynamically\n * Modified settings to be public, plugin settings can be accessed / modified after initialization\n * Created globals to access some basic globals from outside the library\n * Allowed disabling of ajax requests, using new setting ajaxDisabled\n * Delete row permanently if restore button not enabled\n */\n\n/**\n * @description Inline editor for HTML tables compatible with Bootstrap\n * @version 1.2.3\n * @author Celso Marques\n * @author Tyler Moore\n */\n\nif (typeof jQuery === 'undefined') {\n  throw new Error('Tabledit requires jQuery library.');\n}\n\n(function($) {\n    'use strict';\n\n    $.Tabledit = function(element, options) {\n        if (!$(element).is('table')) {\n            throw new Error('Tabledit only works when applied to a table.');\n        }\n\n        var plugin = this;\n\n        var defaults = {\n            url: window.location.href,\n            inputClass: 'form-control input-sm',\n            toolbarClass: 'btn-toolbar',\n            groupClass: 'btn-group btn-group-sm',\n            dangerClass: 'danger',\n            warningClass: 'warning',\n            mutedClass: 'text-muted',\n            eventType: 'click',\n            rowIdentifier: 'id',\n            ajaxDisabled: false,\n            removeOnDelete: false,\n            hideIdentifier: false,\n            autoFocus: true,\n            editButton: true,\n            deleteButton: true,\n            saveButton: true,\n            restoreButton: true,\n            buttons: {\n                edit: {\n                    class: 'btn btn-sm btn-default',\n                    html: '<span class=\"glyphicon glyphicon-pencil\"></span>',\n                    action: 'edit'\n                },\n                delete: {\n                    class: 'btn btn-sm btn-default',\n                    html: '<span class=\"glyphicon glyphicon-trash\"></span>',\n                    action: 'delete'\n                },\n                save: {\n                    class: 'btn btn-sm btn-success',\n                    html: 'Save'\n                },\n                restore: {\n                    class: 'btn btn-sm btn-warning',\n                    html: 'Restore',\n                    action: 'restore'\n                },\n                confirm: {\n                    class: 'btn btn-sm btn-danger',\n                    html: 'Confirm'\n                }\n            },\n            onDraw: function() { return; },\n            onSuccess: function() { return; },\n            onFail: function() { return; },\n            onAlways: function() { return; },\n            onAjax: function() { return; }\n        };\n\n        plugin.settings = $.extend(true, defaults, options);\n        plugin.globals = {\n            \"tableObject\": $(element),\n            \"lastEditedRow\": $(),\n            \"lastDeletedRow\": $(),\n            \"lastRestoredRow\": $()\n        };\n\n        /**\n         * Draw Tabledit structure (identifier column, editable columns, toolbar column).\n         *\n         * @type {object}\n         */\n        plugin.Draw = {\n            columns: {\n                identifier: function() {\n                    // Hide identifier column.\n                    if (plugin.settings.hideIdentifier) {\n                        plugin.globals.tableObject.find('th:nth-child(' + parseInt(plugin.settings.columns.identifier[0]) + 1 + '), tbody td:nth-child(' + parseInt(plugin.settings.columns.identifier[0]) + 1 + ')').hide();\n                    }\n\n                    var $td = plugin.globals.tableObject.find('tbody tr:not([data-tabledit-done]) td:nth-child(' + (parseInt(plugin.settings.columns.identifier[0]) + 1) + ')');\n\n                    $td.each(function() {\n                        // Create hidden input with row identifier.\n                        var span = '<span class=\"tabledit-span tabledit-identifier\">' + $(this).text() + '</span>';\n                        var input = '<input class=\"tabledit-input tabledit-identifier\" type=\"hidden\" name=\"' + plugin.settings.columns.identifier[1] + '\" value=\"' + $(this).text() + '\" disabled>';\n\n                        // Add elements to table cell.\n                        $(this).html(span + input);\n\n                        // Add attribute \"id\" to table row.\n                        $(this).parent('tr').attr(plugin.settings.rowIdentifier, $(this).text());\n                    });\n                },\n                editable: function() {\n                    for (var i = 0; i < plugin.settings.columns.editable.length; i++) {\n                        var $td = plugin.globals.tableObject.find('tbody tr:not([data-tabledit-done]) td:nth-child(' + (parseInt(plugin.settings.columns.editable[i][0]) + 1) + ')');\n\n                        $td.each(function() {\n                            // Get text of this cell.\n                            var text = $(this).text();\n\n                            // Add pointer as cursor.\n                            if (!plugin.settings.editButton) {\n                                $(this).css('cursor', 'pointer');\n                            }\n\n                            // Create span element.\n                            var span = '<span class=\"tabledit-span\">' + text + '</span>';\n\n                            // Check if exists the third parameter of editable array.\n                            if (typeof plugin.settings.columns.editable[i][2] !== 'undefined') {\n                                // Create select element.\n                                var input = '<select class=\"tabledit-input ' + plugin.settings.inputClass + '\" name=\"' + plugin.settings.columns.editable[i][1] + '\" style=\"display: none;\" disabled>';\n\n                                // Create options for select element.\n                                $.each(jQuery.parseJSON(plugin.settings.columns.editable[i][2]), function(index, value) {\n                                    if (text === value) {\n                                        input += '<option value=\"' + index + '\" selected>' + value + '</option>';\n                                    } else {\n                                        input += '<option value=\"' + index + '\">' + value + '</option>';\n                                    }\n                                });\n\n                                // Create last piece of select element.\n                                input += '</select>';\n                            } else {\n                                // Create text input element.\n                                var input = '<input class=\"tabledit-input ' + plugin.settings.inputClass + '\" type=\"text\" name=\"' + plugin.settings.columns.editable[i][1] + '\" value=\"' + $(this).text() + '\" style=\"display: none;\" disabled>';\n                            }\n\n                            // Add elements and class \"view\" to table cell.\n                            $(this).html(span + input);\n                            $(this).addClass('tabledit-view-mode');\n                       });\n                    }\n                },\n                toolbar: function() {\n                    if (plugin.settings.editButton || plugin.settings.deleteButton) {\n                        var editButton = '';\n                        var deleteButton = '';\n                        var saveButton = '';\n                        var restoreButton = '';\n                        var confirmButton = '';\n\n                        // Add toolbar column header if not exists.\n                        if (plugin.globals.tableObject.find('th.tabledit-toolbar-column').length === 0) {\n                            plugin.globals.tableObject.find('tr:first').append('<th class=\"tabledit-toolbar-column\"></th>');\n                        }\n\n                        // Create edit button.\n                        if (plugin.settings.editButton) {\n                            editButton = '<button type=\"button\" class=\"tabledit-edit-button ' + plugin.settings.buttons.edit.class + '\" style=\"float: none;\">' + plugin.settings.buttons.edit.html + '</button>';\n                        }\n\n                        // Create delete button.\n                        if (plugin.settings.deleteButton) {\n                            deleteButton = '<button type=\"button\" class=\"tabledit-delete-button ' + plugin.settings.buttons.delete.class + '\" style=\"float: none;\">' + plugin.settings.buttons.delete.html + '</button>';\n                            confirmButton = '<button type=\"button\" class=\"tabledit-confirm-button ' + plugin.settings.buttons.confirm.class + '\" style=\"display: none; float: none;\">' + plugin.settings.buttons.confirm.html + '</button>';\n                        }\n\n                        // Create save button.\n                        if (plugin.settings.editButton && plugin.settings.saveButton) {\n                            saveButton = '<button type=\"button\" class=\"tabledit-save-button ' + plugin.settings.buttons.save.class + '\" style=\"display: none; float: none;\">' + plugin.settings.buttons.save.html + '</button>';\n                        }\n\n                        // Create restore button.\n                        if (plugin.settings.deleteButton && plugin.settings.restoreButton) {\n                            restoreButton = '<button type=\"button\" class=\"tabledit-restore-button ' + plugin.settings.buttons.restore.class + '\" style=\"display: none; float: none;\">' + plugin.settings.buttons.restore.html + '</button>';\n                        }\n\n                        var toolbar = '<div class=\"tabledit-toolbar ' + plugin.settings.toolbarClass + '\" style=\"text-align: left;\">\\n\\\n                                           <div class=\"' + plugin.settings.groupClass + '\" style=\"float: none;\">' + editButton + deleteButton + '</div>\\n\\\n                                           ' + saveButton + '\\n\\\n                                           ' + confirmButton + '\\n\\\n                                           ' + restoreButton + '\\n\\\n                                       </div></div>';\n\n                        // Add toolbar column cells.\n                        plugin.globals.tableObject.find('tr:gt(0):not([data-tabledit-done])').append('<td style=\"white-space: nowrap; width: 1%;\">' + toolbar + '</td>').attr('data-tabledit-done', 1);\n                    }\n                }\n            }\n        };\n\n        /**\n         * Change to view mode or edit mode with table td element as parameter.\n         *\n         * @type object\n         */\n        var Mode = {\n            view: function(td) {\n                // Get table row.\n                var $tr = $(td).parent('tr');\n                // Disable identifier.\n                $(td).parent('tr').find('.tabledit-input.tabledit-identifier').prop('disabled', true);\n                // Hide and disable input element.\n                $(td).find('.tabledit-input').blur().hide().prop('disabled', true);\n                // Show span element.\n                $(td).find('.tabledit-span').show();\n                // Add \"view\" class and remove \"edit\" class in td element.\n                $(td).addClass('tabledit-view-mode').removeClass('tabledit-edit-mode');\n                // Update toolbar buttons.\n                if (plugin.settings.editButton) {\n                    $tr.find('button.tabledit-save-button').hide();\n                    $tr.find('button.tabledit-edit-button').removeClass('active').blur();\n                }\n            },\n            edit: function(td) {\n                Delete.reset(td);\n                // Get table row.\n                var $tr = $(td).parent('tr');\n                // Enable identifier.\n                $tr.find('.tabledit-input.tabledit-identifier').prop('disabled', false);\n                // Hide span element.\n                $(td).find('.tabledit-span').hide();\n                // Get input element.\n                var $input = $(td).find('.tabledit-input');\n                // Enable and show input element.\n                $input.prop('disabled', false).show();\n                // Focus on input element.\n                if (plugin.settings.autoFocus) {\n                    $input.focus();\n                }\n                // Add \"edit\" class and remove \"view\" class in td element.\n                $(td).addClass('tabledit-edit-mode').removeClass('tabledit-view-mode');\n                // Update toolbar buttons.\n                if (plugin.settings.editButton) {\n                    $tr.find('button.tabledit-edit-button').addClass('active');\n                    $tr.find('button.tabledit-save-button').show();\n                }\n            }\n        };\n\n        /**\n         * Available actions for edit function, with table td element as parameter or set of td elements.\n         *\n         * @type object\n         */\n        var Edit = {\n            reset: function(td) {\n                $(td).each(function() {\n                    // Get input element.\n                    var $input = $(this).find('.tabledit-input');\n                    // Get span text.\n                    var text = $(this).find('.tabledit-span').text();\n                    // Set input/select value with span text.\n                    if ($input.is('select')) {\n                        $input.find('option').filter(function() {\n                            return $.trim($(this).text()) === text;\n                        }).attr('selected', true);\n                    } else {\n                        $input.val(text);\n                    }\n                    // Change to view mode.\n                    Mode.view(this);\n                });\n            },\n            submit: function(td) {\n                // Send AJAX request to server.\n                var ajaxResult = ajax(plugin.settings.buttons.edit.action);\n\n                if (ajaxResult === false) {\n                    return;\n                }\n\n                $(td).each(function() {\n                    // Get input element.\n                    var $input = $(this).find('.tabledit-input');\n                    // Set span text with input/select new value.\n                    if ($input.is('select')) {\n                        $(this).find('.tabledit-span').text($input.find('option:selected').text());\n                    } else {\n                        $(this).find('.tabledit-span').text($input.val());\n                    }\n                    // Change to view mode.\n                    Mode.view(this);\n                });\n\n                // Set last edited column and row.\n                plugin.globals.lastEditedRow = $(td).parent('tr');\n            }\n        };\n\n        /**\n         * Available actions for delete function, with button as parameter.\n         *\n         * @type object\n         */\n        var Delete = {\n            reset: function(td) {\n                // Reset delete button to initial status.\n                plugin.globals.tableObject.find('.tabledit-confirm-button').hide();\n                // Remove \"active\" class in delete button.\n                plugin.globals.tableObject.find('.tabledit-delete-button').removeClass('active').blur();\n            },\n            submit: function(td) {\n                Delete.reset(td);\n                // Enable identifier hidden input.\n                $(td).parent('tr').find('input.tabledit-identifier').attr('disabled', false);\n                // Send AJAX request to server.\n                var ajaxResult = ajax(plugin.settings.buttons.delete.action);\n                // Disable identifier hidden input.\n                $(td).parents('tr').find('input.tabledit-identifier').attr('disabled', true);\n\n                if (ajaxResult === false) {\n                    return;\n                }\n\n                // delete permanently if restore button not enabled\n                if (plugin.settings.restoreButton === false) {\n                    $(td).parent('tr').remove();\n                }\n                else {\n                    // Add class \"deleted\" to row.\n                    $(td).parent('tr').addClass('tabledit-deleted-row');\n                    // Hide table row.\n                    $(td).parent('tr').addClass(plugin.settings.mutedClass).find('.tabledit-toolbar button:not(.tabledit-restore-button)').attr('disabled', true);\n                    // Show restore button.\n                    $(td).find('.tabledit-restore-button').show();\n                    // Set last deleted row.\n                    plugin.globals.lastDeletedRow = $(td).parent('tr');\n                }\n            },\n            confirm: function(td) {\n                // Reset all cells in edit mode.\n                plugin.globals.tableObject.find('td.tabledit-edit-mode').each(function() {\n                    Edit.reset(this);\n                });\n                // Add \"active\" class in delete button.\n                $(td).find('.tabledit-delete-button').addClass('active');\n                // Show confirm button.\n                $(td).find('.tabledit-confirm-button').show();\n            },\n            restore: function(td) {\n                // Enable identifier hidden input.\n                $(td).parent('tr').find('input.tabledit-identifier').attr('disabled', false);\n                // Send AJAX request to server.\n                var ajaxResult = ajax(plugin.settings.buttons.restore.action);\n                // Disable identifier hidden input.\n                $(td).parents('tr').find('input.tabledit-identifier').attr('disabled', true);\n\n                if (ajaxResult === false) {\n                    return;\n                }\n\n                // Remove class \"deleted\" to row.\n                $(td).parent('tr').removeClass('tabledit-deleted-row');\n                // Hide table row.\n                $(td).parent('tr').removeClass(plugin.settings.mutedClass).find('.tabledit-toolbar button').attr('disabled', false);\n                // Hide restore button.\n                $(td).find('.tabledit-restore-button').hide();\n                // Set last restored row.\n                plugin.globals.lastRestoredRow = $(td).parent('tr');\n            }\n        };\n\n        /**\n         * Send AJAX request to server.\n         *\n         * @param {string} action\n         */\n        function ajax(action)\n        {\n            if (plugin.settings.ajaxDisabled) {\n                try {\n                    if (action === plugin.settings.buttons.edit.action) {\n                        plugin.globals.lastEditedRow.removeClass(plugin.settings.dangerClass).addClass(plugin.settings.warningClass);\n                        setTimeout(function() {\n                            plugin.globals.tableObject.find('tr.' + plugin.settings.warningClass).removeClass(plugin.settings.warningClass);\n                        }, 1400);\n                    }\n                    return true;\n                }\n                catch(error) {\n                    if (action === plugin.settings.buttons.delete.action) {\n                        plugin.globals.lastDeletedRow.removeClass(plugin.settings.mutedClass).addClass(plugin.settings.dangerClass);\n                        plugin.globals.lastDeletedRow.find('.tabledit-toolbar button').attr('disabled', false);\n                        plugin.globals.lastDeletedRow.find('.tabledit-toolbar .tabledit-restore-button').hide();\n                    }\n                    else if (action === plugin.settings.buttons.edit.action) {\n                        plugin.globals.lastEditedRow.addClass(plugin.settings.dangerClass);\n                    }\n                    return false;\n                }\n            }\n\n            var serialize = plugin.globals.tableObject.find('.tabledit-input').serialize() + '&action=' + action;\n\n            var result = plugin.settings.onAjax(action, serialize);\n\n            if (result === false) {\n                return false;\n            }\n\n            var jqXHR = $.post(plugin.settings.url, serialize, function(data, textStatus, jqXHR) {\n                if (action === plugin.settings.buttons.edit.action) {\n                    plugin.globals.lastEditedRow.removeClass(plugin.settings.dangerClass).addClass(plugin.settings.warningClass);\n                    setTimeout(function() {\n                        //plugin.globals.lastEditedRow.removeClass(plugin.settings.warningClass);\n                        plugin.globals.tableObject.find('tr.' + plugin.settings.warningClass).removeClass(plugin.settings.warningClass);\n                    }, 1400);\n                }\n\n                plugin.settings.onSuccess(data, textStatus, jqXHR);\n            }, 'json');\n\n            jqXHR.fail(function(jqXHR, textStatus, errorThrown) {\n                if (action === plugin.settings.buttons.delete.action) {\n                    plugin.globals.lastDeletedRow.removeClass(plugin.settings.mutedClass).addClass(plugin.settings.dangerClass);\n                    plugin.globals.lastDeletedRow.find('.tabledit-toolbar button').attr('disabled', false);\n                    plugin.globals.lastDeletedRow.find('.tabledit-toolbar .tabledit-restore-button').hide();\n                } else if (action === plugin.settings.buttons.edit.action) {\n                    plugin.globals.lastEditedRow.addClass(plugin.settings.dangerClass);\n                }\n\n                plugin.settings.onFail(jqXHR, textStatus, errorThrown);\n            });\n\n            jqXHR.always(function() {\n                plugin.settings.onAlways();\n            });\n\n            return jqXHR;\n        }\n\n        plugin.Draw.columns.identifier();\n        plugin.Draw.columns.editable();\n        plugin.Draw.columns.toolbar();\n\n        plugin.settings.onDraw();\n\n        plugin.reload = function() {\n            plugin.globals.lastEditedRow = $();\n            plugin.globals.lastDeletedRow = $();\n            plugin.globals.lastRestoredRow = $();\n            plugin.Draw.columns.identifier();\n            plugin.Draw.columns.editable();\n            plugin.Draw.columns.toolbar();\n            plugin.settings.onDraw();\n        };\n\n        if (plugin.settings.deleteButton) {\n            /**\n             * Delete one row.\n             *\n             * @param {object} event\n             */\n            plugin.globals.tableObject.on('click', 'button.tabledit-delete-button', function(event) {\n                if (event.handled !== true) {\n                    event.preventDefault();\n\n                    // Get current state before reset to view mode.\n                    var activated = $(this).hasClass('active');\n\n                    var $td = $(this).parents('td');\n\n                    Delete.reset($td);\n\n                    if (!activated) {\n                        Delete.confirm($td);\n                    }\n\n                    event.handled = true;\n                }\n            });\n\n            /**\n             * Delete one row (confirm).\n             *\n             * @param {object} event\n             */\n            plugin.globals.tableObject.on('click', 'button.tabledit-confirm-button', function(event) {\n                if (event.handled !== true) {\n                    event.preventDefault();\n\n                    var $td = $(this).parents('td');\n\n                    Delete.submit($td);\n\n                    event.handled = true;\n                }\n            });\n        }\n\n        if (plugin.settings.restoreButton) {\n            /**\n             * Restore one row.\n             *\n             * @param {object} event\n             */\n            plugin.globals.tableObject.on('click', 'button.tabledit-restore-button', function(event) {\n                if (event.handled !== true) {\n                    event.preventDefault();\n\n                    Delete.restore($(this).parents('td'));\n\n                    event.handled = true;\n                }\n            });\n        }\n\n        if (plugin.settings.editButton) {\n            /**\n             * Activate edit mode on all columns.\n             *\n             * @param {object} event\n             */\n            plugin.globals.tableObject.on('click', 'button.tabledit-edit-button', function(event) {\n                if (event.handled !== true) {\n                    event.preventDefault();\n\n                    var $button = $(this);\n\n                    // Get current state before reset to view mode.\n                    var activated = $button.hasClass('active');\n\n                    // Change to view mode columns that are in edit mode.\n                    Edit.reset(plugin.globals.tableObject.find('td.tabledit-edit-mode'));\n\n                    if (!activated) {\n                        // Change to edit mode for all columns in reverse way.\n                        $($button.parents('tr').find('td.tabledit-view-mode').get().reverse()).each(function() {\n                            Mode.edit(this);\n                        });\n                    }\n\n                    event.handled = true;\n                }\n            });\n\n            /**\n             * Save edited row.\n             *\n             * @param {object} event\n             */\n            plugin.globals.tableObject.on('click', 'button.tabledit-save-button', function(event) {\n                if (event.handled !== true) {\n                    event.preventDefault();\n\n                    // Submit and update all columns.\n                    Edit.submit($(this).parents('tr').find('td.tabledit-edit-mode'));\n\n                    event.handled = true;\n                }\n            });\n        } else {\n            /**\n             * Change to edit mode on table td element.\n             *\n             * @param {object} event\n             */\n            plugin.globals.tableObject.on(plugin.settings.eventType, 'tr:not(.tabledit-deleted-row) td.tabledit-view-mode', function(event) {\n                if (event.handled !== true) {\n                    event.preventDefault();\n\n                    // Reset all td's in edit mode.\n                    Edit.reset(plugin.globals.tableObject.find('td.tabledit-edit-mode'));\n\n                    // Change to edit mode.\n                    Mode.edit(this);\n\n                    event.handled = true;\n                }\n            });\n\n            /**\n             * Change event when input is a select element.\n             */\n            plugin.globals.tableObject.on('change', 'select.tabledit-input:visible', function() {\n                if (event.handled !== true) {\n                    // Submit and update the column.\n                    Edit.submit($(this).parent('td'));\n\n                    event.handled = true;\n                }\n            });\n\n            /**\n             * Click event on document element.\n             *\n             * @param {object} event\n             */\n            $(document).on('click', function(event) {\n                var $editMode = plugin.globals.tableObject.find('.tabledit-edit-mode');\n                // Reset visible edit mode column.\n                if (!$editMode.is(event.target) && $editMode.has(event.target).length === 0) {\n                    Edit.reset(plugin.globals.tableObject.find('.tabledit-input:visible').parent('td'));\n                }\n            });\n        }\n\n        /**\n         * Keyup event on document element.\n         *\n         * @param {object} event\n         */\n        $(document).on('keyup', function(event) {\n            // Get input element with focus or confirmation button.\n            var $input = plugin.globals.tableObject.find('.tabledit-input:visible');\n            var $button = plugin.globals.tableObject.find('.tabledit-confirm-button');\n\n            if ($input.length > 0) {\n                var $td = $input.parents('td');\n            } else if ($button.length > 0 && event.keyCode == 27) {\n                var $td = $button.parents('td');\n            } else {\n                return;\n            }\n\n            // Key?\n            switch (event.keyCode) {\n                case 9:  // Tab.\n                    if (!plugin.settings.editButton) {\n                        Edit.submit($td);\n                        Mode.edit($td.closest('td').next());\n                    }\n                    break;\n                case 13: // Enter.\n                    Edit.submit($td);\n                    break;\n                case 27: // Escape.\n                    Edit.reset($td);\n                    Delete.reset($td);\n                    break;\n            }\n        });\n    };\n\n    $.fn.Tabledit = function (options) {\n        return this.each(function () {\n            if (undefined == $(this).data('Tabledit')) {\n                var plugin = new $.Tabledit(this, options);\n                $(this).data('Tabledit', plugin);\n            }\n        });\n    }\n\n}(jQuery));\n"
  },
  {
    "path": "gui/static/js/license_manager.js",
    "content": ";(function(window, document) {\n  'use strict';\n\n  // throw an error if required functions not defined\n  if (typeof validateFields === \"undefined\") {\n    throw new Error(\"validateFields() is required and is not defined\");\n  }\n  if (typeof showNotification === \"undefined\") {\n    throw new Error(\"showNotification() is required and is not defined\");\n  }\n  if (typeof toggleElemDisabled === \"undefined\") {\n    throw new Error(\"toggleElemDisabled() is required and is not defined\");\n  }\n\n  // throw an error if required globals not defined\n  if (typeof API_BASE_URL === \"undefined\") {\n    throw new Error(\"API_BASE_URL is required and is not defined\");\n  }\n\n  // global variables/constants for this script\n  var license_table = {};\n  var loading_spinner = $('#loading-spinner');\n\n  // TODO: add laading animation while waiting on woocommerce query to finish\n  function activateLicense() {\n    var selector = \"#add\";\n    var modal_body = $(selector + ' .modal-body');\n    var payload = {\n      \"license_key\": modal_body.find(\".key\").val(),\n      \"key_encrypted\": false,\n    };\n\n    $.ajax({\n      type: \"PUT\",\n      url: API_BASE_URL + \"licensing/activate\",\n      dataType: \"json\",\n      contentType: \"application/json; charset=utf-8\",\n      data: JSON.stringify(payload),\n      success: function(response, textStatus, jqXHR) {\n        if (response.error.length !== 0) {\n          showNotification(response.msg, true);\n          return;\n        }\n\n        hideModal('#add');\n        showNotification(response.msg);\n\n        license_table.row.add({\n          \"tags\": response.data[0].tags,\n          \"license_key\": response.data[0].license_key,\n          \"active\": response.data[0].active,\n          \"valid\": response.data[0].valid,\n          \"expires\": response.data[0].expires,\n        }).draw();\n      },\n      error: function(xhr, msg, error_msg) {\n        error_msg = JSON.parse(xhr.responseText)[\"msg\"];\n        showNotification(error_msg, true);\n      },\n    })\n  }\n\n  // TODO: add laading animation while waiting on woocommerce query to finish\n  function deactivateLicense() {\n    var modal_body = $('#delete .modal-body');\n\n    var license_key = modal_body.find(\".key\").val().trim();\n\n    var payload = {\n      \"license_key\": license_key,\n      \"key_encrypted\": false,\n    };\n\n    $.ajax({\n      type: \"PUT\",\n      url: API_BASE_URL + \"licensing/deactivate\",\n      dataType: \"json\",\n      contentType: \"application/json; charset=utf-8\",\n      data: JSON.stringify(payload),\n      success: function(response, textStatus, jqXHR) {\n        if (response.error.length !== 0) {\n          showNotification(response.msg, true);\n          return;\n        }\n\n        hideModal('#delete');\n        showNotification(response.msg);\n\n        license_table.row(function(idx, data, node) {\n          return data.license_key === license_key;\n        }).remove().draw();\n      },\n      error: function(xhr, msg, error_msg) {\n        error_msg = JSON.parse(xhr.responseText)[\"msg\"];\n        showNotification(error_msg, true);\n      },\n    });\n  }\n\n  // noinspection JSUnusedLocalSymbols\n  function updateDeleteModal(self=null) {\n    // attached via vanilla js event listener or called outside an event listener\n    if (self === null) {\n      self = $(this);\n    }\n    // attached via jQuery event listener\n    else if (self instanceof jQuery.Event) {\n      self = $(self.currentTarget);\n    }\n    // calling node passed ref to itself\n    else {\n      self = $(self);\n    }\n\n    var modal_body = $('#delete .modal-body');\n    var license_key = self.closest('tr').find('.key').val().trim();\n\n    modal_body.find(\".key\").val(license_key);\n  }\n  // export function to make it available in scope when called via dataTable widgets\n  window.updateDeleteModal = updateDeleteModal;\n\n  // noinspection JSUnusedLocalSymbols\n  function togglePasswordHidden(self=null) {\n    // attached via vanilla js event listener or called outside an event listener\n    if (self === null) {\n      self = $(this);\n    }\n    // attached via jQuery event listener\n    else if (self instanceof jQuery.Event) {\n      self = $(self.currentTarget);\n    }\n    // calling node passed ref to itself\n    else {\n      self = $(self);\n    }\n\n    var input = $(self.attr(\"data-toggle\"));\n    if (input.attr(\"type\") === \"password\") {\n      input.attr(\"type\", \"text\");\n      self.removeClass(\"glyphicon glyphicon-eye-close\");\n      self.addClass(\"glyphicon glyphicon-eye-open\");\n    }\n    else {\n      input.attr(\"type\", \"password\");\n      self.removeClass(\"glyphicon glyphicon-eye-open\");\n      self.addClass(\"glyphicon glyphicon-eye-close\");\n    }\n  }\n  // export function to make it available in scope when called via dataTable widgets\n  window.togglePasswordHidden = togglePasswordHidden;\n\n  function createDeleteButton() {\n    return '' +\n      '<div class=\"dt-resize-height\">' +\n      '  <button class=\"open-Delete btn btn-danger btn-xs\" data-title=\"Deactivate\" data-toggle=\"modal\" data-target=\"#delete\" onclick=\"updateDeleteModal(this)\">' +\n      '    <span class=\"glyphicon glyphicon-trash\"></span>' +\n      '  </button>' +\n      '</div>';\n  }\n\n  function createLicenseKeyField(data, type, row, meta) {\n    var unique_key_id = \"key-\" + meta.row;\n\n    return '' +\n      '<div class=\"wrapper-fieldicon-right dt-resize-height\">' +\n      '  <input id=\"' + unique_key_id + '\" class=\"key\" type=\"password\" name=\"key\" value=\"' + data + '\" readonly>' +\n      '  <span class=\"field-icon toggle-password glyphicon glyphicon-eye-close\" data-toggle=\"#' + unique_key_id + '\" onclick=\"togglePasswordHidden(this)\"></span>' +\n      '</div>';\n  }\n\n  function createReadonlyInputField(data, type, row, meta) {\n    return '' +\n      '<div class=\"dt-resize-height\">' +\n      '  <input type=\"text\" value=\"' + data + '\" readonly>' +\n      '</div>';\n  }\n\n  function createReadonlyTextArea(data, type, row, meta) {\n    return '' +\n      '<div class=\"dt-resize-height\">' +\n      '  <textarea readonly>' + data.join('\\n') + '</textarea>' +\n      '</div>';\n  }\n\n  function dtDrawCallback(settings) {\n    var rows = $('#licensing > tbody > tr');\n    var row_height = rows.eq(0).find('td:eq(0)').get(0).clientHeight;\n    rows.find('td .dt-resize-height').css({'height': row_height});\n  }\n\n  function hideModal(selector) {\n    var modal_elem = $(selector);\n    // do nothing if not visible\n    if (modal_elem.is(':visible')) {\n      // hide the modal after 1.5 sec\n      setTimeout(function() {\n        modal_elem.modal('hide');\n      }, 1500);\n    }\n  }\n\n  $(document).ready(function() {\n    // datatable init\n    license_table = $('#licensing').DataTable({\n      \"ajax\": {\n        \"url\": API_BASE_URL + \"licensing/list\",\n        \"type\": \"GET\",\n        \"error\": function(xhr, error, code) {\n          var response = JSON.parse(xhr.responseText);\n          requestErrorHandler(xhr.status, response.msg, response.error);\n        }\n      },\n      \"columns\": [\n        {\"data\": \"tags\", \"render\": createReadonlyTextArea},\n        {\"data\": \"license_key\", \"render\": createLicenseKeyField},\n        {\"data\": \"active\", \"render\": createReadonlyInputField},\n        {\"data\": \"valid\", \"render\": createReadonlyInputField},\n        {\"data\": \"expires\", \"render\": createReadonlyInputField},\n        {\"data\": null, \"render\": createDeleteButton, \"searchable\": false, \"orderable\": false},\n      ],\n      \"order\": [[0, 'asc']],\n      \"drawCallback\": dtDrawCallback,\n    });\n\n    // make license key a password hidden field\n    $(\".toggle-password\").on('click', togglePasswordHidden);\n\n    // reset add modal before displaying\n    $('#open-LicenseAdd').click(function() {\n      var modal_body = $('#add .modal-body');\n      var key = modal_body.find(\".key\");\n      var toggle = modal_body.find(\".toggle-password\");\n\n      // clear fields\n      key.val('');\n\n      // reset toggles\n      if (key.attr(\"type\") !== \"password\") {\n        key.attr(\"type\", \"password\");\n        toggle.removeClass(\"glyphicon glyphicon-eye-open\");\n        toggle.addClass(\"glyphicon glyphicon-eye-close\");\n      }\n    });\n\n    // submit activation request to api\n    $('#addButton').click(function(ev) {\n      /* prevent form default submit */\n      ev.preventDefault();\n\n      if (validateFields('#add')) {\n        activateLicense();\n      }\n    });\n\n    // submit deactivation request to api\n    $('#deleteButton').click(function(ev) {\n      ev.preventDefault();\n\n      deactivateLicense();\n    });\n  });\n\n})(window, document);\n"
  },
  {
    "path": "gui/static/js/main.js",
    "content": "/* TODO: decouple scope like other js scripts */\n\n// throw an error if required functions not defined\nif (typeof showNotification === \"undefined\") {\n  throw new Error(\"showNotification() is required and is not defined\");\n}\nif (typeof descendingSearch === \"undefined\") {\n  throw new Error(\"descendingSearch() is required and is not defined\");\n}\nif (typeof toggleElemDisabled === \"undefined\") {\n  throw new Error(\"toggleElemDisabled() is required and is not defined\");\n}\nif (typeof runUntilTimeout === \"undefined\") {\n  throw new Error(\"runUntilTimeout() is required and is not defined\");\n}\nif (typeof reloadKamRequired === \"undefined\") {\n  throw new Error(\"reloadKamRequired() is required and is not defined\");\n}\nif (typeof reloadDsipRequired === \"undefined\") {\n  throw new Error(\"reloadDsipRequired() is required and is not defined\");\n}\n\n// throw an error if required globals not defined\nif (typeof API_BASE_URL === \"undefined\") {\n  throw new Error(\"API_BASE_URL is required and is not defined\");\n}\n\n/* TODO: replace shorthands with $(document).ready(...) its more verbose */\n$(function() {\n  var accordionActive = false;\n\n  $(window).on('resize', function() {\n    var windowWidth = $(window).width();\n    var $topMenu = $('#top-menu');\n    var $sideMenu = $('#side-menu');\n    var top_bar = $('.top-bar');\n    var msg_bar = $('.message-bar');\n\n    if (windowWidth < 768) {\n      top_bar.show();\n      msg_bar.hide();\n\n      if ($topMenu.hasClass(\"active\")) {\n        $topMenu.removeClass(\"active\");\n        $sideMenu.addClass(\"active\");\n\n        var $ddl = $('#top-menu .movable.dropdown');\n        $ddl.detach();\n        $ddl.removeClass('dropdown');\n        $ddl.addClass('nav-header');\n\n        $ddl.find('.dropdown-toggle').removeClass('dropdown-toggle').addClass('link');\n        $ddl.find('.dropdown-menu').removeClass('dropdown-menu').addClass('submenu');\n\n        $ddl.prependTo($sideMenu.find('.accordion'));\n        $('#top-menu #qform').detach().removeClass('navbar-form').prependTo($sideMenu);\n\n        if (!accordionActive) {\n          var Accordion2 = function(el, multiple) {\n            this.el = el || {};\n            this.multiple = multiple || false;\n\n            // Variables privadas\n            var links = this.el.find('.movable .link');\n            // Evento\n            links.on('click', {el: this.el, multiple: this.multiple}, this.dropdown);\n          };\n\n          Accordion2.prototype.dropdown = function(e) {\n            var $el = e.data.el;\n            $this = $(this);\n            $next = $this.next();\n\n            $next.slideToggle();\n            $this.parent().toggleClass('open');\n\n            if (!e.data.multiple) {\n              $el.find('.movable .submenu').not($next).slideUp().parent().removeClass('open');\n            }\n          };\n\n          var accordion = new Accordion2($('ul.accordion'), false);\n          accordionActive = true;\n        }\n      }\n    }\n    else {\n      top_bar.hide();\n      msg_bar.show();\n\n      if ($sideMenu.hasClass(\"active\")) {\n        $sideMenu.removeClass('active');\n        $topMenu.addClass('active');\n\n        var $ddl = $('#side-menu .movable.nav-header');\n        $ddl.detach();\n        $ddl.removeClass('nav-header');\n        $ddl.addClass('dropdown');\n\n        $ddl.find('.link').removeClass('link').addClass('dropdown-toggle');\n        $ddl.find('.submenu').removeClass('submenu').addClass('dropdown-menu');\n\n        $('#side-menu #qform').detach().addClass('navbar-form').appendTo($topMenu.find('.nav'));\n        $ddl.appendTo($topMenu.find('.nav'));\n      }\n    }\n  });\n\n  /**/\n  var $menulink = $('.side-menu-link'),\n    $wrap = $('.wrap');\n\n  $menulink.click(function() {\n    $menulink.toggleClass('active');\n    $wrap.toggleClass('active');\n    return false;\n  });\n\n  /*Accordion*/\n  var Accordion = function(el, multiple) {\n    this.el = el || {};\n    this.multiple = multiple || false;\n\n    // Variables privadas\n    var links = this.el.find('.link');\n    // Evento\n    links.on('click', {el: this.el, multiple: this.multiple}, this.dropdown);\n  };\n\n  Accordion.prototype.dropdown = function(e) {\n    var $el = e.data.el;\n    var $this = $(this);\n    var $next = $this.next();\n    var anchor = $this.find('a')\n\n    $next.slideToggle();\n    $this.parent().toggleClass('open');\n\n    if (!e.data.multiple) {\n      $el.find('.submenu').not($next).slideUp().parent().removeClass('open');\n    }\n  };\n\n  var accordion = new Accordion($('ul.accordion'), false);\n});\n\n$(function() {\n  /* styling links */\n  $('a').each(function() {\n    if ($(this).prop('href') === window.location.href) {\n      $(this).removeClass('navlink');\n      $(this).addClass('currentlink');\n    }\n  });\n  /* prevent empty links from jumping to top of page */\n  $('a[href$=\\\\#]').on('click', function(event) {\n    event.preventDefault();\n  });\n});\n\n/* TODO: do we have a use for this anymore? */\n/* Update an attribute of an endpoint\n/* row - Javascript DOM that contains the row of the PBX\n * attr - is the attribute that we want to update\n */\n// function updateEndpoint(row, attr, attrvalue) {\n//   checkbox = row.cells[0].getElementsByClassName('checkthis');\n//   pbxid = checkbox[0].value;\n//   requestPayload = '{\"maintmode\":' + attrvalue + '}';\n//\n//   $.ajax({\n//     type: \"POST\",\n//     url: \"/api/v1/endpoint/\" + pbxid,\n//     dataType: \"json\",\n//     contentType: \"application/json; charset=utf-8\",\n//     success: function(response, text_status, xhr) {\n//       // uncheck the Checkbox\n//       if (attr === 'maintmode') {\n//         $('#checkbox_' + pbxid)[0].checked = false;\n//         if (attrvalue == 1) {\n//           $('#maintmode_' + pbxid).html(\"<span class='glyphicon glyphicon-wrench'>\");\n//         }\n//         else {\n//           $('#maintmode_' + pbxid).html(\"\");\n//         }\n//       }\n//     },\n//     data: requestPayload\n//   });\n// }\n\n/* TODO: update to work with endpoint groups */\n// function enableMaintenanceMode() {\n//   var table = document.getElementById(\"pbxs\");\n//\n//   r = 1;\n//   while (row = table.rows[r++]) {\n//     checkbox = row.cells[0].getElementsByClassName('checkthis');\n//     if (checkbox[0].checked) {\n//       updateEndpoint(row, 'maintmode', 1);\n//     }\n//   }\n// }\n\n/* TODO: update to work with endpoint groups */\n// function disableMaintenanceMode() {\n//   var table = document.getElementById(\"pbxs\");\n//\n//   r = 1;\n//   while (row = table.rows[r++]) {\n//     checkbox = row.cells[0].getElementsByClassName('checkthis');\n//     if (checkbox[0].checked) {\n//       updateEndpoint(row, 'maintmode', 0);\n//     }\n//   }\n// }\n\n$(document).ready(function() {\n  var reloading_overlay = $('#reloading_overlay');\n\n  /* query param actions */\n  if (getQueryString('action') === 'add') {\n    $('#add').modal('show');\n  }\n\n  /* kam reload button listener */\n  $('#reload_kam').click(function() {\n    reloading_overlay.removeClass('hidden');\n\n    $.ajax({\n      type: \"POST\",\n      url: API_BASE_URL + \"reload/kamailio\",\n      dataType: \"json\",\n      global: false,\n      success: function(response, text_status, xhr) {\n        reloadKamRequired(false);\n        reloading_overlay.addClass('hidden');\n        showNotification(\"Kamailio was reloaded\");\n      },\n      error: function(xhr, text_status, error_msg) {\n        error_msg = JSON.parse(xhr.responseText)[\"msg\"];\n        reloading_overlay.addClass('hidden');\n        showNotification(\"Kamailio was NOT reloaded: \" + error_msg, true);\n      }\n    });\n  });\n\n  /* dsiprouter reload button listener */\n  $('#reload_dsip').click(function() {\n    var url = API_BASE_URL + \"reload/dsiprouter\";\n    reloading_overlay.removeClass('hidden');\n\n    $.ajax({\n      type: \"POST\",\n      url: url,\n      dataType: \"json\",\n      global: false,\n      success: function(response, text_status, xhr) {\n        // we started a reload in the background\n        showNotification(response.msg, false, 3000);\n        // poll the server until up or timeout\n        runUntilTimeout(\n          async function() {\n            try {\n              var response = await fetch(\n                url,\n                {\n                  method: \"GET\",\n                  cache: \"no-cache\",\n                  signal: AbortSignal.timeout(1500),\n                }\n              );\n              if (response.status === 200) {\n                var json = await response.json();\n                return json.data[0] === true;\n              }\n              return false;\n            }\n            catch {\n              return false;\n            }\n          },\n          60000,\n          2000,\n          function() {\n            showNotification(\"dSIPRouter was reloaded, reloading current session..\", false, 5000);\n            // refresh the page and bypass the cache\n            setTimeout(function() {\n              window.location.reload(true);\n            }, 3000)\n          },\n          function() {\n            reloading_overlay.addClass('hidden');\n            showNotification(\"dSIPRouter reload timed out. Check the logs for more information.\", true);\n          }\n        )\n      },\n      error: function(xhr, text_status, error_msg) {\n        error_msg = JSON.parse(xhr.responseText)[\"msg\"];\n        reloading_overlay.addClass('hidden');\n        showNotification(\"dSIPRouter reload failed to start: \" + error_msg, true);\n      }\n    });\n  });\n\n  /* clicks on \"reload\" button go to the drowdon instead */\n  $('#reload').click(function(ev){\n    ev.stopPropagation();\n    $('#reload-split').trigger('click');\n  });\n\n  /* listener for authtype radio buttons */\n  $('.authoptions.radio').get().forEach(function(elem) {\n    elem.addEventListener('click', function(e) {\n      var target_radio = $(e.target);\n      /* keep descending down DOM tree until input hit */\n      target_radio = descendingSearch($(e.target), function(node) {\n        return node.get(0).nodeName.toLowerCase() === \"input\"\n      });\n      if (target_radio === null) {\n        return false;\n      }\n      var auth_radios = $(e.currentTarget).find('input[type=\"radio\"]');\n      var modal_body = $(this).closest('.modal-body');\n      var hide_show_ids = [];\n      $.each(auth_radios, function() {\n        hide_show_ids.push('#' + $(this).data('toggle'));\n      });\n      var hide_show_divs = modal_body.find(hide_show_ids.join(', '));\n\n      if (target_radio.is(\":checked\") || target_radio.prop(\"checked\")) {\n        /* enable ip_addr on ip auth in #edit modal only */\n        if ($(this).closest('div.modal').attr('id').toLowerCase().indexOf('edit') > -1) {\n          if (target_radio.data('toggle') === \"ip_enabled\") {\n            toggleElemDisabled(modal_body.find('input.ip_addr'), false);\n          }\n          else {\n            toggleElemDisabled(modal_body.find('input.ip_addr'), true);\n          }\n        }\n\n        /* change value of authtype inputs */\n        modal_body.find('.authtype').val(target_radio.data('toggle').split('_')[0]);\n\n        /* show correct div's */\n        $.each(hide_show_divs, function(i, elem) {\n          if (target_radio.data('toggle') === $(elem).attr('name')) {\n            $(elem).removeClass(\"hidden\");\n          }\n          else {\n            $(elem).addClass(\"hidden\");\n          }\n        });\n      }\n      else {\n        /* change value of authtype inputs */\n        modal_body.find('.authtype').val('');\n\n        /* show correct div's */\n        $.each(hide_show_divs, function(i, elem) {\n          if (target_radio.data('toggle') === $(elem).attr('name')) {\n            $(elem).addClass(\"hidden\");\n          }\n          else {\n            $(elem).removeClass(\"hidden\");\n          }\n        });\n      }\n      /* trickle down DOM tree (capture event) */\n    }, true);\n  });\n\n  /* remove non-printable ascii chars on paste */\n  $('form input[type!=\"hidden\"]').on(\"paste\", function() {\n    $(this).val(this.value.replace(/[^\\x20-\\x7E]+/g, ''))\n  });\n\n  /* make sure autofocus is honored on loaded modals */\n  $('.modal').on('shown.bs.modal', function() {\n    $(this).find('[autofocus]').focus();\n  });\n\n  /* enable bootstrap tooltips */\n  $('[data-toggle=\"tooltip\"]').tooltip();\n});\n\n/* handle multiple modal stacking */\n$(window).on('show.bs.modal', function(e) {\n  modal = $(e.target);\n  zIndexTop = Math.max.apply(null, $('.modal').map(function() {\n    var z = parseInt($(this).css('z-index'));\n    return isNaN(z, 10) ? 0 : z;\n  }));\n  modal.css('z-index', zIndexTop + 10);\n  modal.addClass('modal-open');\n});\n$(window).on('hide.bs.modal', function(e) {\n  modal = $(e.target);\n  modal.css('z-index', '1050');\n});\n"
  },
  {
    "path": "gui/static/js/msteams.js",
    "content": ";(function(window, document) {\n  'use strict';\n\n  /* Update the display with the status of the tests */\n  function updateConnectivtyStatus(msg) {\n    var hostcheck_obj = $(\"#hostname_check\");\n    var tlscheck_obj = $(\"#tls_check\");\n    var tlscheckrow_obj = $(\"#tls_check_row\");\n    var optioncheck_obj = $(\"#option_check\");\n    var tlscheck_msg = \"\";\n\n    hostcheck_obj.removeClass();\n    tlscheck_obj.removeClass();\n    optioncheck_obj.removeClass();\n\n    //Hostname Check\n    if (msg.hostname_check == true) {\n      hostcheck_obj.addClass(\"glyphicon glyphicon-ok\");\n      hostcheck_obj.css(\"color\", \"green\");\n    }\n    else {\n      hostcheck_obj.addClass(\"glyphicon glyphicon-remove\");\n      hostcheck_obj.css(\"color\", \"red\");\n    }\n\n    if (msg.tls_check.tls_cert_valid == true) {\n      tlscheck_obj.addClass(\"glyphicon glyphicon-ok\");\n      tlscheck_obj.css(\"color\", \"green\");\n    }\n    else {\n      tlscheck_obj.addClass(\"glyphicon glyphicon-remove\");\n      tlscheck_obj.css(\"color\", \"red\");\n      if (msg.tls_check.tls_error.length > 0) {\n        tlscheck_msg = msg.tls_check.tls_error;\n      }\n      else {\n        tlscheck_msg = \"Cert commonname doesn't match the domain:\" + JSON.stringify(msg.tls_check.tls_cert_details);\n      }\n\n      tlscheckrow_obj.tooltip({\n        'title': tlscheck_msg,\n        'placement': 'right',\n        'trigger': 'manual',\n        'tooltipClass': 'tooltipclass'\n      });\n      tlscheckrow_obj.tooltip('show');\n    }\n\n    //Option Check\n    if (msg.option_check == true) {\n      optioncheck_obj.addClass(\"glyphicon glyphicon-ok\");\n      optioncheck_obj.css(\"color\", \"green\");\n    }\n    else {\n      optioncheck_obj.addClass(\"glyphicon glyphicon-remove\");\n      optioncheck_obj.css(\"color\", \"red\");\n    }\n  }\n\n  /* Set the width of the sidebar to 250px (show it) */\n  function openNav() {\n    document.getElementById(\"configurationPanel\").style.width = \"auto\";\n  }\n\n  /* Set the width of the sidebar to 0 (hide it) */\n  function closeNav() {\n    document.getElementById(\"configurationPanel\").style.width = \"0\";\n  }\n\n  function runTests() {\n    var testconn_obj = $('#testConnectivity');\n    var domain = testconn_obj.val();\n\n    //Disable Test Connectivity Button\n    testconn_obj.prop('disabled', true);\n\n    //Run Test using API\n    $.ajax({\n      type: \"GET\",\n      url: \"/api/v1/domains/msteams/test/\" + domain,\n      dataType: \"json\",\n      contentType: \"application/json; charset=utf-8\",\n      success: function(response, text_status, xhr) {\n        // Update the display\n        updateConnectivtyStatus(response.data[0])\n      }\n    });\n\n    //Enable Test Connectivity Button\n    testconn_obj.prop('disabled', false);\n  }\n\n  /* once DOM is ready init variables and listeners */\n  $(document).ready(function() {\n    runTests();\n    $('#testConnectivity').click(function() {\n      runTests();\n    });\n  });\n\n})(window, document);\n"
  },
  {
    "path": "gui/static/js/npm.js",
    "content": "// This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.\nrequire('../../js/transition.js');\nrequire('../../js/alert.js');\nrequire('../../js/button.js');\nrequire('../../js/carousel.js');\nrequire('../../js/collapse.js');\nrequire('../../js/dropdown.js');\nrequire('../../js/modal.js');\nrequire('../../js/tooltip.js');\nrequire('../../js/popover.js');\nrequire('../../js/scrollspy.js');\nrequire('../../js/tab.js');\nrequire('../../js/affix.js');"
  },
  {
    "path": "gui/static/js/outboundroutes.js",
    "content": ";(function(window, document) {\n  'use strict';\n\n  $(document).ready(function () {\n    /* data tables init */\n    $('#outboundmapping').DataTable({\n      \"columnDefs\": [\n        {\"orderable\": true, \"targets\": [1, 3, 4, 5, 6, 7, 8, 9]},\n        {\"orderable\": false, \"targets\": [0, 2, 10, 11]},\n      ],\n      \"order\": [[1, 'asc']]\n    });\n\n    /* validator init */\n    $('#addOutboundRoutes').validator({\n      custom: {\n        tocheck: function ($el) {\n          return $el.length > 0 && $('prefix').length > 0;\n        }\n      },\n      errors: {\n        tocheck: \"You must enter a To Prefix as well.  Entering just a From prefix is not supported\"\n      }\n    });\n\n    /* listeners */\n    $('#outboundmapping').on('click', '#open-Update', function () {\n      var row_index = $(this).parent().parent().parent().index() + 1;\n      var c = document.getElementById('outboundmapping');\n\n      var ruleid = $(c).find('tr:eq(' + row_index + ') > td.ruleid').text();\n      var groupid = $(c).find('tr:eq(' + row_index + ') > td.groupid').text();\n      var prefix = $(c).find('tr:eq(' + row_index + ') > td.prefix').text();\n      var from_prefix = $(c).find('tr:eq(' + row_index + ') > td.from_prefix').text();\n      var timerec = $(c).find('tr:eq(' + row_index + ') > td.timerec').text();\n      var priority = $(c).find('tr:eq(' + row_index + ') > td.priority').text();\n      var routeid = $(c).find('tr:eq(' + row_index + ') > td.routeid').text();\n      var gwgroupid = $(c).find('tr:eq(' + row_index + ') > td.gwgroupid').text();\n      var name = $(c).find('tr:eq(' + row_index + ') > td.description').text();\n\n      /** Clear out the modal */\n      var modal_body = $('#edit .modal-body');\n      modal_body.find(\"input.ruleid\").val('');\n      modal_body.find(\"input.groupid\").val('');\n      modal_body.find(\"input.prefix\").val('');\n      modal_body.find(\"input.from_prefix\").val('');\n      modal_body.find(\"input.timerec\").val('');\n      modal_body.find(\"input.priority\").val('');\n      modal_body.find(\"input.gwgroupid\").val('');\n      modal_body.find(\"input.name\").val('');\n\n      /* update modal fields */\n      modal_body.find(\"input.ruleid\").val(ruleid);\n      modal_body.find(\"input.groupid\").val(groupid);\n      modal_body.find(\"input.prefix\").val(prefix);\n      modal_body.find(\"input.from_prefix\").val(from_prefix);\n      modal_body.find(\"input.timerec\").val(timerec);\n      modal_body.find(\"input.priority\").val(priority);\n      modal_body.find(\"input.name\").val(name);\n\n      /* update options selected */\n      modal_body.find(\"select\").val('');\n      modal_body.find(\"select.gwgroupid\").val(gwgroupid);\n      modal_body.find(\"select.routeid\").val(routeid);\n    });\n\n    $('#outboundmapping').on('click','#open-Delete', function () {\n      var row_index = $(this).parent().parent().parent().index() + 1;\n      var c = document.getElementById('outboundmapping');\n      var ruleid = $(c).find('tr:eq(' + row_index + ') td:eq(1)').text();\n\n      /* update modal fields */\n      var modal_body = $('#delete .modal-body');\n      modal_body.find(\".ruleid\").val(ruleid);\n    });\n  });\n\n})(window, document);\n\n"
  },
  {
    "path": "gui/static/js/stirshaken.js",
    "content": ";(function (window, document) {\n  'use strict';\n\n  $(document).ready(function() {\n    var toggle = $('#toggleStirShaken');\n\n    function updateToggle() {\n      if (toggle.is(\":checked\") || toggle.prop(\"checked\")) {\n        $('#stirShakenOptions').removeClass(\"hidden\");\n        $(this).val(\"1\");\n        $(this).bootstrapToggle('on');\n      }\n      else {\n        $('#stirShakenOptions').addClass(\"hidden\");\n        $(this).val(\"0\");\n        $(this).bootstrapToggle('off');\n      }\n    }\n\n    /* update toggle on page load */\n    updateToggle();\n    /* listener for toggle changes */\n    toggle.change(updateToggle);\n  });\n\n})(window, document);\n"
  },
  {
    "path": "gui/static/js/teleblock.js",
    "content": ";(function (window, document) {\n  'use strict';\n\n  $(document).ready(function() {\n    /* listener for teleblock toggle */\n    $('#toggleTeleblock').change(function () {\n      if ($(this).is(\":checked\") || $(this).prop(\"checked\")) {\n        $('#teleblockOptions').removeClass(\"hidden\");\n        $(this).val(\"1\");\n        $(this).bootstrapToggle('on');\n      }\n      else {\n        $('#teleblockOptions').addClass(\"hidden\");\n        $(this).val(\"0\");\n        $(this).bootstrapToggle('off');\n      }\n    });\n  });\n\n})(window, document);\n"
  },
  {
    "path": "gui/static/js/transnexus.js",
    "content": ";(function (window, document) {\n  'use strict';\n\n  function updateToggle(toggle_node, settings_node) {\n    if (toggle_node.is(\":checked\") || toggle_node.prop(\"checked\")) {\n      settings_node.removeClass(\"hidden\");\n      toggle_node.val(\"1\");\n      toggle_node.bootstrapToggle('on');\n    }\n    else {\n      settings_node.addClass(\"hidden\");\n      toggle_node.val(\"0\");\n      toggle_node.bootstrapToggle('off');\n    }\n  }\n\n  $(document).ready(function() {\n    var auth_toggle = $('#toggle_auth_settings');\n    var auth_settings = $('#authservice_settings');\n    var verify_toggle = $('#toggle_verify_settings');\n    var verify_settings = $('#verifyservice_settings');\n\n    /* update toggle on page load */\n    updateToggle(auth_toggle, auth_settings);\n    updateToggle(verify_toggle, verify_settings);\n    /* listener for toggle changes */\n    auth_toggle.change(function() {\n      updateToggle(auth_toggle, auth_settings);\n    });\n    verify_toggle.change(function() {\n      updateToggle(verify_toggle, verify_settings);\n    });\n  });\n\n})(window, document);\n"
  },
  {
    "path": "gui/static/js/upgrade.js",
    "content": ";(function(window, document) {\n  'use strict';\n\n  // throw an error if required functions not defined\n  if (typeof reloadDsipRequired === \"undefined\") {\n    throw new Error(\"reloadDsipRequired() is required and is not defined\");\n  }\n\n  var upgrade_form = $('#upgrade_form');\n  var upgrade_output_row = $('#upgrade_output_row');\n  var upgrade_output = $('#upgrade_output');\n\n  function displayUpgradeLog() {\n    upgrade_form.hide();\n    upgrade_output.html(\"\");\n    upgrade_output_row.show();\n  }\n\n  function streamLog() {\n    var source = new EventSource(\"/upgrade/log\");\n    source.onmessage = function(event) {\n      upgrade_output.append(event.data);\n    };\n    source.onerror = function(event) {\n      source.close();\n    };\n  }\n\n  function dumpLog() {\n    $.ajax({\n      type: \"GET\",\n      url: \"/upgrade/log\",\n      success: function(response, textStatus, jqXHR) {\n        upgrade_output.append(response);\n      }\n    });\n  }\n\n  function monitorUpgrade() {\n    var source = new EventSource(\"/upgrade/status\");\n    source.onmessage = function(event) {\n      if (event.data === '0') {\n        reloadDsipRequired(true);\n      }\n    };\n    source.onerror = function(event) {\n      source.close();\n    };\n  }\n\n  $(document).ready(function() {\n    $('#btnShowLog').click(function() {\n      displayUpgradeLog();\n      dumpLog();\n    });\n\n    upgrade_form.submit(function(e) {\n      e.preventDefault();\n\n      var formData = $(this).serialize();\n\n      $.ajax({\n        type: \"POST\",\n        url: \"/upgrade/start\",\n        async: true,\n        data: formData,\n        success: function(response, text_status, xhr) {\n          showNotification(\"Upgrade started. See log below for more details..\", 5000);\n          displayUpgradeLog();\n          streamLog();\n          monitorUpgrade();\n        },\n        error: function(xhr, text_status, error_msg) {\n          showNotification(\"Could not start upgrade: \" + error_msg, true);\n        }\n      });\n    });\n  });\n\n})(window, document);\n"
  },
  {
    "path": "gui/static/js/util.js",
    "content": ";(function(window, document) {\n  'use strict';\n\n  /**\n   * Get the value of a querystring\n   * @param  {String} field     The field to get the value of\n   * @param  {String} url       The URL to get the value from (optional)\n   * @return {String}           The field value\n   */\n  window.getQueryString = function(field, url) {\n    var href = url ? url : window.location.href;\n    var reg = new RegExp('[?&]' + field + '=([^&#]*)', 'i');\n    var string = reg.exec(href);\n    return string ? string[1] : null;\n  };\n\n  /**\n   * Disable / Re-enable a form submittable element<br>\n   * Use this instead of the HTML5 disabled prop\n   * @param {String|jQuery|Object} selector   The selector for element to toggle\n   * @param {Boolean} disable                 Whether to disable or re-enable\n   * @param {Boolean} child                   Whether to change cursor on child instead\n   */\n  window.toggleElemDisabled = function(selector, disable, child) {\n    var select_elem = null;\n\n    if (typeof selector === 'string' || selector instanceof String) {\n      select_elem = $(selector);\n    }\n    else if (selector instanceof jQuery) {\n      select_elem = selector;\n    }\n    else {\n      console.err(\"toggleElemDisabled(): invalid selector argument\");\n      return;\n    }\n\n    /* by default change cursor on parent not child */\n    child = child || false;\n\n    if (disable) {\n      if (!child) {\n        select_elem.parent().css({'cursor': 'not-allowed'});\n      }\n      else {\n        select_elem.css({'cursor': 'not-allowed'});\n      }\n      select_elem.css({\n        'background-color': '#EEEEEE',\n        'opacity': '0.7',\n        'pointer-events': 'none'\n      });\n      select_elem.prop('readonly', true);\n      select_elem.prop('tabindex', -1);\n      select_elem.prop('disabled', true);\n    }\n    else {\n      if (!child) {\n        select_elem.parent().removeAttr('style');\n      }\n      select_elem.removeAttr('style');\n      select_elem.prop('readonly', false);\n      select_elem.prop('tabindex', 0);\n      select_elem.prop('disabled', false);\n    }\n  };\n\n  /**\n   * Recursively search DOM tree until test is true<br>\n   * Starts at and includes selected node, tests each descendant<br>\n   * Note that test callback applies to jQuery objects throughout\n   * @param {String|jQuery|Object} selector   The selector for start node\n   * @param {Function} test                   Test to apply to each node\n   * @return {jQuery|null}                    Returns found node or null\n   */\n  window.descendingSearch = function(selector, test) {\n    var select_node = null;\n    if (typeof selector === 'string' || selector instanceof String) {\n      select_node = $(selector);\n    }\n    else if (selector instanceof jQuery) {\n      select_node = selector;\n    }\n    else {\n      return null;\n    }\n\n    var num_nodes = select_node.length || 0;\n    if (num_nodes > 1) {\n      for (var i = 0; i < num_nodes; i++) {\n        if (test(select_node[i])) {\n          return select_node[i];\n        }\n      }\n    }\n    else {\n      if (test(select_node)) {\n        return select_node;\n      }\n    }\n\n    var node_list = select_node.children();\n    if (node_list.length <= 0) {\n      return null;\n    }\n\n    descendingSearch(node_list, test)\n  };\n\n  /**\n   * Validate form fields in a selected node<br>\n   * The optional test function is passed a an array of fields found<br>\n   * Note that in this project our forms are usually the 1st child in a modal body<br>\n   * If provided the test function should return an object of this structure:<br>\n   * <pre>\n   *    {\n   *      result:     boolean                 - whether the test passed or failed\n   *      err_node:   element|jQuery|Object   - the node that caused the failure\n   *      err_msg:    String                  - the error message to display\n   *    }\n   * </pre>\n   * @param {String|jQuery|Object} selector   Selector for a node with fields\n   * @param {Function} test                   Custom validation test to run\n   * @returns {Boolean}                       Whether the validation passed\n   * @throws {Error}                          If selector is invalid\n   */\n  window.validateFields = function(selector, test) {\n    var select_node = null;\n    if (typeof selector === 'string' || selector instanceof String) {\n      select_node = $(selector);\n    }\n    else if (selector instanceof jQuery) {\n      select_node = selector;\n    }\n    else if (typeof selector === \"object\" || selector instanceof Object) {\n      select_node = $(selector);\n    }\n    else {\n      throw new Error(\"validateFields(): invalid selector argument\");\n    }\n\n    /* grab the fields as a jquery object */\n    var field_elem_list = select_node.find('input,textarea,select,output').filter(':not(:hidden)').get();\n\n    /* check the builtin form validation */\n    for (var i = 0; i < field_elem_list.length; i++) {\n      if (!field_elem_list[i].reportValidity()) {\n        return false;\n      }\n    }\n\n    /* if supplied, run the custom test function */\n    if (typeof test === 'function') {\n      // convert the array of DOM elements into a Map of \"name\" => jQuery(elem) key pairs\n      var fields = new Map(field_elem_list.map(function(elem) {\n        return [elem.getAttribute('name'), $(elem)];\n      }));\n\n      var resp = test(fields);\n      if (resp.result === false) {\n        var err_elem = null;\n        if (resp.err_node instanceof jQuery) {\n          err_elem = resp.err_node.get(0);\n        }\n        else {\n          err_elem = resp.err_node;\n        }\n\n        err_elem.setCustomValidity(resp.err_msg);\n        err_elem.reportValidity();\n        setTimeout(function() {\n          err_elem.setCustomValidity('');\n        }, 2500);\n        return false;\n      }\n    }\n\n    return true;\n  };\n\n  /**\n   * Checks if selected element has a particular event listener<br>\n   * Works for on<event> DOM events, and dynamic events added using jquery<br>\n   * Events added dynamically using addEventListener will not be found\n   * @param {String|jQuery|Object} selector   Selector for a node with fields\n   * @param {String} event                    Event listener type to check for\n   * @returns {Boolean}\n   * @throws {Error}                          If selector is invalid\n   */\n  function hasEventListener(selector, event) {\n    var element = undefined;\n    if (typeof selector === 'string' || selector instanceof String) {\n      element = $(selector).get(0);\n    }\n    else if (selector instanceof jQuery) {\n      element = selector.get(0);\n    }\n    else if (typeof selector === \"object\" || selector instanceof Object) {\n      element = $(selector).get(0);\n    }\n    if (element === undefined) {\n      throw new Error(\"validateFields(): invalid selector argument\");\n    }\n\n    if (element.hasAttribute('on' + event)) {\n      return true;\n    }\n    else if ($._data(element, \"events\").hasOwnProperty(event)) {\n      return true;\n    }\n    return false;\n  }\n\n  /**\n   * Show notification in top notification bar\n   * @param {String} msg      message to display\n   * @param {Boolean} error   whether the notification is an error\n   */\n  window.showNotification = function(msg, error = false, duration = 10000) {\n    var top_bar = $('.top-bar');\n    var msg_bar = $('.message-bar');\n    var visible_modals = $('.modal').filter(':not(:hidden)');\n\n    // hide modals if shown\n    visible_modals.modal('hide');\n\n    // stop the animation if already running\n    top_bar.stop(true, true);\n\n    // change the notification accordingly\n    if (error === true) {\n      msg_bar.removeClass(\"alert-success\");\n      msg_bar.addClass(\"alert alert-danger\");\n      msg_bar.html(\"<strong>Failed!</strong> \" + msg);\n    }\n    else {\n      msg_bar.removeClass(\"alert-danger\");\n      msg_bar.addClass(\"alert alert-success\");\n      msg_bar.html(\"<strong>Success!</strong> \" + msg);\n    }\n\n    // start the animation showing the notification\n    top_bar.show();\n    top_bar.slideUp(duration, function() {\n      top_bar.hide();\n    });\n  };\n\n  /**\n   * Update reload kamailio button to indicate if reload is required\n   * @param {Boolean} required    whether a reload is required\n   */\n  window.reloadKamRequired = function(required = true) {\n    var reload_btn = $('#reload');\n    var split_btn = $('#reload-split');\n    var kamailio_btn = $('#reload_kam');\n\n    if (required) {\n      reload_btn.removeClass('btn-primary');\n      split_btn.removeClass('btn-primary');\n      kamailio_btn.removeClass('btn-secondary');\n      reload_btn.addClass('btn-warning');\n      split_btn.addClass('btn-warning');\n      kamailio_btn.addClass('btn-warning');\n    }\n    else {\n      reload_btn.removeClass('btn-warning');\n      split_btn.removeClass('btn-warning');\n      kamailio_btn.removeClass('btn-warning');\n      reload_btn.addClass('btn-primary');\n      split_btn.addClass('btn-primary');\n      kamailio_btn.addClass('btn-secondary');\n    }\n  };\n\n  /**\n   * Update reload dsiprouter button to indicate if reload is required\n   * @param {Boolean} required    whether a reload is required\n   */\n  window.reloadDsipRequired = function(required = true) {\n    var reload_btn = $('#reload');\n    var split_btn = $('#reload-split');\n    var dsiprouter_btn = $('#reload_dsip');\n\n    if (required) {\n      reload_btn.removeClass('btn-primary');\n      split_btn.removeClass('btn-primary');\n      dsiprouter_btn.removeClass('btn-secondary');\n      reload_btn.addClass('btn-warning');\n      split_btn.addClass('btn-warning');\n      dsiprouter_btn.addClass('btn-warning');\n    }\n    else {\n      reload_btn.removeClass('btn-warning');\n      split_btn.removeClass('btn-warning');\n      dsiprouter_btn.removeClass('btn-warning');\n      reload_btn.addClass('btn-primary');\n      split_btn.addClass('btn-primary');\n      dsiprouter_btn.addClass('btn-secondary');\n    }\n  };\n\n  /**\n   * Run a function periodically until successful or timed out\n   * @param {Function}  run_fn      Function to run (must return true or false in a Promise)\n   * @param {Number}    timeout     Timeout (ms) until failure\n   * @param {Number}    interval    Period (ms) between attempting run_fn again\n   * @param {Function}  success_fn  Function to run on successful completion\n   * @param {Function}  timeout_fn  Function to run on timeout failure\n   */\n  window.runUntilTimeout = function(\n    run_fn, timeout, interval = 1000,\n    success_fn = null, timeout_fn = null\n  ) {\n    var timeout_timer = setTimeout(function() {\n      // ran unsuccessfully until timeout\n      clearInterval(interval_timer);\n      if (timeout_fn !== null) {\n        timeout_fn();\n      }\n    }, timeout);\n\n    var interval_timer = setInterval(function() {\n      run_fn().then((result) => {\n        if (result === true) {\n          // ran successfully without timeout\n          clearInterval(interval_timer);\n          clearTimeout(timeout_timer);\n          if (success_fn !== null) {\n            success_fn();\n          }\n        }\n      });\n    }, interval);\n  }\n\n  /**\n   * Queued delay of a callback\n   * @param fn  The callback function\n   * @param ms  The timeout in milliseconds\n   * @returns {(function(...[*]): void)|*}\n   */\n  window.delayedCallback = function(fn, ms) {\n    let timer = 0\n    return function(...args) {\n      clearTimeout(timer)\n      timer = setTimeout(fn.bind(this, ...args), ms || 0)\n    }\n  };\n\n})(window, document);\n"
  },
  {
    "path": "gui/static/js/validator.js",
    "content": "/*!\n * Validator v0.11.9 for Bootstrap 3, by @1000hz\n * Copyright 2017 Cina Saffary\n * Licensed under http://opensource.org/licenses/MIT\n *\n * https://github.com/1000hz/bootstrap-validator\n */\n\n+function ($) {\n  'use strict';\n\n  // VALIDATOR CLASS DEFINITION\n  // ==========================\n\n  function getValue($el) {\n    return $el.is('[type=\"checkbox\"]') ? $el.prop('checked')                                     :\n           $el.is('[type=\"radio\"]')    ? !!$('[name=\"' + $el.attr('name') + '\"]:checked').length :\n           $el.is('select[multiple]')  ? ($el.val() || []).length                                :\n                                         $el.val()\n  }\n\n  var Validator = function (element, options) {\n    this.options    = options\n    this.validators = $.extend({}, Validator.VALIDATORS, options.custom)\n    this.$element   = $(element)\n    this.$btn       = $('button[type=\"submit\"], input[type=\"submit\"]')\n                        .filter('[form=\"' + this.$element.attr('id') + '\"]')\n                        .add(this.$element.find('input[type=\"submit\"], button[type=\"submit\"]'))\n\n    this.update()\n\n    this.$element.on('input.bs.validator change.bs.validator focusout.bs.validator', $.proxy(this.onInput, this))\n    this.$element.on('submit.bs.validator', $.proxy(this.onSubmit, this))\n    this.$element.on('reset.bs.validator', $.proxy(this.reset, this))\n\n    this.$element.find('[data-match]').each(function () {\n      var $this  = $(this)\n      var target = $this.attr('data-match')\n\n      $(target).on('input.bs.validator', function (e) {\n        getValue($this) && $this.trigger('input.bs.validator')\n      })\n    })\n\n    // run validators for fields with values, but don't clobber server-side errors\n    this.$inputs.filter(function () {\n      return getValue($(this)) && !$(this).closest('.has-error').length\n    }).trigger('focusout')\n\n    this.$element.attr('novalidate', true) // disable automatic native validation\n  }\n\n  Validator.VERSION = '0.11.9'\n\n  Validator.INPUT_SELECTOR = ':input:not([type=\"hidden\"], [type=\"submit\"], [type=\"reset\"], button)'\n\n  Validator.FOCUS_OFFSET = 20\n\n  Validator.DEFAULTS = {\n    delay: 500,\n    html: false,\n    disable: true,\n    focus: true,\n    custom: {},\n    errors: {\n      match: 'Does not match',\n      minlength: 'Not long enough'\n    },\n    feedback: {\n      success: 'glyphicon-ok',\n      error: 'glyphicon-remove'\n    }\n  }\n\n  Validator.VALIDATORS = {\n    'native': function ($el) {\n      var el = $el[0]\n      if (el.checkValidity) {\n        return !el.checkValidity() && !el.validity.valid && (el.validationMessage || \"error!\")\n      }\n    },\n    'match': function ($el) {\n      var target = $el.attr('data-match')\n      return $el.val() !== $(target).val() && Validator.DEFAULTS.errors.match\n    },\n    'minlength': function ($el) {\n      var minlength = $el.attr('data-minlength')\n      return $el.val().length < minlength && Validator.DEFAULTS.errors.minlength\n    }\n  }\n\n  Validator.prototype.update = function () {\n    var self = this\n\n    this.$inputs = this.$element.find(Validator.INPUT_SELECTOR)\n      .add(this.$element.find('[data-validate=\"true\"]'))\n      .not(this.$element.find('[data-validate=\"false\"]')\n        .each(function () { self.clearErrors($(this)) })\n      )\n\n    this.toggleSubmit()\n\n    return this\n  }\n\n  Validator.prototype.onInput = function (e) {\n    var self        = this\n    var $el         = $(e.target)\n    var deferErrors = e.type !== 'focusout'\n\n    if (!this.$inputs.is($el)) return\n\n    this.validateInput($el, deferErrors).done(function () {\n      self.toggleSubmit()\n    })\n  }\n\n  Validator.prototype.validateInput = function ($el, deferErrors) {\n    var value      = getValue($el)\n    var prevErrors = $el.data('bs.validator.errors')\n\n    if ($el.is('[type=\"radio\"]')) $el = this.$element.find('input[name=\"' + $el.attr('name') + '\"]')\n\n    var e = $.Event('validate.bs.validator', {relatedTarget: $el[0]})\n    this.$element.trigger(e)\n    if (e.isDefaultPrevented()) return\n\n    var self = this\n\n    return this.runValidators($el).done(function (errors) {\n      $el.data('bs.validator.errors', errors)\n\n      errors.length\n        ? deferErrors ? self.defer($el, self.showErrors) : self.showErrors($el)\n        : self.clearErrors($el)\n\n      if (!prevErrors || errors.toString() !== prevErrors.toString()) {\n        e = errors.length\n          ? $.Event('invalid.bs.validator', {relatedTarget: $el[0], detail: errors})\n          : $.Event('valid.bs.validator', {relatedTarget: $el[0], detail: prevErrors})\n\n        self.$element.trigger(e)\n      }\n\n      self.toggleSubmit()\n\n      self.$element.trigger($.Event('validated.bs.validator', {relatedTarget: $el[0]}))\n    })\n  }\n\n\n  Validator.prototype.runValidators = function ($el) {\n    var errors   = []\n    var deferred = $.Deferred()\n\n    $el.data('bs.validator.deferred') && $el.data('bs.validator.deferred').reject()\n    $el.data('bs.validator.deferred', deferred)\n\n    function getValidatorSpecificError(key) {\n      return $el.attr('data-' + key + '-error')\n    }\n\n    function getValidityStateError() {\n      var validity = $el[0].validity\n      return validity.typeMismatch    ? $el.attr('data-type-error')\n           : validity.patternMismatch ? $el.attr('data-pattern-error')\n           : validity.stepMismatch    ? $el.attr('data-step-error')\n           : validity.rangeOverflow   ? $el.attr('data-max-error')\n           : validity.rangeUnderflow  ? $el.attr('data-min-error')\n           : validity.valueMissing    ? $el.attr('data-required-error')\n           :                            null\n    }\n\n    function getGenericError() {\n      return $el.attr('data-error')\n    }\n\n    function getErrorMessage(key) {\n      return getValidatorSpecificError(key)\n          || getValidityStateError()\n          || getGenericError()\n    }\n\n    $.each(this.validators, $.proxy(function (key, validator) {\n      var error = null\n      if ((getValue($el) || $el.attr('required')) &&\n          ($el.attr('data-' + key) !== undefined || key == 'native') &&\n          (error = validator.call(this, $el))) {\n         error = getErrorMessage(key) || error\n        !~errors.indexOf(error) && errors.push(error)\n      }\n    }, this))\n\n    if (!errors.length && getValue($el) && $el.attr('data-remote')) {\n      this.defer($el, function () {\n        var data = {}\n        data[$el.attr('name')] = getValue($el)\n        $.get($el.attr('data-remote'), data)\n          .fail(function (jqXHR, textStatus, error) { errors.push(getErrorMessage('remote') || error) })\n          .always(function () { deferred.resolve(errors)})\n      })\n    } else deferred.resolve(errors)\n\n    return deferred.promise()\n  }\n\n  Validator.prototype.validate = function () {\n    var self = this\n\n    $.when(this.$inputs.map(function (el) {\n      return self.validateInput($(this), false)\n    })).then(function () {\n      self.toggleSubmit()\n      self.focusError()\n    })\n\n    return this\n  }\n\n  Validator.prototype.focusError = function () {\n    if (!this.options.focus) return\n\n    var $input = this.$element.find(\".has-error:first :input\")\n    if ($input.length === 0) return\n\n    $('html, body').animate({scrollTop: $input.offset().top - Validator.FOCUS_OFFSET}, 250)\n    $input.focus()\n  }\n\n  Validator.prototype.showErrors = function ($el) {\n    var method = this.options.html ? 'html' : 'text'\n    var errors = $el.data('bs.validator.errors')\n    var $group = $el.closest('.form-group')\n    var $block = $group.find('.help-block.with-errors')\n    var $feedback = $group.find('.form-control-feedback')\n\n    if (!errors.length) return\n\n    errors = $('<ul/>')\n      .addClass('list-unstyled')\n      .append($.map(errors, function (error) { return $('<li/>')[method](error) }))\n\n    $block.data('bs.validator.originalContent') === undefined && $block.data('bs.validator.originalContent', $block.html())\n    $block.empty().append(errors)\n    $group.addClass('has-error has-danger')\n\n    $group.hasClass('has-feedback')\n      && $feedback.removeClass(this.options.feedback.success)\n      && $feedback.addClass(this.options.feedback.error)\n      && $group.removeClass('has-success')\n  }\n\n  Validator.prototype.clearErrors = function ($el) {\n    var $group = $el.closest('.form-group')\n    var $block = $group.find('.help-block.with-errors')\n    var $feedback = $group.find('.form-control-feedback')\n\n    $block.html($block.data('bs.validator.originalContent'))\n    $group.removeClass('has-error has-danger has-success')\n\n    $group.hasClass('has-feedback')\n      && $feedback.removeClass(this.options.feedback.error)\n      && $feedback.removeClass(this.options.feedback.success)\n      && getValue($el)\n      && $feedback.addClass(this.options.feedback.success)\n      && $group.addClass('has-success')\n  }\n\n  Validator.prototype.hasErrors = function () {\n    function fieldErrors() {\n      return !!($(this).data('bs.validator.errors') || []).length\n    }\n\n    return !!this.$inputs.filter(fieldErrors).length\n  }\n\n  Validator.prototype.isIncomplete = function () {\n    function fieldIncomplete() {\n      var value = getValue($(this))\n      return !(typeof value == \"string\" ? $.trim(value) : value)\n    }\n\n    return !!this.$inputs.filter('[required]').filter(fieldIncomplete).length\n  }\n\n  Validator.prototype.onSubmit = function (e) {\n    this.validate()\n    if (this.isIncomplete() || this.hasErrors()) e.preventDefault()\n  }\n\n  Validator.prototype.toggleSubmit = function () {\n    if (!this.options.disable) return\n    this.$btn.toggleClass('disabled', this.isIncomplete() || this.hasErrors())\n  }\n\n  Validator.prototype.defer = function ($el, callback) {\n    callback = $.proxy(callback, this, $el)\n    if (!this.options.delay) return callback()\n    window.clearTimeout($el.data('bs.validator.timeout'))\n    $el.data('bs.validator.timeout', window.setTimeout(callback, this.options.delay))\n  }\n\n  Validator.prototype.reset = function () {\n    this.$element.find('.form-control-feedback')\n      .removeClass(this.options.feedback.error)\n      .removeClass(this.options.feedback.success)\n\n    this.$inputs\n      .removeData(['bs.validator.errors', 'bs.validator.deferred'])\n      .each(function () {\n        var $this = $(this)\n        var timeout = $this.data('bs.validator.timeout')\n        window.clearTimeout(timeout) && $this.removeData('bs.validator.timeout')\n      })\n\n    this.$element.find('.help-block.with-errors')\n      .each(function () {\n        var $this = $(this)\n        var originalContent = $this.data('bs.validator.originalContent')\n\n        $this\n          .removeData('bs.validator.originalContent')\n          .html(originalContent)\n      })\n\n    this.$btn.removeClass('disabled')\n\n    this.$element.find('.has-error, .has-danger, .has-success').removeClass('has-error has-danger has-success')\n\n    return this\n  }\n\n  Validator.prototype.destroy = function () {\n    this.reset()\n\n    this.$element\n      .removeAttr('novalidate')\n      .removeData('bs.validator')\n      .off('.bs.validator')\n\n    this.$inputs\n      .off('.bs.validator')\n\n    this.options    = null\n    this.validators = null\n    this.$element   = null\n    this.$btn       = null\n    this.$inputs    = null\n\n    return this\n  }\n\n  // VALIDATOR PLUGIN DEFINITION\n  // ===========================\n\n\n  function Plugin(option) {\n    return this.each(function () {\n      var $this   = $(this)\n      var options = $.extend({}, Validator.DEFAULTS, $this.data(), typeof option == 'object' && option)\n      var data    = $this.data('bs.validator')\n\n      if (!data && option == 'destroy') return\n      if (!data) $this.data('bs.validator', (data = new Validator(this, options)))\n      if (typeof option == 'string') data[option]()\n    })\n  }\n\n  var old = $.fn.validator\n\n  $.fn.validator             = Plugin\n  $.fn.validator.Constructor = Validator\n\n\n  // VALIDATOR NO CONFLICT\n  // =====================\n\n  $.fn.validator.noConflict = function () {\n    $.fn.validator = old\n    return this\n  }\n\n\n  // VALIDATOR DATA-API\n  // ==================\n\n  $(window).on('load', function () {\n    $('form[data-toggle=\"validator\"]').each(function () {\n      var $form = $(this)\n      Plugin.call($form, $form.data())\n    })\n  })\n\n}(jQuery);\n"
  },
  {
    "path": "gui/static/template/DID_example.csv",
    "content": "DID,EndpointGroupID,Name\n13134860409,52,Inbound1\n13134860410,53,Inbound2\n13134860411,34,Inbound3\n"
  },
  {
    "path": "gui/sysloginit.py",
    "content": "import logging\nimport settings\nfrom logging.handlers import SysLogHandler\nfrom copy import copy\n\ndef initSyslogLogger():\n    \"\"\"\n    Configure syslog loggers\n    :return:    syslog log handler\n    \"\"\"\n    # close any zombie handlers for root logger\n    for handler in copy(logging.getLogger().handlers):\n        logging.getLogger().removeHandler(handler)\n        handler.close()\n\n    # create our own custom formatter and handler for syslog\n    log_formatter = logging.Formatter('[%(asctime)s] [%(process)d->%(thread)d] [%(levelname)s]: %(message)s',\n                                      datefmt='%Y-%m-%d %H:%M:%S')\n    log_handler = logging.handlers.SysLogHandler(address=\"/dev/log\", facility=settings.DSIP_LOG_FACILITY)\n    log_handler.setLevel(settings.DSIP_LOG_LEVEL)\n    log_handler.setFormatter(log_formatter)\n\n    # set log handler for our dsiprouter app\n    logging.getLogger().setLevel(settings.DSIP_LOG_LEVEL)\n    logging.getLogger().addHandler(log_handler)\n\n    # redirect stderr and stdout to syslog\n    # if not settings.DEBUG:\n    #     sys.stderr = StreamLogger(level=logging.WARNING)\n    #     sys.stdout = StreamLogger(level=logging.DEBUG)\n\n    return log_handler\n\n# import this file to setup logging\n# syslog handler created globally\n# syslog_handler = initSyslogLogger()\n"
  },
  {
    "path": "gui/templates/backupandrestore.html",
    "content": "{% extends 'fullwidth_layout.html' %}\n\n{% block title %}Backup and Restore{% endblock %}\n\n{% block custom_css %}\n{% endblock %}\n\n{% block body %}\n\n  <div class=\"wrapper-horizontal edge-centered children-align-inherit content-header\">\n    <div>\n      <h3>Backup and Restore</h3>\n    </div>\n\n    {% if reloadstatus %}\n      <h3>Kamailio reload: {{ reloadstatus }}</h3>\n    {% endif %}\n  </div>\n\n  <div class=\"content-section\"> <!-- begin backup/restore section -->\n    <div class=\"col-md-12\">\n      <div id=\"endpoint-nav\" class=\"navbar\"> <!-- begin nav tabs -->\n        <ul class=\"nav nav-tabs\">\n          <li role=\"presentation\" class=\"auth-tab active\">\n            <a href=\"#backup\" name=\"backup-toggle\" data-toggle=\"tab\">Backup</a>\n          </li>\n          <li role=\"presentation\">\n            <a href=\"#restore\" name=\"restore-toggle\" data-toggle=\"tab\">Restore</a>\n          </li>\n        </ul>\n      </div><!-- end nav tabs -->\n\n      <div class=\"tab-content\"> <!-- begin tab content -->\n        <div id=\"backup\" class=\"tab-pane fade in active\" name=\"backup-toggle\">\n          <div class=\"form-group\">\n            <button id=\"start-Backup\" class=\"btn btn-success btn-md\">Download Backup</button>\n          </div>\n        </div>\n\n        <div id=\"restore\" class=\"tab-pane fade in\" name=\"restore-toggle\">\n          <form id=\"restore-backup\" action=\"#\">\n            <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n\n            <div class=\"form-group\" style=\"position:relative;\">\n              <a class='btn btn-primary' href='javascript:;'>\n                Choose Backup File...\n                <input type=\"file\"\n                       style='position:absolute;z-index:2;top:0;left:0;filter: alpha(opacity=0);-ms-filter:\"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)\";opacity:0;background-color:transparent;color:transparent;'\n                       name=\"file\" size=\"40\"\n                       onchange='$(\"#upload-file-info\").html($(this).val());$(\"#start-Restore\").show();'>\n              </a>\n              &nbsp;\n              <span class='label label-info' id=\"upload-file-info\"></span>\n            </div>\n\n            <div id=\"start-Restore\" style=\"display:none\">\n              <div class=\"form-group\">\n                <input type=\"submit\" class=\"btn btn-success btn-md\" value=\"Start Restore\"/>\n              </div>\n            </div>\n          </form>\n        </div>\n      </div> <!-- end tab content -->\n    </div>\n  </div> <!-- end backup/restore section -->\n\n{% endblock %}\n\n{% block custom_js %}\n  {{ script_tag('backupandrestore') }}\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/carriergroups.html",
    "content": "{% extends 'table_layout.html' %}\n\n{% block title %}Carrier Groups{% endblock %}\n\n{% block custom_css %}\n  {{ link_tag('carriergroups') }}\n{% endblock %}\n\n{% block table_headers %}\n  <div>\n    <h3>List of Carrier Groups</h3>\n  </div>\n\n  <div class=\"tableAddButton\">\n    <button id='open-CarrierGroupAdd' class='btn btn-success btn-md' data-title=\"Add\" data-toggle=\"modal\"\n            data-target=\"#add-group\"> Add\n    </button>\n  </div>\n{% endblock %}\n\n\n{% block table %}\n  <!-- carrier-groups table -->\n  <table id=\"carrier-groups\" class=\"table table-striped table-centered\">\n    <thead>\n    <tr class='element-row'>\n      <th><input type=\"checkbox\" class=\"checkall\"/></th>\n      <th data-field=\"gwgroup\">ID</th>\n      <th data-field=\"name\">Name</th>\n      <th data-field=\"gwlist\">Carriers</th>\n      <th class=\"hidden\"></th>\n      <th class=\"hidden\"></th>\n      <th class=\"hidden\"></th>\n      <th class=\"hidden\"></th>\n      <th class=\"hidden\"></th>\n      <th class=\"hidden\"></th>\n      <th class=\"hidden\"></th>\n      <th></th>\n      <th></th>\n    </tr>\n    </thead>\n    <tbody>\n    {% for row in rows %}\n      <tr class='element-row' data-gwgroup=\"{{ row.id }}\">\n        <td><input type=\"checkbox\" class=\"checkthis\" value=\"1\"/></td>\n        <td class='gwgroup'>{{ row.id|noneFilter() }}</td>\n        <td class='name'>{{ row.description|attrFilter('name')|noneFilter() }}</td>\n        <td class='gwlist'>{{ row.gwlist|noneFilter() }}</td>\n        <td class='authtype hide'>{{ row.r_username|noneFilter() }}</td>\n        <td class='r_username hide'>{{ row.r_username|noneFilter() }}</td>\n        <td class='auth_password hide'>{{ row.auth_password|noneFilter() }}</td>\n        <td class='auth_domain hide'>{{ row.r_domain|noneFilter() }}</td>\n        <td class='auth_username hide'>{{ row.auth_username|noneFilter() }}</td>\n        <td class='auth_proxy hide'>{{ row.auth_proxy|noneFilter() }}</td>\n        <td class='lb_enabled hide'>{{ row.lb_enabled|noneFilter() }}</td>\n        <td>\n          <p data-placement=\"top\" data-toggle=\"tooltip\" title=\"Edit\">\n            <button id=\"open-Update\" class=\"open-Update btn btn-primary btn-xs\" data-title=\"Edit\" data-toggle=\"modal\"\n                    data-target=\"#edit-group\"><span class=\"glyphicon glyphicon-pencil\"></span></button>\n          </p>\n        </td>\n        <td>\n          <p data-placement=\"top\" data-toggle=\"tooltip\" title=\"Delete\">\n            <button id=\"open-Delete\" class=\"open-Delete btn btn-danger btn-xs\" data-title=\"Delete\" data-toggle=\"modal\"\n                    data-target=\"#delete-group\"><span class=\"glyphicon glyphicon-trash\"></span></button>\n          </p>\n        </td>\n      </tr>\n    {% endfor %}\n    </tbody>\n  </table>\n{% endblock %}\n\n\n{% block edit_modal %}\n  <!-- edit-endpoint modal -->\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Edit Your Carrier Detail</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form class=\"gwform\" action=\"/carriers\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n      <input type=\"hidden\" class=\"gwid\" name=\"gwid\">\n      <input type=\"hidden\" class=\"gwgroup\" name=\"gwgroup\" value=\"{{ gwgroup|noneFilter() }}\">\n\n      <div class=\"form-group\">\n        <input class=\"form-control name\" type=\"text\" name=\"name\"\n               placeholder=\"Friendly Name(Optional)\" autofocus=\"autofocus\">\n      </div>\n      <div class=\"form-group\">\n\n        <input class=\"form-control ip_addr\" type=\"text\" name=\"ip_addr\"\n               placeholder=\"Hostname/IP\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"form-control strip\" type=\"text\" name=\"strip\"\n               placeholder=\"# of characters to strip from RURI (outbound calls)\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"form-control prefix\" type=\"text\" name=\"prefix\"\n               placeholder=\"The characters to prefix to a RURI (outbound calls)\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"form-control rweight\" type=\"text\" name=\"rweight\"\n               placeholder=\"The amount of traffic allocated to the carrier endpoint (outbound calls)\">\n      </div>\n\n      <div class=\"modal-footer \">\n        <button type=\"submit\" id=\"updateButton\" class=\"btn btn-warning btn-lg\" style=\"width: 100%;\"><span\n            class=\"glyphicon glyphicon-ok-sign\"></span> Update\n        </button>\n      </div>\n    </form>\n  </div>\n{% endblock %}\n\n\n{% block add_modal %}\n  <!-- add-endpoint modal -->\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Add New Carrier Detail</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form class=\"gwform\" action=\"/carriers\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n      <input type=\"hidden\" class=\"gwid\" name=\"gwid\">\n      <input type=\"hidden\" class=\"gwgroup\" name=\"gwgroup\" value=\"{{ gwgroup|noneFilter() }}\">\n\n      <div class=\"form-group\">\n        <input class=\"form-control name\" type=\"text\" name=\"name\"\n               placeholder=\"Friendly Name(Optional)\" autofocus=\"autofocus\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"form-control ip_addr\" type=\"text\" name=\"ip_addr\"\n               placeholder=\"Hostname/IP\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"form-control strip\" type=\"text\" name=\"strip\"\n               placeholder=\"# of characters to strip from RURI (outbound calls)\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"form-control prefix\" type=\"text\" name=\"prefix\"\n               placeholder=\"The characters to prefix to a RURI (outbound calls)\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"form-control rweight\" type=\"text\" name=\"rweight\"\n               placeholder=\"The amount of traffic allocated to the carrier endpoint (outbound calls)\">\n      </div>\n\n      <div class=\"modal-footer \">\n        <button type=\"submit\" class=\"btn btn-success btn-lg\" style=\"width: 100%;\"><span\n            class=\"glyphicon glyphicon-ok-sign\"></span> Add\n        </button>\n      </div>\n    </form>\n  </div>\n{% endblock %}\n\n\n{% block delete_modal %}\n  <!-- delete-endpoint modal -->\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Delete this entry</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form class=\"gwform\" action=\"/carrierdelete\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n      <input type=\"hidden\" class=\"gwid\" name=\"gwid\">\n      <input type=\"hidden\" class=\"name\" name=\"name\">\n      <input type=\"hidden\" class=\"ip_addr\" name=\"ip_addr\">\n      <input type=\"hidden\" class=\"gwgroup\" name=\"gwgroup\" value=\"{{ gwgroup|noneFilter() }}\">\n      <input type=\"hidden\" class=\"related_rules\" name=\"related_rules\">\n\n      <div class=\"alert alert-danger\"><span class=\"glyphicon glyphicon-warning-sign\"></span> Are you sure you want\n        to delete this Record?\n      </div>\n\n      <div class=\"modal-footer \">\n        <button type=\"submit\" class=\"btn btn-success\"><span class=\"glyphicon glyphicon-ok-sign\"></span> Yes</button>\n        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\"><span\n            class=\"glyphicon glyphicon-remove\"></span> No\n        </button>\n      </div>\n    </form>\n  </div>\n{% endblock %}\n\n\n{% block custom_modals %}\n  <!-- edit-group modal -->\n  <div class=\"modal fade\" id=\"edit-group\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"Edit\" aria-hidden=\"true\">\n    <div class=\"modal-dialog resizable\">\n      <div class=\"modal-content\">\n\n        <div class=\"modal-header\">\n          <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n              class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n          <h4 class=\"modal-title custom_align\" id=\"Heading\">Edit Your Carrier Group</h4>\n        </div>\n\n        <div class=\"modal-body\">\n          <form class=\"gwgroupform\" action=\"/carriergroups\" method=\"POST\" onsubmit=\"event.preventDefault();\"\n                role=\"form\">\n            <!-- nav tabs -->\n            <div id=\"carrier-nav\" class=\"navbar\">\n              <ul class=\"nav nav-tabs\">\n                <li role=\"presentation\" class=\"nav-item\">\n                  <a href=\"#\" name=\"auth-toggle\" data-type=\"toggle\" class=\"nav-link\">Auth</a>\n                </li>\n                <li role=\"presentation\" class=\"nav-item\">\n                  <a href=\"#\" name=\"config-toggle\" data-type=\"toggle\" class=\"nav-link\">Config</a>\n                </li>\n                <li role=\"presentation\" class=\"nav-item\">\n                  <a href=\"#\" name=\"carriers-toggle\" data-type=\"toggle\" class=\"nav-link\">Endpoints</a>\n                </li>\n                {#              <li role=\"presentation\" class=\"nav-item\">#}\n                {#                <a href=\"/carriers\" name=\"carriers-link\" data-type=\"link\" class=\"nav-link\">Endpoints</a>#}\n                {#              </li>#}\n              </ul>\n            </div>\n\n            <!-- config form -->\n            <div class=\"hidden\" name=\"config-toggle\" style=\"padding-top: 15px;\">\n\n              <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n              <input type=\"hidden\" class=\"gwgroup\" name=\"gwgroup\">\n              <input type=\"hidden\" class=\"name\" name=\"name\">\n              <input type=\"hidden\" class=\"gwlist\" name=\"gwlist\">\n\n              <div class=\"form-group\">\n                <input class=\"form-control new_name\" type=\"text\" name=\"new_name\" placeholder=\"Group Name\"\n                       autofocus=\"autofocus\" required=\"required\">\n              </div>\n\n              <div class=\"form-group\">\n                <div class=\"checkbox\">\n                  <label class=\"label-toggle\">\n                    <input class=\"toggle-loadbalancing\" type=\"checkbox\" data-toggle=\"toggle\" value=\"1\"\n                           data-on=\"<span class='icon-load_balance'></span> Enabled\"\n                           data-off=\"<span class='icon-load_balance'></span> Disabled\"\n                           data-width=\"125px\">\n                    Load Balancing\n                  </label>\n                </div>\n                <input class=\"lb_enabled\" type=\"hidden\" name=\"lb_enabled\" value=\"0\">\n              </div>\n            </div>\n\n            <!-- auth form -->\n            <div class=\"hidden\" name=\"auth-toggle\">\n              <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n              <input type=\"hidden\" class=\"gwgroup\" name=\"gwgroup\">\n              <input type=\"hidden\" class=\"name\" name=\"name\">\n              <input type=\"hidden\" class=\"gwlist\" name=\"gwlist\">\n\n              <div class=\"form-group\">\n                <div class=\"authoptions radio\">\n                  <label><input type=\"radio\" data-toggle=\"ip_enabled\" class=\"authtype\" name=\"authtype\" value=\"ip\"\n                                checked>IP Auth</label>\n                  <label><input type=\"radio\" data-toggle=\"userpwd_enabled\" class=\"authtype\" name=\"authtype\"\n                                value=\"userpwd\">Username/Password Auth</label>\n                </div>\n              </div>\n\n              <div class=\"form-group hidden\" id=\"userpwd_enabled\" name=\"userpwd_enabled\" value=\"0\">\n                <p>Please enter the registration username and password provided by the carrier.</p>\n                <div class=\"form-group\">\n                  <input class=\"form-control r_username\" type=\"text\" name=\"r_username\"\n                         placeholder=\"Username\" autofocus=\"autofocus\">\n                </div>\n                <div class=\"form-group\">\n                  <input class=\"form-control auth_password\" type=\"password\" name=\"auth_password\"\n                         placeholder=\"Auth Password\">\n                </div>\n                <div class=\"form-group\">\n                  <input class=\"form-control auth_domain\" type=\"text\" name=\"auth_domain\"\n                         placeholder=\"Registration Server or Domain\">\n                </div>\n                <div class=\"form-group\">\n                  <h5 class=\"modal-title custom_align\" id=\"Heading\">Optional Credentials</h5>\n                </div>\n                <div class=\"form-group\">\n                  <input class=\"form-control auth_username\" type=\"text\" name=\"auth_username\"\n                         placeholder=\"Auth Username\">\n                </div>\n                <div class=\"form-group\">\n                  <input class=\"form-control auth_proxy\" type=\"text\" name=\"auth_proxy\"\n                         placeholder=\"Outbound Proxy\">\n                </div>\n              </div>\n            </div>\n            <!-- carriers table -->\n            <div class=\"hidden\" name=\"carriers-toggle\">\n              <div class=\"wrapper-horizontal edge-centered\">\n\n                <div class=\"tableAddButton\">\n                  <button id='open-CarrierAdd' class='btn btn-success btn-md' data-title=\"Add\" data-toggle=\"modal\"\n                          data-target=\"#add\"> Add\n                  </button>\n                </div>\n\n                <div class=\"navBarButtons\">\n                </div>\n              </div>\n\n              <div id=\"carriers-table\" class=\"table-responsive\">\n                <div id=\"carriers-loading wrapper-vertical centered\">\n                  <div class=\"spinner spinner-circle\"></div>\n                  <h5>Loading Your Carriers..</h5>\n                </div>\n              </div>\n            </div>\n            <div class=\"modal-footer \">\n              <button type=\"submit\" id=\"updateGroupButton\" class=\"btn btn-warning btn-lg\" style=\"width: 100%;\"><span\n                  class=\"glyphicon glyphicon-ok-sign\"></span> Update\n              </button>\n            </div>\n          </form>\n        </div><!-- /.modal-body -->\n\n      </div><!-- /.modal-content -->\n    </div><!-- /.modal-dialog -->\n  </div><!-- /.modal -->\n\n\n  <!-- add-group modal -->\n  <div class=\"modal fade\" id=\"add-group\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"Add\" aria-hidden=\"true\">\n    <div class=\"modal-dialog\">\n      <div class=\"modal-content\">\n\n        <div class=\"modal-header\">\n          <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n              class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n          <h4 class=\"modal-title custom_align\" id=\"Heading\">Add New Carrier Group</h4>\n        </div>\n\n        <div class=\"modal-body\">\n          <form class=\"gwgroupform\" action=\"/carriergroups\" method=\"POST\" role=\"form\">\n            <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n            <input type=\"hidden\" class=\"gwgroup\" name=\"gwgroup\">\n            <input type=\"hidden\" class=\"gwlist\" name=\"gwlist\">\n\n            <div class=\"form-group\">\n              <input class=\"form-control name\" type=\"text\" id=\"name\" name=\"name\" placeholder=\"Group Name\"\n                     autofocus=\"autofocus\" required=\"required\">\n            </div>\n\n            <div class=\"form-group\">\n              <div class=\"checkbox\">\n                <label class=\"label-toggle\">\n                  <input class=\"toggle-loadbalancing\" type=\"checkbox\" data-toggle=\"toggle\" value=\"1\"\n                         data-on=\"<span class='icon-load_balance'></span> Enabled\"\n                         data-off=\"<span class='icon-load_balance'></span> Disabled\"\n                         data-width=\"125px\">\n                  Load Balancing\n                </label>\n              </div>\n              <input class=\"lb_enabled\" type=\"hidden\" name=\"lb_enabled\" value=\"0\">\n            </div>\n\n            <div class=\"form-group\">\n              <select class=\"form-control plugin_name\" id=\"plugin_name\" name=\"plugin_name\" title=\"plugin_name\">\n                <option value=\"\" selected=\"\">Select Carrier Plugin (Optional)</option>\n                <option value=\"Twilio\">Twilio</option>\n              </select>\n            </div>\n\n            <div class=\"form-group plugin_creds hidden\" id=\"plugin_creds\" name=\"plugin_creds\" value=\"0\">\n              <p>Please enter the plugin credentials.</p>\n              <div class=\"form-group\">\n                <input class=\"form-control plugin_account_sid\" id=\"plugin_account_sid\" type=\"text\"\n                       name=\"plugin_account_sid\"\n                       placeholder=\"Account SID\" autofocus=\"autofocus\">\n              </div>\n              <div class=\"form-group\">\n                <input class=\"form-control plugin_account_token\" id=\"plugin_account_token\" type=\"password\"\n                       name=\"plugin_account_token\"\n                       placeholder=\"Account Token\" autofocus=\"autofocus\">\n              </div>\n            </div>\n\n            <div class=\"form-group\">\n              <div class=\"authoptions radio\">\n                <label><input type=\"radio\" data-toggle=\"ip_enabled\" class=\"authtype\" name=\"authtype\" value=\"ip\" checked>IP\n                  Auth</label>\n                <label><input type=\"radio\" data-toggle=\"userpwd_enabled\" class=\"authtype\" name=\"authtype\"\n                              value=\"userpwd\">Username/Password Auth</label>\n              </div>\n            </div>\n\n            <div class=\"form-group hidden\" id=\"userpwd_enabled\" name=\"userpwd_enabled\" value=\"0\">\n              <p>Please enter the registration username and password provided by the carrier.</p>\n              <div class=\"form-group\">\n                <input class=\"form-control r_username\" type=\"text\" name=\"r_username\"\n                       placeholder=\"Username\" autofocus=\"autofocus\">\n              </div>\n              <div class=\"form-group\">\n                <input class=\"form-control auth_password\" type=\"password\" name=\"auth_password\"\n                       placeholder=\"Auth Password\">\n              </div>\n              <div class=\"form-group\">\n                <input class=\"form-control auth_domain\" type=\"text\" name=\"auth_domain\"\n                       placeholder=\"Registration Server or Domain\">\n              </div>\n              <div class=\"form-group\">\n                <h5 class=\"modal-title custom_align\" id=\"Heading\">Optional Credentials</h5>\n              </div>\n              <div class=\"form-group\">\n                <input class=\"form-control auth_username\" type=\"text\" name=\"auth_username\"\n                       placeholder=\"Auth Username\">\n              </div>\n              <div class=\"form-group\">\n                <input class=\"form-control auth_proxy\" type=\"text\" name=\"auth_proxy\"\n                       placeholder=\"Outbound Proxy\">\n              </div>\n            </div>\n\n            <div class=\"modal-footer \">\n              <button type=\"submit\" id=\"addButton\" class=\"btn btn-lg btn-primary\" style=\"width: 100%;\"><span\n                  class=\"glyphicon glyphicon-ok-sign\"></span> Add\n              </button>\n            </div>\n          </form>\n        </div>\n\n      </div><!-- /.modal-content -->\n    </div><!-- /.modal-dialog -->\n  </div><!-- /.modal -->\n\n\n  <!-- delete-group modal -->\n  <div class=\"modal fade\" id=\"delete-group\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"Delete\" aria-hidden=\"true\">\n    <div class=\"modal-dialog\">\n      <div class=\"modal-content\">\n\n        <div class=\"modal-header\">\n          <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n              class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n          <h4 class=\"modal-title custom_align\" id=\"Heading\">Delete this entry</h4>\n        </div>\n\n        <div class=\"modal-body\">\n          <form class=\"gwgroupform\" action=\"/carriergroupdelete\" method=\"POST\" role=\"form\">\n            <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n            <input type=\"hidden\" class=\"gwgroup\" name=\"gwgroup\">\n            <input type=\"hidden\" class=\"name\" name=\"name\">\n            <input type=\"hidden\" class=\"gwlist\" name=\"gwlist\">\n\n            <div class=\"alert alert-danger\"><span class=\"glyphicon glyphicon-warning-sign\"></span> Are you sure you want\n              to delete all endpoints in this group?\n            </div>\n\n            <div class=\"modal-footer \">\n              <button type=\"submit\" class=\"btn btn-success\"><span class=\"glyphicon glyphicon-ok-sign\"></span> Yes\n              </button>\n              <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\"><span\n                  class=\"glyphicon glyphicon-remove\"></span> No\n              </button>\n            </div>\n          </form>\n        </div>\n\n      </div><!-- /.modal-content -->\n    </div><!-- /.modal-dialog -->\n  </div><!-- /.modal -->\n{% endblock %}\n\n{% block custom_js %}\n  {{ script_tag('carriergroups') }}\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/carriers.html",
    "content": "<script type=\"application/javascript\">\n{% if state.kam_reload_required == True %}\n  reloadKamRequired(true);\n{% else %}\n  reloadKamRequired(false);\n{% endif %}\n</script>\n\n<table id=\"carriers\" class=\"table table-striped table-centered\">\n  <thead>\n  <tr class='element-row'>\n    <th><input type=\"checkbox\" class=\"checkall\"/></th>\n    <th data-field=\"id\">Carrier ID</th>\n    <th data-field=\"name\">Name</th>\n    <th data-field=\"ip_addr\">Hostname/IP</th>\n    <th data-field=\"strip\">Strip</th>\n    <th data-field=\"prefix\">Prefix</th>\n    <th data-field=\"rweight\">\n      <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"\"\n            data-original-title=\"relative weight (load balancing only) out of sum of carrier weights in this group.\n            value must be 1-100 or 0 to disable load balancing on this carrier\">\n        Weight\n      </span>\n    </th>\n    <th colspan=\"2\"></th>\n  </tr>\n  </thead>\n  <tbody>\n  {% for row,related_rules in zip(rows,routes) %}\n    {% if new_gwid is not none and row.gwid == new_gwid %}\n    <tr class='element-row new_gw' data-gwid=\"{{ row.gwid }}\">\n    {% else %}\n    <tr class='element-row' data-gwid=\"{{ row.gwid }}\">\n    {% endif %}\n      <td><input type=\"checkbox\" class=\"checkthis\" value=\"1\"/></td>\n      <td class='gwid'>{{ row.gwid }}</td>\n      <td class='name'>{{ row.description|attrFilter('name') }}</td>\n      <td class='ip_addr'>{{ row.address }}</td>\n      <td class='strip'>{{ row.strip }}</td>\n      <td class='prefix'>{{ row.pri_prefix }}</td>\n      <td class='rweight'>{{ row.dispatcher_attrs|attrFilter('rweight', delims=(';', '=')) }}</td>\n      <td class='related_rules hidden'>{{ related_rules }}</td>\n      <td>\n        <p data-placement=\"top\" data-toggle=\"tooltip\" title=\"Edit\">\n          <button id=\"open-Update\" class=\"open-Update btn btn-primary btn-xs\" data-title=\"Edit\"\n                  data-toggle=\"modal\" data-target=\"#edit\"><span class=\"glyphicon glyphicon-pencil\"></span>\n          </button>\n        </p>\n      </td>\n      <td>\n        <p data-placement=\"top\" data-toggle=\"tooltip\" title=\"Delete\">\n          <button id=\"open-Delete\" class=\"open-Delete btn btn-danger btn-xs\" data-title=\"Delete\"\n                  data-toggle=\"modal\"\n                  data-target=\"#delete\"><span class=\"glyphicon glyphicon-trash\"></span>\n          </button>\n        </p>\n      </td>\n    </tr>\n  {% endfor %}\n  </tbody>\n</table>\n"
  },
  {
    "path": "gui/templates/cdrs.html",
    "content": "{% extends 'table_layout.html' %}\n\n{% block title %}Call Detail Records{% endblock %}\n\n{% block custom_css %}\n  {{ link_tag('cdrs') }}\n{% endblock %}\n\n{% block table_headers %}\n  <div>\n    <h3>Call Detail Records</h3>\n  </div>\n\n  <div class=\"btn-group col-sm-3\" id=\"menu\">\n    <select class=\"btn-primary btn-md\" id=\"endpointgroups\">\n      <option class=\"hidden\" value=\"0\" selected disabled>Select Endpoint Group</option>\n    </select>\n    <i class=\"glyphicon glyphicon glyphicon-refresh\" id=\"refreshCDR\"></i>\n    <i class=\"glyphicon glyphicon-download-alt\" id=\"downloadCDR\"></i>\n  </div>\n\n  <div>\n    <label for=\"toggle_completed_calls\">\n    Filter Calls\n    </label>\n    <input id=\"toggle_completed_calls\" type=\"checkbox\" data-toggle=\"toggle\" value=\"1\"\n           data-on=\"All Calls <span class='icon-phone_enabled'></span>\"\n           data-off=\"Billable Calls <span class='icon-money'></span>\"\n           data-width=\"125px\">\n  </div>\n{% endblock %}\n\n\n{% block table %}\n  <table id=\"cdrs\" class=\"table table-striped table-centered\">\n    <thead>\n    <tr class='element-row'>\n      <th data-field=\"cdr_id\">CDR ID</th>\n      <th data-field=\"call_start_time\">Call Start</th>\n      <th data-field=\"call_duration\">Call Duration</th>\n      <th data-field=\"call_direction\">Call Direction</th>\n      <th data-field=\"src_gwgroupid\" class=\"hidden\"></th>\n      <th data-field=\"src_gwgroupname\">Source Gateway Group</th>\n      <th data-field=\"dst_gwgroupid\" class=\"hidden\"></th>\n      <th data-field=\"dst_gwgroupname\">Destination Gateway Group</th>\n      <th data-field=\"src_username\">Source Username</th>\n      <th data-field=\"dst_username\">Destination Username</th>\n      <th data-field=\"src_address\">Source Address</th>\n      <th data-field=\"dst_address\">Destination Address</th>\n      <th data-field=\"call_id\">Call ID</th>\n    </tr>\n    </thead>\n  </table>\n{% endblock %}\n\n\n{% block custom_js %}\n  {{ script_tag('jquery.tabledit') }}\n  {{ script_tag('cdrs') }}\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/certificates.html",
    "content": "{% extends 'table_layout.html' %}\n\n{% block title %}Certificates{% endblock %}\n\n{% block custom_css %}\n  {{ link_tag('certificates') }}\n{% endblock %}\n\n{% block table_headers %}\n  <div>\n    <h3>Certificates</h3>\n  </div>\n\n  <div class=\"tableAddButton\">\n    <button id='open-Add' class='btn btn-primary btn-md' data-title=\"Add\" data-toggle=\"modal\"\n            data-target=\"#add\">\n      Add\n    </button>\n  </div>\n{% endblock %}\n\n\n{% block table %}\n  <table id=\"certificates\" class=\"table table-striped table-centered\">\n    <thead>\n    <tr class='element-row'>\n      <th data-field=\"id\">ID</th>\n      <th data-field=\"domain\">Name</th>\n      <th data-field=\"type\">Type</th>\n      <th data-field=\"assigned_domains\">Assigned Domains</th>\n    </tr>\n    </thead>\n  </table>\n{% endblock %}\n\n\n<!--\n  TODO:\n    this seems to be copied directly from endpointgroups\n    there will likely be conflicts now or in the future\n    this needs to be updated\n-->\n{% block edit_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Update Certificate\n      <button id=\"open-Delete\" class=\"open-Delete btn btn-danger btn-xs\" data-title=\"Delete\"\n              data-toggle=\"modal\" data-target=\"#delete\"><span class=\"glyphicon glyphicon-trash\"></span>\n      </button>\n    </h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form id=\"editCertificateForm\" action=\"#\" method=\"POST\" role=\"form\" enctype=\"multipart/form-data\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n\n      <div class=\"form-group\">\n        <input class=\"form-control\" type=\"text\" id=\"domain2\" name=\"domain\" placeholder=\"Domain name for the certificate\"\n               autofocus=\"autofocus\">\n      </div>\n      <div class=\"form-group\">\n        <div id=\"certtypeoptions2\" class=\"btn-group\" data-toggle=\"buttons\">\n{#          <label>#}\n{#            <input type=\"radio\" name=\"certtype\" id=\"certtype_generate2\" value=\"generated\" checked>#}\n{#            Generate#}\n{#          </label>#}\n          <label>\n            <input type=\"radio\" name=\"certtype\" id=\"certtype_upload2\" value=\"uploaded\">\n            Upload\n          </label>\n        </div>\n      </div>\n{#      <div id=\"generate2\">#}\n{#        <div class=\"alert alert-warning pre-scrollable hide\" id=\"terminalDiv2\">#}\n{#          <strong>#}\n{#            Access this server via ssh and execute the command below. Follow the instructions and upload the certificate<br><br>#}\n{#          </strong>#}\n{#          <pre><code class=\"bash\" id=\"terminalCommand2\">#}\n{##}\n{#         </code></pre>#}\n{#        </div>#}\n{#      </div><!-- end of generate tab -->#}\n\n      <div id=\"uploaded2\" class=\"form-group hide\">\n\n        <input type=\"file\" name=\"certandkey\" multiple>\n\n      </div> <!-- end of upload tab -->\n\n      <div class=\"modal-footer \">\n        <button type=\"button\" id=\"updateButton\" class=\"btn btn-primary btn-lg\" style=\"width: 100%;\"><span\n            class=\"glyphicon glyphicon-ok-sign\"></span> Update Certificate\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n\n{% block add_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Add</h4>\n  </div>\n  <div class=\"modal-body\">\n    <form id=\"addCertificateForm\" action=\"#\" method=\"POST\" role=\"form\" enctype=\"multipart/form-data\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n\n      <div class=\"form-group\">\n        <input class=\"form-control\" type=\"text\" id=\"domain\" name=\"domain\" placeholder=\"Domain name for the certificate\"\n               autofocus=\"autofocus\">\n      </div>\n      <div class=\"form-check\">\n        <input type=\"checkbox\" class=\"form-check-input\" id=\"replace_default_cert\" name=\"replace_default_cert\"\n               value=\"true\" checked>\n        <label class=\"form-check-label\" for=\"replace_default_cert\">Replace the default certificate</label>\n      </div>\n      <div class=\"form-group\">\n        <div id=\"certtypeoptions\" class=\"btn-group\" data-toggle=\"buttons\">\n{#          <label>#}\n{#            <input type=\"radio\" name=\"certtype\" id=\"certtype_generate\" value=\"generated\">#}\n{#            Generate#}\n{#          </label>#}\n          <label>\n            <input type=\"radio\" name=\"certtype\" id=\"certtype_upload\" value=\"uploaded\" checked>\n            Upload\n          </label>\n        </div>\n      </div>\n{#      <div id=\"generate\">#}\n{#        <div class=\"alert alert-warning pre-scrollable hide\" id=\"terminalDiv\">#}\n{#          <strong>#}\n{#            Access this server via ssh and execute the command below. Follow the instructions and upload the certificate<br><br>#}\n{#          </strong>#}\n{#          <pre><code class=\"bash\" id=\"terminalCommand\">#}\n{#dsiprouter generatecert#}\n{#           </code></pre>#}\n{#        </div>#}\n{#      </div><!-- end of generate tab -->#}\n\n      <div id=\"uploaded\" class=\"form-group hide\">\n\n        <label class=\"form-check-label\" for=\"certandkey\">\n          Select certificate (.crt or .cert) and private key file (.key or .pem)\n        </label>\n        <input type=\"file\" id=\"certandkey\" name=\"certandkey\" multiple>\n\n      </div> <!-- end of upload tab -->\n\n      <div class=\"modal-footer \">\n        <button type=\"button\" id=\"addButton\" class=\"btn btn-primary btn-lg\" style=\"width: 100%;\"><span\n            class=\"glyphicon glyphicon-ok-sign\"></span> Add Certifcate\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n\n{% block delete_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Delete this entry</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form action=\"/pbxdelete\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n\n      <div class=\"form-group\">\n        <input class=\"gwgroup form-control\" type=\"hidden\" name=\"gwgroup\">\n        <input class=\"gwid form-control\" type=\"hidden\" name=\"gwid\">\n        <input class=\"name form-control\" type=\"hidden\" name=\"name\">\n      </div>\n\n      <div class=\"alert alert-danger\"><span class=\"glyphicon glyphicon-warning-sign\"></span> Are you sure you want\n        to delete this Certificate?\n      </div>\n\n      <div class=\"modal-footer \">\n        <button id=\"deleteButton\" type=\"button\" class=\"btn btn-success\"><span\n            class=\"glyphicon glyphicon-ok-sign\"></span> Yes\n        </button>\n        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\"><span\n            class=\"glyphicon glyphicon-remove\"></span> No\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n{% block custom_js %}\n  {{ script_tag('jquery.tabledit') }}\n  {{ script_tag('certificates') }}\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/dashboard.html",
    "content": "{% extends 'fullwidth_layout.html' %}\n\n{% block title %}Dashboard{% endblock %}\n\n{% block custom_css %}\n  {{ link_tag('dashboard') }}\n{% endblock %}\n\n{% block body %}\n\n  <div class=\"wrapper-horizontal edge-centered children-align-inherit content-header\">\n    <h2>Dashboard</h2>\n\n    {% if reloadstatus %}\n      <h3>Kamailio Reload: {{ reloadstatus }}</h3>\n    {% endif %}\n  </div>\n\n  <div class=\"content-section\"> <!-- begin placeholders -->\n    <div class=\"col-xs-4 col-sm-4 placeholder\">\n      <div class=\"dashboard-metric-label\">Active SIP Messages</div>\n      <div id=\"dashboard_current_calls\" class=\"dashboard-metric\"></div>\n    </div>\n    <div class=\"col-xs-4 col-sm-4 placeholder\">\n      <div class=\"dashboard-metric-label\">SIP Messages in Queue</div>\n      <div id=\"dashboard_calls_inqueue\" class=\"dashboard-metric\"></div>\n    </div>\n    <div class=\"col-xs-4 col-sm-4 placeholder\">\n      <div class=\"dashboard-metric-label\">Total SIP Messages</div>\n      <div id=\"dashboard_total_calls_processed\" class=\"dashboard-metric\"></div>\n    </div>\n  </div> <!-- end placeholders -->\n\n  <div class=\"content-section\"> <!-- begin dashboard info -->\n    <div class=\"col-xs-4\">\n      <div class=\"panel panel-default\">\n        <div class=\"panel-heading\">Documentation</div>\n        <div class=\"panel-body\">\n          <ul>\n            <li><a href=\"/docs/user/index.html\" target=\"_blank\">User Documentation</a></li>\n            <li><a href=\"/docs/dev/index.html\" target=\"_blank\">Developer Documentation</a></li>\n            <li><a href=\"/docs/routes/index.html\" target=\"_blank\">Routes Documentation</a></li>\n          </ul>\n        </div>\n      </div>\n    </div>\n    <div class=\"col-xs-4\">\n      <div class=\" panel  panel-default\">\n        <div class=\"panel-heading\">Training</div>\n        <div class=\"panel-body\">\n          <ul>\n            <li><a href=\"https://dopensource.com/product/dsiprouter-admin-course/\" target=\"_blank\">dSIPRouter 1 Day\n              Admin Course</a></li>\n          </ul>\n        </div>\n      </div>\n    </div>\n    <div class=\"col-xs-4\">\n      <div class=\"panel panel-default\">\n        <div class=\"panel-heading\">Support</div>\n        <div class=\"panel-body\">\n          <ul>\n            <li>Paid Support\n              <ul>\n                <li><a href=\"https://dopensource.com/dsiprouter-core/\" target=\"_blank\">Annual Subscription</a></li>\n              </ul>\n            </li>\n            <li>Free Support\n              <ul>\n                <li><a href=\"https://join.slack.com/t/dsiproutercommunity/shared_invite/zt-1dtqvpyck-H9k~DYgJJ2XIFgh_rWqdPA/\" target=\"_blank\">Slack</a></li>\n                <li><a href=\"https://groups.google.com/forum/#!forum/dsiprouter\" target=\"_blank\">User Forum</a></li>\n              </ul>\n            </li>\n            <li>Kamailio Consultants Directory\n              <ul>\n                <li><a href=\"https://www.kamailio.org/w/business-directory/\" target=\"_black\">Kamailio Business\n                  Directory</a></li>\n              </ul>\n            </li>\n          </ul>\n        </div>\n      </div>\n    </div>\n  </div> <!-- end dashboard info -->\n\n{% endblock %}\n\n{% block custom_js %}\n  {{ script_tag('dashboard') }}\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/domains.html",
    "content": "{% extends 'table_layout.html' %}\n\n{% block title %}Domains{% endblock %}\n\n{% block custom_css %}\n{% endblock %}\n\n\n{% block table_headers %}\n  <div>\n    <h3>List of Domain(s)</h3>\n  </div>\n\n  <div class=\"tableAddButton\">\n    <button id='open-DomainAdd' class='btn btn-success btn-md' data-title=\"Add\" data-toggle=\"modal\" data-target=\"#add\">\n      Add\n    </button>\n  </div>\n{% endblock %}\n\n\n{% block table %}\n\n  <table data-toggle=\"table\" id=\"domains\" class=\"table table-striped table-centered\">\n\n    <thead>\n    <tr class='element-row'>\n      <!--<th><input class=\"checkall\" type=\"checkbox\"/></th>-->\n      <th></th>\n      <th data-field=\"domain_id\">Domain ID</th>\n      <th data-field=\"domain_name\">Domain Name</th>\n      <th data-field=\"domain_type\">Domain Type</th>\n      <th data-field=\"pbx_name\">Domain Created By</th>\n      <th data-field=\"authtype\">Domain Auth</th>\n      <th data-field=\"pbx_list\">Mapped Endpoints</th>\n      <th data-field=\"notes\">Notes</th>\n      <th></th>\n      <th></th>\n      <th></th>\n    </tr>\n    </thead>\n    <tbody>\n    {% for row in rows %}\n      <tr class='element-row'>\n        <td><input type=\"checkbox\" class=\"checkthis\" value=\"1\"/></td>\n        <td class='domain_id'>{{ row.id }}</td>\n        <td class='domain_name'>{{ row.domain }}</td>\n        <td class='domain_type'>{{ row.type|domainTypeFilter() }}</td>\n        {% if row.type|domainTypeFilter() == \"Dynamic\" %}\n          <td class='pbx_name'><a\n              href=\"/endpointgroups?id={{ pbxlookup[row.domain]['pbx_list'] }}\">{{ pbxlookup[row.domain]['name'] }}</a>\n          </td>\n        {% else %}\n          <td class='pbx_name'>Manually Created</td>\n        {% endif %}\n        <td class='authtype'>{{ pbxlookup[row.domain]['domain_auth'] }}</td>\n        <td class='pbx_list'>{{ pbxlookup[row.domain]['pbx_list'] }}</td>\n        <td class='notes'>{{ pbxlookup[row.domain]['notes'] }}</td>\n\n        {% if row.type|domainTypeFilter() != \"Dynamic\" %}\n          <td>\n            <p data-placement=\"top\" data-toggle=\"tooltip\" title=\"Edit\">\n              <button id=\"open-Update\" class=\"open-Update btn btn-primary btn-xs\" data-title=\"Edit\" data-toggle=\"modal\"\n                      data-target=\"#edit\"><span class=\"glyphicon glyphicon-pencil\"></span>\n              </button>\n            </p>\n          </td>\n          <td>\n            {% if pbxlookup[row.domain]['domain_auth'] == \"msteams\" %}\n              <p data-placement=\"top\" data-toggle=\"tooltip\" title=\"Test MSTeams Connectivity\">\n                <button id=\"open-Test\" class=\"open-Test btn btn-warning btn-xs\" data-title=\"Test MSTeams Connectivity\"\n                        data-toggle=\"modal\"\n                        onclick=\"window.location='domains/msteams/{{ row.id }}'\">\n                  <span class=\"glyphicon glyphicon-plane\"></span></button>\n              </p>\n            {% endif %}\n          </td>\n          <td>\n            <p data-placement=\"top\" data-toggle=\"tooltip\" title=\"Delete\">\n              <button id=\"open-Delete\" class=\"open-Delete btn btn-danger btn-xs\" data-title=\"Delete\" data-toggle=\"modal\"\n                      data-target=\"#delete\"><span class=\"glyphicon glyphicon-trash\"></span></button>\n            </p>\n          </td>\n        {% else %}\n          <td></td>\n          <td></td>\n        {% endif %}\n      </tr>\n    {% endfor %}\n    </tbody>\n  </table>\n\n{% endblock %}\n\n\n{% block edit_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Edit Your Domain Detail</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form id=\"updateDomainForm\" action=\"/domains\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n      <input class=\"domain_id\" type=\"hidden\" name=\"domain_id\">\n\n      <div class=\"form-group\">\n        <input class=\"domain_name form-control\" type=\"text\" name=\"domainlist\"\n               placeholder=\"domainA.com,domainB.com\">\n      </div>\n\n\n      <div class=\"form-group\">\n        <div class=\"domainauthoptions radio\">\n          <select class=\"authtype form-control\" name=\"authtype\">\n            <option value=\"\">Select Domain Type</option>\n            <option value=\"passthru\">Pass Thru to PBX</option>\n            {% if licenseValid('DSIP_MSTEAMS') == False %}(Subscription Required)\n              <option value=\"msteams-nosub\">Microsoft Teams Direct Routing (Subscription Required)</option>\n            {% else %}\n              <option value=\"msteams\">Microsoft Teams Direct Routing</option>\n            {% endif %}\n\n            <option value=\"local\">Local Subscriber Table</option>\n            <option value=\"realtime\">Real Time DB</option>\n          </select>\n          <!--\n             <label><input class=\"authtype \" type=\"radio\" data-toggle=\"realtime\" name=\"authtype\" value=\"realtime\" checked>\n                  Realtime DB (aka Asterisk Realtime)</label>\n                <label><input class=\"authtype \" type=\"radio\" data-toggle=\"local\" name=\"authtype\" value=\"local\">\n                  Local Subscriber Table</label>\n                <label><input class=\"authtype \" type=\"radio\" data-toggle=\"passthru\" name=\"authtype\" value=\"passthru\">\n            Pass Thru to PBX</label>-->\n        </div>\n      </div>\n\n      <div class=\"form-group\">\n        <input class=\"pbx_list form-control\" type=\"text\" name=\"pbx_list\"\n               placeholder=\"List of backend PBX ID's \">\n      </div>\n\n      <div class=\"form-group\">\n        <input class=\"notes form-control\" type=\"text\" name=\"notes\"\n               placeholder=\"Notes\">\n      </div>\n\n      <div class=\"modal-footer \">\n        <button type=\"submit\" class=\"btn btn-warning btn-lg\" style=\"width: 100%;\"><span\n            class=\"glyphicon glyphicon-ok-sign\"></span> Update\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n\n{% block add_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Add New Domain</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form id=\"addDomainForm\" action=\"/domains\" method=\"POST\" role=\"form\">\n      <input class=\"domain_id\" type=\"hidden\" name=\"domain_id\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n\n      <div class=\"form-group\">\n        <input class=\"domain_name form-control\" type=\"text\" name=\"domainlist\" autofocus=\"autofocus\"\n               placeholder=\"domainA.com,domainB.com\">\n      </div>\n\n\n      <div class=\"form-group\">\n        <div class=\"domainauthoptions radio\">\n\n          <select class=\"authtype form-control\" name=\"authtype\">\n            <option value=\"\">Select Domain Type</option>\n            <option value=\"passthru\">Pass Thru to PBX</option>\n            {% if licenseValid('DSIP_MSTEAMS') == False %}\n              <option value=\"msteams-nosub\">Microsoft Teams Direct Routing (Subscription Required)</option>\n            {% else %}\n              <option value=\"msteams\">Microsoft Teams Direct Routing</option>\n            {% endif %}\n            <option value=\"local\">Local Subscriber Table</option>\n            <option value=\"realtime\">Real Time DB</option>\n          </select>\n        </div>\n      </div>\n\n      <div class=\"form-group\">\n        <input class=\"pbx_list form-control\" type=\"text\" name=\"pbx_list\"\n               placeholder=\"List of backend PBX ID's \">\n      </div>\n\n      <div class=\"form-group\">\n        <input class=\"notes form-control\" type=\"text\" name=\"notes\"\n               placeholder=\"Notes\">\n      </div>\n\n      <div class=\"modal-footer \">\n        <button type=\"submit\" class=\"btn btn-success btn-lg\" style=\"width: 100%;\"><span\n            class=\"glyphicon glyphicon-ok-sign\"></span> Add\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n\n{% block delete_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Delete this entry</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form action=\"/domainsdelete\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n\n      <div class=\"form-group\">\n        <input class=\"domain_id form-control\" type=\"hidden\" name=\"domain_id\">\n        <input class=\"domain_name form-control\" type=\"hidden\" name=\"domain_name\">\n      </div>\n\n      <div class=\"alert alert-danger\"><span class=\"glyphicon glyphicon-warning-sign\"></span> Are you sure you want\n        to delete this Record?\n      </div>\n\n      <div class=\"modal-footer \">\n        <button type=\"submit\" class=\"btn btn-success\"><span class=\"glyphicon glyphicon-ok-sign\"></span> Yes</button>\n        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\"><span\n            class=\"glyphicon glyphicon-remove\"></span> No\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n{% block custom_js %}\n  {{ script_tag('domains') }}\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/endpointgroups.html",
    "content": "{% extends 'table_layout.html' %}\n\n{% block title %}Endpoint Groups{% endblock %}\n\n{% block custom_css %}\n{% endblock %}\n\n{% block table_headers %}\n  <div>\n    <h3>List of Endpoint Groups</h3>\n  </div>\n\n  <div class=\"tableAddButton\">\n    <div class=\"btn-group mr-2\">\n      <button id='open-EndpointGroupsAdd' class='btn btn-success btn-md' data-title=\"Add\" data-toggle=\"modal\"\n              data-target=\"#add\">\n        Add\n      </button>\n    </div>\n    <!--\n    <div class=\"btn-group\">\n      <div class=\"dropdown\">\n        <button class=\"btn btn-secondary dropdown-toggle\" type=\"button\" id=\"dropdownMenuButton\" data-toggle=\"dropdown\" aria-haspopup=\"true\" aria-expanded=\"false\">\n        Batch Actions<span class=\"caret\"></span>\n        </button>\n\n        <div class=\"dropdown-menu\" style=\"width:190px;padding:5px\" aria-labelledby=\"dropdownMenuButton\">\n          <a class=\"dropdown-item\" href=\"#\" onclick='enableMaintenanceMode()'>Enable Maintenance Mode</a>\n          <a class=\"dropdown-item\" href=\"#\" onclick='disableMaintenanceMode()'>Disable Maintenance Mode</a>\n        </div>\n      </div>\n    </div>\n    -->\n  </div>\n{% endblock %}\n\n\n{% block table %}\n  <table id=\"endpointgroups\" class=\"table table-striped table-centered\">\n    <thead>\n    <tr class='element-row'>\n      <th data-field=\"name\">Name</th>\n      <th data-field=\"gwgroupid\">ID</th>\n    </tr>\n    </thead>\n  </table>\n{% endblock %}\n\n\n{% block edit_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Edit Endpoint Group Details\n      <button id=\"open-Delete\" class=\"open-Delete btn btn-danger btn-xs\" data-title=\"Delete\"\n              data-toggle=\"modal\" data-target=\"#delete\"><span class=\"glyphicon glyphicon-trash\"></span>\n      </button>\n    </h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form action=\"/endpointgroups\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n      <input class=\"gwid\" type=\"hidden\" name=\"gwid\">\n      <input class=\"gwgroupid\" type=\"hidden\" name=\"gwgroupid\">\n\n      <div class=\"form-group\">\n        <input class=\"name form-control\" type=\"text\" name=\"name\" placeholder=\"Friendly Name(Optional)\"\n               autofocus=\"autofocus\">\n      </div>\n\n      <!-- <div class=\"form-group\">\n        <input class=\"ip_addr form-control\" type=\"text\" name=\"ip_addr\" placeholder=\"IP Address\">\n      </div> -->\n\n      <!-- nav tabs -->\n      <div id=\"endpoint-nav\" class=\"navbar\">\n        <ul class=\"nav nav-tabs\">\n          <li role=\"presentation\" class=\"auth-tab active\">\n            <a href=\"#auth\" name=\"auth-toggle\" data-toggle=\"tab\">Auth</a>\n          </li>\n          <li role=\"presentation\">\n            <a href=\"#endpoints\" name=\"endpoints-toggle\" data-toggle=\"tab\">Endpoints</a>\n          </li>\n          <li role=\"presentation\">\n            <a href=\"#call_settings\" name=\"settings-toggle\" data-toggle=\"tab\">Call Settings</a>\n          </li>\n          <li role=\"presentation\">\n            <a href=\"#notifications\" name=\"notifications-toggle\" data-toggle=\"tab\">Notifications</a>\n          </li>\n          <li role=\"presentation\">\n            <a href=\"#cdr\" name=\"cdr-toggle\" data-toggle=\"tab\">CDR</a>\n          </li>\n          <li role=\"presentation\">\n            <a href=\"#fusionpbx\" name=\"fusion-toggle\" data-toggle=\"tab\">FusionPBX</a>\n          </li>\n        </ul>\n      </div>\n\n      <!-- tab content -->\n      <div class=\"tab-content\">\n        <div id=\"auth\" class=\"tab-pane fade in active\" name=\"auth-toggle\">\n          <div class=\"form-group\">\n            <div id=\"authoptions2\" class=\"btn-group\" data-toggle=\"buttons\">\n              <label><input class=\"authtype\" type=\"radio\" name=\"authtype\" id=\"ip2\" value=\"ip\" checked> IP\n                Auth</label>\n              <label><input class=\"authtype\" type=\"radio\" name=\"authtype\" id=\"userpwd2\" value=\"userpwd\">\n                Username/Password\n                Auth</label>\n            </div>\n          </div>\n\n          <div id=\"userpwd_enabled2\" class=\"userpwd\">\n            <p>Please enter a username and password for the PBX/Endpoint you want to register to. Specify domain if\n              different than the default domain: <b>{{ DEFAULT_auth_domain }}</b></p>\n            <div class=\"form-group\">\n              <input class=\"auth_username form-control\" type=\"text\" name=\"auth_username\"\n                     placeholder=\"Auth Username\">\n            </div>\n            <div class=\"form-group wrapper-fieldicon-right\">\n              <input id=\"auth_password2\" class=\"form-control\" type=\"password\" name=\"auth_password\"\n                     placeholder=\"Auth Password\">\n              <span toggle=\"#auth_password2\" class=\"field-icon toggle-password glyphicon glyphicon-eye-close\"></span>\n            </div>\n            <div class=\"form-group\">\n              <input class=\"auth_domain form-control\" type=\"text\" name=\"auth_domain\"\n                     placeholder=\"Auth Domain (optional)\">\n            </div>\n          </div>\n        </div> <!-- end of auth tab -->\n\n        <div id=\"endpoints\" class=\"tab-pane fade in\" name=\"endpoints-toggle\">\n          <table id=\"endpoint-table\" class=\"table\">\n            <thead>\n            <tr>\n              <th>ID</th>\n              <th>\n                <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"\"\n                      data-original-title=\"hostname or IP address to route to (and/or) authenticate endpoint\">\n                  Host\n                </span>\n              </th>\n              <th>\n                <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"\"\n                      data-original-title=\"port to use when routing to endpoint\">\n                  Port\n                </span>\n              </th>\n              <th>\n                <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"\"\n                      data-original-title=\"signalling transport for calls to endpoint\">\n                  Signalling\n                </span>\n              </th>\n              <th>\n                <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"\"\n                      data-original-title=\"media transport for calls to endpoint\">\n                  Media\n                </span>\n              </th>\n              <th>Description</th>\n              <th>\n                <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"\"\n                      data-original-title=\"relative weight (load balancing only) out of sum of endpoint weights in this group.\n                      value must be 1-100 or 0 to disable load balancing on this endpoint\">\n                  Weight\n                </span>\n              </th>\n              <th>\n                <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"\"\n                      data-original-title=\"when enabled, the gateway is sent keepalive messages (at a default interval of 60 seconds).\n                      if disabled, the destination is always attempted when routing\">\n                  Keepalive\n                </span>\n              </th>\n              <th>\n                <button class=\"btn btn-success btn-md\" type=\"button\" id=\"updateEndpointRow\">Add Row</button>\n              </th>\n            </tr>\n            </thead>\n            <tbody id=\"endpoint-tablebody\">\n            </tbody>\n          </table>\n        </div> <!-- enf of endpoints tab -->\n\n        <div id=\"call_settings\" class=\"tab-pane fade in\" name=\"settings-toggle\">\n          <div class=\"form-group\">\n            <input class=\"strip form-control\" type=\"text\" name=\"strip\"\n                   placeholder=\"# of characters to strip from RURI (inbound calls)\">\n          </div>\n          <div class=\"form-group\">\n            <input class=\"prefix form-control\" type=\"text\" name=\"prefix\"\n                   placeholder=\"The characters to prefix to a RURI (inbound calls)\">\n          </div>\n          <div class=\"form-group\">\n            <input class=\"call_limit form-control\" type=\"number\" name=\"call_limit\" placeholder=\"Max Concurrent Calls\"\n                   autocomplete=\"off\" min=\"0\">\n          </div>\n          <div class=\"form-group\">\n            <input class=\"call_timeout form-control\" type=\"number\" name=\"call_timeout\" placeholder=\"Maximum Call Length (seconds)\"\n                   autocomplete=\"off\" min=\"0\">\n          </div>\n        </div> <!-- end of call settings tab -->\n\n        <div id=\"notifications\" class=\"tab-pane fade in\" name=\"notifications-toggle\">\n          <div class=\"form-group\">\n            <input class=\"email_over_max_calls form-control\" type=\"text\" name=\"email_over_max_calls\"\n                   placeholder=\"Email for Over Max Concurrent Calls\">\n          </div>\n          <div class=\"form-group\">\n            <input class=\"email_endpoint_failure form-control\" type=\"text\" name=\"email_endpoint_failure\"\n                   placeholder=\"Email for Endpont Failure\">\n          </div>\n        </div> <!-- end of notifications tab -->\n\n        <div id=\"cdr\" class=\"tab-pane fade in\" name=\"cdr-toggle\">\n          <div class=\"form-group\">\n            <input class=\"cdr_email form-control\" type=\"text\" name=\"cdr_email\"\n                   placeholder=\"Email to send CDR's\">\n          </div>\n          <!-- CDR report interval chooser, based on crontab design -->\n          <!-- TODO: make this a combobox -->\n          <div class=\"form-group\">\n            <label>CDR Report Interval (minute hour day month weekday)</label>\n            <div class=\"wrapper-horizontal centered\">\n              <input class=\"cdr_send_minute form-control\" type=\"text\" name=\"cdr_send_minute\" value=\"*\"\n                     title=\"Minute of the hour (0-59) or (* is any minute)\"\n                     pattern=\"\\*(/([0-5]?[0-9]))?|([0-5]?[0-9])(,[0-5]?[0-9])*\">\n              <input class=\"cdr_send_hour form-control\" type=\"text\" name=\"cdr_send_hour\" value=\"*\"\n                     title=\"Hour of the day (0-23) or (* is any hour)\"\n                     pattern=\"\\*(/(2[0-3]|1[0-9]|[0-9]))?|(2[0-3]|1[0-9]|[0-9])(,(2[0-3]|1[0-9]|[0-9]))*\">\n              <input class=\"cdr_send_day form-control\" type=\"text\" name=\"cdr_send_day\" value=\"1\"\n                     title=\"Day of the month (1-31) or (* is any day)\"\n                     pattern=\"\\*(/(3[0-1]|[1-2][0-9]|[1-9]))?|(3[0-1]|[1-2][0-9]|[1-9])(,(3[0-1]|[1-2][0-9]|[1-9]))*\">\n              <input class=\"cdr_send_month form-control\" type=\"text\" name=\"cdr_send_month\" value=\"*\"\n                     title=\"Month of the year (1-12) or (* is any month) or (jan-dec)\"\n                     pattern=\"\\*(/(1[0-2]|[1-9]))?|(1[0-2]|[1-9])(,(1[0-2]|[1-9]))*|(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\">\n              <input class=\"cdr_send_weekday form-control\" type=\"text\" name=\"cdr_send_weekday\" value=\"*\"\n                     title=\"Day of the week (0-6) or (* is any weekday) or (sun-sat)\"\n                     pattern=\"\\*(/([0-6]))?|([0-6])(,[0-6])*|(sun|mon|tue|wed|thur|fri|sat)\">\n            </div>\n          </div>\n        </div> <!-- end of cdr  tab -->\n\n        <div id=\"fusionpbx\" class=\"tab-pane fade in\" name=\"fusion-toggle\">\n          <div class=\"form-group\">\n            <div class=\"checkbox\">\n              <label class=\"label-toggle\">\n                <input class=\"toggleFusionPBXDomain\" type=\"checkbox\" data-toggle=\"toggle\" value=\"1\"\n                       data-on=\"<span class='icon-fusionpbx'></span> Enabled\"\n                       data-off=\"<span class='icon-fusionpbx'></span> Disabled\"\n                       data-width=\"125px\">\n                FusionPBX Domain Support\n              </label>\n            </div>\n            <input class=\"fusionpbx_db_enabled \" type=\"hidden\" name=\"fusionpbx_db_enabled\" value=\"0\">\n          </div>\n          <div class=\"FusionPBXDomainOptions form-group hidden\">\n            <div class=\"alert alert-warning pre-scrollable\">\n              <strong>\n                You need access to the FusionPBX database. Run these commands as root on the FusionPBX server.\n              </strong>\n              <pre><code class=\"bash\">DSIPROUTER_IP={{ dsiprouter_ip }}\nsed  -i -r \"s/^#?listen_addresses[ \\t]?=[ \\t]?.*/listen_addresses = '*'/m\" /etc/postgresql/*/main/postgresql.conf\niptables -A INPUT -p tcp -s $DSIPROUTER_IP/32 --dport 5432 -j ACCEPT\niptables-save &gt;/etc/iptables/rules.v4\n# Run this command if your don't want to enter a password for the FusionPBX Database(DB) Password\necho -e \"host    all             all            $DSIPROUTER_IP/32            trust\" &gt;&gt;/etc/postgresql/*/main/pg_hba.conf\nsystemctl restart postgresql\n# Run this command if your using fail2ban\nsed -i -r \"s|^#?(ignoreip = .*)|\\1 $DSIPROUTER_IP/32|\" /etc/fail2ban/jail.conf\nsystemctl restart fail2ban</code></pre>\n            </div>\n            <div class=\"form-group\">\n              <input class=\"fusionpbx_db_server form-control\" type=\"text\" name=\"fusionpbx_db_server\"\n                     placeholder=\"FusionPBX Database IP or Hostname\">\n            </div>\n            <div class=\"form-group\">\n              <input class=\"fusionpbx_db_username form-control\" type=\"text\" name=\"fusionpbx_db_username\"\n                     placeholder=\"FusionPBX DB Username\">\n            </div>\n            <div class=\"form-group\">\n              <input class=\"fusionpbx_db_password form-control\" type=\"password\" name=\"fusionpbx_db_password\"\n                     placeholder=\"FusionPBX DB Password(Optional)\">\n            </div>\n          </div>\n        </div> <!-- end of fusionpbx tab -->\n\n        <div class=\"FreePBXDomainOptions form-group hidden\">\n          <div class=\"alert alert-warning pre-scrollable\">\n            <strong>\n              Run these commands as root on the FreePBX server.\n              Replace values within angle brackets '&lt;value&gt;' with your own values.\n            </strong>\n            <pre><code class=\"bash\">DSIPROUTER_IP={{ dsiprouter_ip }}\nPBX_SIP_PORTS=(5060 5080)\nfor PORT in ${PBX_SIP_PORTS[@]}; do\n    iptables -A INPUT -p udp -s $DSIPROUTER_IP/32 --dport $PORT -j ACCEPT\ndone\niptables-save &gt;/etc/iptables/rules.v4\n# Run this command if your using fail2ban\nsed -i -r \"s|(ignoreip = .*)|\\1 $DSIPROUTER_IP/32|\" /etc/fail2ban/jail.conf\nsystemctl restart fail2ban</code></pre>\n          </div>\n        </div> <!-- end of freepbx tab -->\n      </div> <!-- end of tabcontent tab -->\n\n      <div class=\"modal-footer\">\n        <button type=\"submit\" id=\"updateButton\" class=\"btn btn-warning btn-lg\" style=\"width: 100%;\"><span\n            class=\"glyphicon glyphicon-ok-sign\"></span> Update\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n\n{% block add_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Add Endpoint Group Details</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form action=\"/endpointgroups\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n      <input class=\"gwid\" type=\"hidden\" name=\"gwid\">\n      <input class=\"gwgroup\" type=\"hidden\" name=\"gwgroup\">\n\n      <div class=\"form-group\">\n        <input class=\"name form-control\" type=\"text\" name=\"name\" placeholder=\"Friendly Name(Optional)\"\n               autofocus=\"autofocus\">\n      </div>\n\n      <!-- <div class=\"form-group\">\n        <input class=\"ip_addr form-control\" type=\"text\" name=\"ip_addr\" placeholder=\"IP Address\">\n      </div> -->\n\n      <!-- nav tabs -->\n      <div id=\"endpoint-nav\" class=\"navbar\">\n        <ul class=\"nav nav-tabs\">\n          <li role=\"presentation\" class=\"active\">\n            <a href=\"#auth2\" name=\"auth-toggle\" data-toggle=\"tab\">Auth</a>\n          </li>\n          <li role=\"presentation\">\n            <a href=\"#endpoints2\" name=\"endpoints-toggle\" data-toggle=\"tab\">Endpoints</a>\n          </li>\n          <li role=\"presentation\">\n            <a href=\"#call_settings2\" name=\"settings-toggle\" data-toggle=\"tab\">Config</a>\n          </li>\n          <li role=\"presentation\">\n            <a href=\"#notifications2\" name=\"notifications-toggle\" data-toggle=\"tab\">Notifications</a>\n          </li>\n          <li role=\"presentation\">\n            <a href=\"#cdr2\" name=\"cdr-toggle\" data-toggle=\"tab\">CDR</a>\n          </li>\n          <li role=\"presentation\">\n            <a href=\"#fusionpbx2\" name=\"fusion-toggle\" data-toggle=\"tab\">FusionPBX</a>\n          </li>\n        </ul>\n      </div>\n\n      <div class=\"tab-content\"> <!-- begin tab content -->\n\n        <div id=\"auth2\" class=\"tab-pane fade in active\" name=\"auth-toggle\">\n          <div class=\"form-group\">\n            <div id=\"authoptions\" class=\"btn-group\" data-toggle=\"buttons\">\n              <label><input class=\"authtype\" type=\"radio\" name=\"authtype\" id=\"ip\" value=\"ip\" checked> IP\n                Auth</label>\n              <label><input class=\"authtype\" type=\"radio\" name=\"authtype\" id=\"userpwd\" value=\"userpwd\">\n                Username/Password\n                Auth</label>\n            </div>\n          </div>\n\n          <div id=\"userpwd_enabled\" class=\"userpwd\">\n            <p>Please enter a username and password for the PBX/Endpoint you want to register to. Specify domain if\n              different than the default domain: <b>{{ DEFAULT_auth_domain }}</b></p>\n            <div class=\"form-group\">\n              <input class=\"auth_username form-control\" type=\"text\" name=\"auth_username\"\n                     placeholder=\"Auth Username\">\n            </div>\n            <div class=\"form-group wrapper-fieldicon-right\">\n              <input id=\"auth_password\" class=\"form-control\" type=\"password\" name=\"auth_password\"\n                     placeholder=\"Auth Password\">\n              <span toggle=\"#auth_password\" class=\"field-icon toggle-password glyphicon glyphicon-eye-close\"></span>\n            </div>\n            <div class=\"form-group\">\n              <input class=\"auth_domain form-control\" typead=\"text\" name=\"auth_domain\"\n                     placeholder=\"Auth Domain (optional)\">\n            </div>\n          </div>\n        </div> <!-- end of auth tab -->\n\n        <div id=\"endpoints2\" class=\"tab-pane fade in\" name=\"endpoints-toggle\">\n          <table id=\"endpoint-table2\" class=\"table\">\n            <thead>\n            <tr>\n              <th>ID</th>\n              <th>\n                <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"\"\n                      data-original-title=\"hostname or IP address to route to (and/or) authenticate endpoint\">\n                  Host\n                </span>\n              </th>\n              <th>\n                <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"\"\n                      data-original-title=\"port to use when routing to endpoint\">\n                  Port\n                </span>\n              </th>\n              <th>\n                <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"\"\n                      data-original-title=\"signalling transport for calls to endpoint\">\n                  Signalling\n                </span>\n              </th>\n              <th>\n                <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"\"\n                      data-original-title=\"media transport for calls to endpoint\">\n                  Media\n                </span>\n              </th>\n              <th>Description</th>\n              <th>\n                <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"\"\n                      data-original-title=\"relative weight (load balancing only) out of sum of endpoint weights in this group.\n                      value must be 1-100 or 0 to disable load balancing on this endpoint\">\n                  Weight\n                </span>\n              </th>\n              <th>\n                <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"\"\n                      data-original-title=\"when enabled, the gateway is sent keepalive messages (at a default interval of 60 seconds).\n                      if disabled, the destination is always attempted when routing\">\n                  Keepalive\n                </span>\n              </th>\n              <th>\n                <button class=\"btn btn-success btn-md\" type=\"button\" id=\"addEndpointRow\">Add Row</button>\n              </th>\n            </tr>\n            </thead>\n            <tbody id=\"endpoint-tablebody2\">\n\n            </tbody>\n          </table>\n        </div> <!-- enf of endpoints tab -->\n\n        <div id=\"call_settings2\" class=\"tab-pane fade in\" name=\"settings-toggle\">\n          <div class=\"form-group\">\n            <input class=\"strip form-control\" type=\"text\" name=\"strip\"\n                   placeholder=\"# of characters to strip from RURI (inbound calls)\">\n          </div>\n          <div class=\"form-group\">\n            <input class=\"prefix form-control\" type=\"text\" name=\"prefix\"\n                   placeholder=\"The characters to prefix to a RURI (inbound calls)\">\n          </div>\n          <div class=\"form-group\">\n            <input class=\"call_limit form-control\" type=\"number\" name=\"call_limit\" placeholder=\"Max Concurrent Calls\"\n                   autocomplete=\"off\" min=\"0\">\n          </div>\n          <div class=\"form-group\">\n            <input class=\"call_timeout form-control\" type=\"number\" name=\"call_timeout\" placeholder=\"Maximum Call Length (seconds)\"\n                   autocomplete=\"off\" min=\"0\">\n          </div>\n        </div> <!-- end of call settings tab -->\n\n        <div id=\"notifications2\" class=\"tab-pane fade in\" name=\"notifications-toggle\">\n          <div class=\"form-group\">\n            <input class=\"email_over_max_calls form-control\" type=\"text\" name=\"email_over_max_calls\"\n                   placeholder=\"Email for Over Max Concurrent Calls\">\n          </div>\n          <div class=\"form-group\">\n            <input class=\"email_endpoint_failure form-control\" type=\"text\" name=\"email_endpoint_failure\"\n                   placeholder=\"Email for Endpont Failure\">\n          </div>\n        </div> <!-- end of notifications tab -->\n\n        <div id=\"cdr2\" class=\"tab-pane fade in\" name=\"cdr-toggle\">\n          <div class=\"form-group\">\n            <input class=\"cdr_email form-control\" type=\"text\" name=\"cdr_email\"\n                   placeholder=\"Email to send CDR's\">\n          </div>\n          <!-- CDR report interval chooser, based on crontab design -->\n          <!-- TODO: make this a combobox -->\n          <div class=\"form-group\">\n            <label>CDR Report Interval (minute, hour, day, month, weekday)</label>\n            <div class=\"wrapper-horizontal centered\">\n              <input class=\"cdr_send_minute form-control\" type=\"text\" name=\"cdr_send_minute\" value=\"*\"\n                     title=\"Minute of the hour (0-59) or (* is any minute)\"\n                     pattern=\"\\*(/([0-5]?[0-9]))?|([0-5]?[0-9])(,[0-5]?[0-9])*\">\n              <input class=\"cdr_send_hour form-control\" type=\"text\" name=\"cdr_send_hour\" value=\"*\"\n                     title=\"Hour of the day (0-23) or (* is any hour)\"\n                     pattern=\"\\*(/(2[0-3]|1[0-9]|[0-9]))?|(2[0-3]|1[0-9]|[0-9])(,(2[0-3]|1[0-9]|[0-9]))*\">\n              <input class=\"cdr_send_day form-control\" type=\"text\" name=\"cdr_send_day\" value=\"1\"\n                     title=\"Day of the month (1-31) or (* is any day)\"\n                     pattern=\"\\*(/(3[0-1]|[1-2][0-9]|[1-9]))?|(3[0-1]|[1-2][0-9]|[1-9])(,(3[0-1]|[1-2][0-9]|[1-9]))*\">\n              <input class=\"cdr_send_month form-control\" type=\"text\" name=\"cdr_send_month\" value=\"*\"\n                     title=\"Month of the year (1-12) or (* is any month) or (jan-dec)\"\n                     pattern=\"\\*(/(1[0-2]|[1-9]))?|(1[0-2]|[1-9])(,(1[0-2]|[1-9]))*|(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\">\n              <input class=\"cdr_send_weekday form-control\" type=\"text\" name=\"cdr_send_weekday\" value=\"*\"\n                     title=\"Day of the week (0-6) or (* is any weekday) or (sun-sat)\"\n                     pattern=\"\\*(/([0-6]))?|([0-6])(,[0-6])*|(sun|mon|tue|wed|thur|fri|sat)\">\n            </div>\n          </div>\n        </div> <!-- end of cdr  tab -->\n\n        <div id=\"fusionpbx2\" class=\"tab-pane fade in\" name=\"fusion-toggle\">\n          <div class=\"form-group\">\n            <div class=\"checkbox-inline\">\n              <label class=\"label-toggle\">\n                <input class=\"toggleFusionPBXDomain\" type=\"checkbox\" data-toggle=\"toggle\" value=\"1\"\n                       data-on=\"<span class='icon-fusionpbx'></span> Enabled\"\n                       data-off=\"<span class='icon-fusionpbx'></span> Disabled\"\n                       data-width=\"125px\">\n                FusionPBX Domain Support\n              </label>\n            </div>\n            <div class=\"checkbox-inline\">\n              <label class=\"checkbox-inline\">\n                <input type=\"checkbox\" class='fusionpbx_clustersupport' name=\"fusionpbx_clustersupport\" value=\"0\">Cluster\n                Support\n              </label>\n            </div>\n          </div>\n          <input class=\"fusionpbx_db_enabled \" type=\"hidden\" name=\"fusionpbx_db_enabled\" value=\"0\">\n          <div class=\"FusionPBXDomainOptions form-group hidden\">\n            <div class=\"alert alert-warning pre-scrollable\">\n              <strong>\n                You need access to the FusionPBX database. Run these commands as root on the FusionPBX server.\n              </strong>\n              <pre><code class=\"bash\">DSIPROUTER_IP={{ dsiprouter_ip }}\nsed  -i \"s/#listen_addresses = 'localhost'/listen_addresses = '*'/\"  /etc/postgresql/*/main/postgresql.conf\niptables -A INPUT -p tcp -s $DSIPROUTER_IP/32 --dport 5432 -j ACCEPT\niptables-save &gt;/etc/iptables/rules.v4\n# Run this command if your don't want to enter a password for the FusionPBX Database(DB) Password\necho -e \"host    all             all            $DSIPROUTER_IP/32            trust\" &gt;&gt;/etc/postgresql/*/main/pg_hba.conf\nsystemctl restart postgresql\n# Run this command if your using fail2ban\nsed -i -r \"s|^#?(ignoreip = .*)|\\1 $DSIPROUTER_IP/32|\" /etc/fail2ban/jail.conf\nsystemctl restart fail2ban</code></pre>\n            </div>\n            <div class=\"form-group\">\n              <input class=\"fusionpbx_db_server form-control\" type=\"text\" name=\"fusionpbx_db_server\"\n                     placeholder=\"FusionPBX Database IP or Hostname\">\n            </div>\n            <div class=\"form-group\">\n              <input class=\"fusionpbx_db_username form-control\" type=\"text\" name=\"fusionpbx_db_username\"\n                     placeholder=\"FusionPBX DB Username\">\n            </div>\n            <div class=\"form-group\">\n              <input class=\"fusionpbx_db_password form-control\" type=\"password\" name=\"fusionpbx_db_password\"\n                     placeholder=\"FusionPBX DB Password(Optional)\">\n            </div>\n          </div>\n        </div> <!-- end of fusionpbx tab -->\n\n        <div class=\"FreePBXDomainOptions form-group hidden\">\n          <div class=\"alert alert-warning pre-scrollable\">\n            <strong>\n              Run these commands as root on the FreePBX server.\n              Replace PBX_SIP_PORTS with your own values if they are not set to the defaults.\n            </strong>\n            <pre><code class=\"bash\">DSIPROUTER_IP={{ dsiprouter_ip }}\nPBX_SIP_PORTS=(5060 5080)\nfor PORT in ${PBX_SIP_PORTS[@]}; do\n    iptables -A INPUT -p udp -s $DSIPROUTER_IP/32 --dport $PORT -j ACCEPT\ndone\niptables-save &gt;/etc/iptables/rules.v4\n# Run this command if your using fail2ban\nsed -i -r \"s|(ignoreip = .*)|\\1 $DSIPROUTER_IP/32|\" /etc/fail2ban/jail.conf\nsystemctl restart fail2ban</code></pre>\n          </div>\n        </div> <!-- end of freepbx tab -->\n      </div> <!-- end of tabcontent tab -->\n\n      <div class=\"modal-footer \">\n        <button type=\"submit\" id=\"addButton\" class=\"btn btn-success btn-lg\" style=\"width: 100%;\"><span\n            class=\"glyphicon glyphicon-ok-sign\"></span> Add\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n\n{% block delete_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Delete this entry</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form action=\"/pbxdelete\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n\n      <div class=\"form-group\">\n        <input class=\"gwgroup form-control\" type=\"hidden\" name=\"gwgroup\">\n        <input class=\"gwid form-control\" type=\"hidden\" name=\"gwid\">\n        <input class=\"name form-control\" type=\"hidden\" name=\"name\">\n      </div>\n\n      <div class=\"alert alert-danger\"><span class=\"glyphicon glyphicon-warning-sign\"></span> Are you sure you want\n        to delete this Endpoint Group?\n      </div>\n\n      <div class=\"modal-footer \">\n        <button id=\"deleteButton\" type=\"button\" class=\"btn btn-success\"><span\n            class=\"glyphicon glyphicon-ok-sign\" autofocus=\"autofocus\"></span> Yes\n        </button>\n        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\"><span\n            class=\"glyphicon glyphicon-remove\"></span> No\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n{% block custom_js %}\n  {{ script_tag('jquery.tabledit') }}\n  {{ script_tag('endpointgroups') }}\n{% endblock %}\n\n"
  },
  {
    "path": "gui/templates/error.html",
    "content": "{% extends 'fullwidth_layout.html' %}\n\n{% block title %}Error Page{% endblock %}\n\n{% block custom_css %}\n{% endblock %}\n\n{% block body %}\n\n  {% set github_url = 'https://github.com/dOpensource/dsiprouter' %}\n  {% set github_issues_url = github_url + '/issues' %}\n  {% set dopensource_url = 'https://dopensource.com' %}\n  {% set dopensource_shop_url = dopensource_url + '/product-category/dsiprouter/' %}\n\n  <div class=\"wrapper-vertical centered\">\n\n    {% if type==\"db\" %}\n\n      <div class=\"alert alert-danger container\">\n        <h2><strong>DB Issue</strong></h2>\n      </div>\n\n      {% if msg is not none %}\n        <div class=\"alert alert-info container\" style=\"padding: 10px 10px\">\n          <p>Detailed Information: <b>{{ msg }}</b></p>\n        </div>\n      {% endif %}\n\n      <br>\n\n      <div class=\"alert container\">\n        <p>Please check your database and try the request again!</p>\n        <p>Check out the <a href=\"{{ github_url }}\">dSIPRouter Github</a> page for more information about the platform.\n        </p>\n      </div>\n\n    {% elif type==\"http\" %}\n\n      <div class=\"alert alert-danger container\">\n        <h2><strong>HTTP Issue</strong></h2>\n      </div>\n\n      {% if msg is not none %}\n        <div class=\"alert alert-info container\" style=\"padding: 10px 10px\">\n          <p>Detailed Information: <b>{{ msg }}</b></p>\n        </div>\n      {% endif %}\n\n      <br>\n\n      <div class=\"alert container\">\n        <p>Please check your connection and try the request again!</p>\n        <p>Check out the <a href=\"{{ github_url }}\">dSIPRouter Github</a> page for more information about the platform.\n        </p>\n      </div>\n\n    {% elif type==\"server\" %}\n\n      <div class=\"alert alert-danger container\">\n        <h2><strong>Server Issue</strong></h2>\n      </div>\n\n      {% if msg is not none %}\n        <div class=\"alert alert-info container\" style=\"padding: 10px 10px\">\n          <p>Detailed Information: <b>{{ msg }}</b></p>\n        </div>\n      {% endif %}\n\n      <br>\n\n      <div class=\"alert container\">\n        <p>Please check your configuration and try the request again!</p>\n        <p>Check out the <a href=\"{{ github_url }}\">dSIPRouter Github</a> page for more information about the platform.\n        </p>\n        <p>To submit a bug for review go to the <a href=\"{{ github_issues_url }}\">dSIPRouter Issues</a> page and submit\n          a new\n          issue with a <strong>detailed</strong> explanation and <strong>screenshots</strong> of the issue.</p>\n      </div>\n\n    {% elif type==\"woocommerce\" %}\n\n      <div class=\"alert alert-danger container\">\n        <h2><strong>Licensing Issue</strong></h2>\n      </div>\n\n      {% if msg is not none %}\n        <div class=\"alert alert-info container\" style=\"padding: 10px 10px\">\n          <p>Detailed Information: <b>{{ msg }}</b></p>\n        </div>\n      {% endif %}\n\n      <br>\n\n      <div class=\"alert container\">\n        <p>Double check your license is still valid.</p>\n        <p>To renew your license go to the <a href=\"{{ dopensource_shop_url }}\">dopensource shop</a>.</p>\n        <p>If your license was associated with a previous machine you have to remove it first.</p>\n        <p>If you purchased a license and are continuing to have issues please <a href=\"https://support.dopensource.com/\">open a ticket</a> for support.</p>\n      </div>\n\n    {% else %}\n\n      <div class=\"alert alert-danger container\">\n        <h2><strong>Unknown Issue</strong></h2>\n      </div>\n\n      {% if msg is not none %}\n        <div class=\"alert alert-info container\" style=\"padding: 10px 10px\">\n          <p>Detailed Information: <b>{{ msg }}</b></p>\n        </div>\n      {% endif %}\n\n      <br>\n\n      <div class=\"alert container\">\n        <p>Please check your request and try it again!</p>\n        <p>Check out the <a href=\"{{ github_url }}\">dSIPRouter Github</a> page for more information about the platform.\n        </p>\n        <p>To submit a bug for review go to the <a href=\"{{ github_issues_url }}\">dSIPRouter Issues</a> page and submit\n          a new\n          issue with a <strong>detailed</strong> explanation and <strong>screenshots</strong> of the issue.</p>\n      </div>\n\n    {% endif %}\n\n  </div>\n\n{% endblock %}\n\n{% block custom_js %}\n  <script type=\"application/javascript\">\n    /* fix section padding */\n    $('.wrap .content .content-inner').css({\n      'padding': 0\n    });\n    /* fix alert text color */\n    $('.alert > *').css({\n      'color': '#000000'\n    });\n  </script>\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/fullwidth_layout.html",
    "content": "<!DOCTYPE html>\n{% from 'util.jinja2.html' import link_tag, script_tag, tracked_link, img_tag %}\n<html lang=\"en\">\n\n<head>\n\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n  <title>dSIPRouter {% block title %}{% endblock %}</title>\n\n  <!-- inherited CSS -->\n  {{ link_tag('bootstrap') }}\n  {{ link_tag('bootstrap-theme') }}\n  {{ link_tag('bootstrap-toggle') }}\n  {{ link_tag('datatables.min') }}\n  {{ link_tag('highlight/{}'.format(highlight_theme|default('github'))) }}\n  {{ link_tag('main') }}\n\n  <!-- custom CSS -->\n  {% block custom_css %}\n  {% endblock %}\n\n</head>\n\n<body>\n\n  <div class=\"container\">\n    <div class=\"wrap\">\n      <nav class=\"nav-bar navbar-inverse\" role=\"navigation\">\n        <div id=\"top-menu\" class=\"container-fluid active\">\n          <a class=\"navbar-brand\" href=\"http://dopensource.com/dsiprouter\"><img\n              src=\"{{ url_for('static', filename='images/dsiprouter_x50px.png') }}\"></a>\n          <ul class=\"nav navbar-nav navbar-right\">\n            <div class=\"btn-group\" style=\"margin-right: 0.5em\">\n              {% if state.kam_reload_required == True or state.dsip_reload_required == True %}\n                <button type=\"button\" class=\"btn btn-warning\" id=\"reload\">Reload</button>\n                <button type=\"button\" class=\"btn btn-warning dropdown-toggle\" id=\"reload-split\" data-toggle=\"dropdown\"\n                        aria-haspopup=\"true\" aria-expanded=\"false\">\n              {% else %}\n                <button type=\"button\" class=\"btn btn-primary\" id=\"reload\">Reload</button>\n                <button type=\"button\" class=\"btn btn-primary dropdown-toggle\" id=\"reload-split\" data-toggle=\"dropdown\"\n                        aria-haspopup=\"true\" aria-expanded=\"false\">\n              {% endif %}\n              <span class=\"caret\"></span>\n              <span class=\"sr-only\">Toggle Dropdown</span>\n              </button>\n              <ul class=\"dropdown-menu\" role=\"menu\">\n                {% if state.kam_reload_required == True %}\n                  {% set kam_reload_state=\"btn-warning\" %}\n                {% else %}\n                  {% set kam_reload_state=\"btn-secondary\" %}\n                {% endif %}\n                {% if state.dsip_reload_required == True %}\n                  {% set dsip_reload_state=\"btn-warning\" %}\n                {% else %}\n                  {% set dsip_reload_state=\"btn-secondary\" %}\n                {% endif %}\n                <li><a class=\"dropdown-item {{ kam_reload_state }}\" id=\"reload_kam\">Reload Kamailio</a></li>\n                <li><a class=\"dropdown-item {{ dsip_reload_state }}\" id=\"reload_dsip\">Reload dSIPRouter</a></li>\n              </ul>\n            </div>\n            <!-- <form id=\"qform\" class=\"navbar-form pull-left\" role=\"search\">\n               <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n               <input type=\"text\" class=\"form-control\" placeholder=\"Search\" />\n             </form> -->\n            <li class=\"dropdown movable\">\n              <a href=\"#\" class=\"dropdown-toggle\" data-toggle=\"dropdown\"><span class=\"caret\"></span><span\n                  class=\"fa fa-4x fa-child\"></span>{{ session.username }}</a>\n              <ul class=\"dropdown-menu\" role=\"menu\">\n                <!-- <li><a href=\"#\"><span class=\"fa fa-user\"></span>My Profile</a></li>\n                <li><a href=\"#\"><span class=\"fa fa-gear\"></span>Settings</a></li> -->\n                <li class=\"divider\"></li>\n                <li><a href=\"/logout\"><span class=\"fa fa-power-off\"></span>Logout</a></li>\n              </ul>\n            </li>\n          </ul>\n        </div>\n      </nav>\n\n      <aside id=\"side-menu\" class=\"aside\" role=\"navigation\">\n        <ul class=\"nav nav-list accordion\">\n\n          <li class=\"nav-header\">\n            <div class=\"link\">\n              <i class=\"fa fa-lg fa-globe\"></i>\n              <a class=\"navlink\" href=\"/\">Dashboard</a>\n              <i class=\"fa fa-chevron-down\"></i>\n            </div>\n          </li>\n\n          <li class=\"nav-header\">\n            <div class=\"link\">\n              <i class=\"fa fa-lg fa-users\"></i>\n              <a class=\"navlink\" href=\"/carriergroups\">Carrier Groups</a>\n              <i class=\"fa fa-chevron-down\"></i>\n            </div>\n          </li>\n\n          <li class=\"nav-header\">\n            <div class=\"link\">\n              <i class=\"fa fa-users\"></i>\n              <a class=\"navlink\" href=\"/endpointgroups\">Endpoint Groups</a>\n              <i class=\"fa fa-chevron-down\"></i>\n            </div>\n          </li>\n\n          <li class=\"nav-header\">\n            <div class=\"link\">\n              <i class=\"fa fa-cloud\"></i>\n              <a class=\"navlink\" href=\"/domains\">Domains</a>\n              <i class=\"fa fa-chevron-down\"></i>\n            </div>\n          </li>\n\n          <li class=\"nav-header\">\n            <div class=\"link\">\n              <i class=\"fa fa-lg fa-map-marker\"></i>\n              <a class=\"navlink\" href=\"/inboundmapping\">Inbound Routes</a>\n              <i class=\"fa fa-chevron-down\"></i>\n            </div>\n          </li>\n\n          <li class=\"nav-header\">\n            <div class=\"link\">\n              <i class=\"fa fa-lg fa-file-image-o\"></i>\n              <a class=\"navlink\" href=\"/outboundroutes\">Outbound Routes</a>\n              <i class=\"fa fa-chevron-down\"></i>\n            </div>\n          </li>\n          <!--\n          <li class=\"nav-header\">\n            <div class=\"link\">\n              <i class=\"fa fa-lg fa-file-image-o\"></i>\n              <a class=\"navlink\" href=\"/numberenrichment\">Number Enrichment</a>\n              <i class=\"fa fa-chevron-down\"></i>\n            </div>\n          </li>\n          -->\n          <li class=\"nav-header\">\n            <div class=\"link\">\n              <i class=\"fa fa-lg fa-file-image-o\"></i>\n              <a class=\"navlink\" href=\"/cdrs\">Call Detail Records</a>\n              <i class=\"fa fa-chevron-down\"></i>\n            </div>\n          </li>\n\n          <li class=\"nav-header\">\n            <div class=\"link\">\n              <i class=\"fa fa-lg fa-file-image-o\"></i>\n              <a class=\"navlink\" href=\"#\">System Settings</a>\n              <i class=\"glyphicon glyphicon-chevron-down\"></i>\n            </div>\n            <ul class=\"submenu\">\n              <li><a class=\"navlink\" href=\"/backupandrestore\">Backup and Restore</a></li>\n              <li><a href=\"/certificates\">Certificates</a></li>\n              <li><a href=\"/teleblock\">Teleblock</a></li>\n              <li><a href=\"/transnexus\">TransNexus</a></li>\n              <li><a href=\"/stirshaken\">STIR/SHAKEN</a></li>\n              <li><a href=\"/licensing\">License Manager</a></li>\n              <li><a href=\"/upgrade\">Upgrade</a></li>\n            </ul>\n          </li>\n\n        </ul>\n      </aside>\n\n      <!--Body content-->\n      <div class=\"content\">\n        <div class=\"top-bar\" style=\"display: none;\">\n          <div class=\"message-bar\" style=\"text-align: center;\"></div>\n          <a href=\"#menu\" class=\"side-menu-link burger\">\n            <span class='burger_inside' id='bgrOne'></span>\n            <span class='burger_inside' id='bgrTwo'></span>\n            <span class='burger_inside' id='bgrThree'></span>\n          </a>\n        </div>\n\n        <section class=\"content-inner\">\n          <div id=\"reloading_overlay\" class=\"hidden\"></div>\n          {% block body %}\n          {% endblock %}\n        </section>\n      </div>\n\n    </div>\n  </div>\n\n  <!-- inherited JS -->\n  {{ script_tag('jquery') }}\n  <script type=\"application/javascript\">\n    /* globals set in window properties */\n    Object.defineProperty(window, \"GUI_BASE_URL\", {\n      configurable: false,\n      writable: false,\n      value: \"{{ settings.DSIP_PROTO }}\" + \"://\" + window.location.hostname + \":\" + \"{{ settings.DSIP_PORT }}\" + \"/\"\n    });\n    Object.defineProperty(window, \"API_BASE_URL\", {\n      configurable: false,\n      writable: false,\n      value: \"{{ settings.DSIP_API_PROTO }}\" + \"://\" + window.location.hostname + \":\" + \"{{ settings.DSIP_API_PORT }}\" + \"/api/v1/\"\n    });\n  </script>\n  {{ script_tag('util') }}\n  <script type=\"application/javascript\">\n    {% include 'includes/overrides.js' %}\n  </script>\n  {{ script_tag('bootstrap') }}\n  {{ script_tag('bootstrap-toggle') }}\n  {{ script_tag('validator') }}\n  {{ script_tag('main') }}\n  {{ script_tag('datatables.min') }}\n  {{ script_tag('highlight/highlight.pack') }}\n  <script type=\"application/javascript\">\n    $(document).ready(function() {\n      /* add code syntax highlighting */\n      $('pre code').each(function(i, block) {\n        hljs.highlightBlock(block);\n      });\n    });\n  </script>\n\n  <!-- custom JS -->\n  {% block custom_js %}\n  {% endblock %}\n\n</body>\n</html>\n"
  },
  {
    "path": "gui/templates/inboundmapping.html",
    "content": "{% extends 'table_layout.html' %}\n\n{% block title %}Inbound Routes{% endblock %}\n\n{% block custom_css %}\n  {{ link_tag('combobox') }}\n{% endblock %}\n\n{% block table_headers %}\n  <div>\n    <h3>List of Inbound Routes</h3>\n  </div>\n\n  <div class=\"tableAddButton\">\n    <button id='open-Add' class='btn btn-success btn-md' data-title=\"Add\" data-toggle=\"modal\"\n            data-target=\"#add\">Add\n    </button>\n    <button id='open-DIDImport' class='btn btn-success btn-md' data-title=\"Import DID's\" data-toggle=\"modal\"\n            data-target=\"#import\">Import DID\n    </button>\n  </div>\n{% endblock %}\n\n\n{% block table %}\n\n  <table id=\"inboundmapping\" class=\"table table-striped table-centered\">\n\n    <thead>\n    <tr class='element-row'>\n      <th></th>\n      <th data-field=\"ruleid\">Rule ID</th>\n      <th data-field=\"prefix\">DID (or DID pattern)</th>\n      <th data-field=\"gwgroupid\" class=\"hidden\"></th>\n      <th data-field=\"gwgroupname\">Endpoint Group</th>\n      <th data-field=\"rulename\">Name</th>\n      <th data-field=\"gwlist\" class=\"hidden\">Gateway List</th>\n      <th data-field=\"lb_enabled\" class=\"hidden\"></th>\n      <th></th>\n      <th></th>\n      <th data-field=\"hf_ruleid\" class=\"hidden\"></th>\n      <th data-field=\"hf_groupid\" class=\"hidden\"></th>\n      <th data-field=\"hf_gwgroupid\" class=\"hidden\"></th>\n      <th data-field=\"hf_fwddid\" class=\"hidden\"></th>\n      <th data-field=\"ff_ruleid\" class=\"hidden\"></th>\n      <th data-field=\"ff_groupid\" class=\"hidden\"></th>\n      <th data-field=\"ff_gwgroupid\" class=\"hidden\"></th>\n      <th data-field=\"ff_fwddid\" class=\"hidden\"></th>\n    </tr>\n    </thead>\n    <tbody>\n    {% for row in rows %}\n      <tr class='element-row'>\n        <td><input type=\"checkbox\" class=\"checkthis\" value=\"1\"/></td>\n        <td class='ruleid'>{{ row.ruleid }}</td>\n        <td class='prefix'>{{ row.prefix }}</td>\n        <td class=\"gwgroupid hidden\">{{ row.gwgroupid }}</td>\n        {% if row.rule_description|attrFilter('lb_enabled') == \"1\" %}\n          <td class='gwgroupname'>{{ row.gwgroup_description|attrFilter('name') }} <small>(Load Balancing)</small></td>\n        {% else %}\n          {% if row.gwlist.split(',')|length > 1 %}\n            <td class='gwgroupname'>{{ row.gwgroup_description|attrFilter('name') }}<sup> +1</sup></td>\n          {% else %}\n            <td class='gwgroupname'>{{ row.gwgroup_description|attrFilter('name') }}</td>\n          {% endif %}\n        {% endif %}\n        <td class=\"rulename\">{{ row.rule_description|attrFilter('name') }}</td>\n        <td class=\"gwlist hidden\">{{ row.gwlist }}</td>\n        <td class=\"lb_enabled hidden\">{{ row.rule_description|attrFilter('lb_enabled') }}</td>\n        <td>\n          <p data-placement=\"top\" data-toggle=\"tooltip\" title=\"Edit\">\n            <button id=\"open-Update\" class=\"open-Update btn btn-primary btn-xs\" data-title=\"Edit\"\n                    data-toggle=\"modal\" data-target=\"#edit\"><span class=\"glyphicon glyphicon-pencil\"></span>\n            </button>\n          </p>\n        </td>\n        <td>\n          <p data-placement=\"top\" data-toggle=\"tooltip\" title=\"Delete\">\n            <button id=\"open-Delete\" class=\"open-Delete btn btn-danger btn-xs\" data-title=\"Delete\"\n                    data-toggle=\"modal\" data-target=\"#delete\"><span class=\"glyphicon glyphicon-trash\"></span>\n            </button>\n          </p>\n        </td>\n        <td class=\"hf_ruleid hidden\">{{ row.hf_ruleid|noneFilter() }}</td>\n        <td class=\"hf_groupid hidden\">{{ row.hf_groupid|noneFilter() }}</td>\n        <td class=\"hf_gwgroupid hidden\">{{ row.hf_gwgroupid|noneFilter() }}</td>\n        <td class=\"hf_fwddid hidden\">{{ row.hf_fwddid|noneFilter() }}</td>\n        <td class=\"ff_ruleid hidden\">{{ row.ff_ruleid|noneFilter() }}</td>\n        <td class=\"ff_groupid hidden\">{{ row.ff_groupid|noneFilter() }}</td>\n        <td class=\"ff_gwgroupid hidden\">{{ row.ff_gwgroupid|noneFilter() }}</td>\n        <td class=\"ff_fwddid hidden\">{{ row.ff_fwddid|noneFilter() }}</td>\n      </tr>\n    {% endfor %}\n    </tbody>\n  </table>\n\n{% endblock %}\n\n\n{% block edit_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Edit Your Inbound Route</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form action=\"/inboundmapping\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n      <input class=\"ruleid\" type=\"hidden\" name=\"ruleid\">\n      <input class=\"hf_ruleid\" type=\"hidden\" name=\"hf_ruleid\">\n      <input class=\"ff_ruleid\" type=\"hidden\" name=\"ff_ruleid\">\n      <input class=\"hf_groupid\" type=\"hidden\" name=\"hf_groupid\">\n      <input class=\"ff_groupid\" type=\"hidden\" name=\"ff_groupid\">\n\n      <div class=\"form-group\">\n        <input class=\"form-control rulename\" type=\"text\" name=\"rulename\" placeholder=\"Friendly Name (Optional)\"\n               autofocus=\"autofocus\">\n      </div>\n\n      <div class=\"form-group\">\n        {% if imported_dids|length > 0 %}\n          <div class=\"combobox-wrapper\">\n            <div class=\"did-combobox\" role=\"combobox\" aria-expanded=\"false\" aria-owns=\"did-listbox\"\n                 aria-haspopup=\"listbox\">\n              <input class=\"did-combobox-input prefix form-control\" type=\"text\" name=\"prefix\" placeholder=\"DID\"\n                     aria-autocomplete=\"both\" aria-controls=\"did-listbox\">\n              <div class=\"did-combobox-arrow combobox-dropdown wrapper-vertical\" tabindex=\"-1\" role=\"button\"\n                   aria-label=\"Toggle DIDs Shown\">\n                <span class=\"did-combobox-span icon-circle-down centered\"></span>\n              </div>\n            </div>\n            <ul class=\"did-listbox listbox hidden\" role=\"listbox\"></ul>\n          </div>\n        {% else %}\n          <input class=\"prefix form-control\" type=\"text\" name=\"prefix\" placeholder=\"DID\">\n        {% endif %}\n      </div>\n\n      <div class=\"form-group\">\n        <select class=\"gwgroupid form-control\" name=\"gwgroupid\" title=\"gwgroupid\" required=\"required\">\n          <option class=\"hidden\" value=\"\" selected disabled>Endpoint Group</option>\n          {% for epgroup in epgroups %}\n            <option value=\"{{ epgroup['id'] }}\">{{ epgroup['description']|attrFilter('name') }}</option>\n            {% if epgroup['description']|attrFilter('lb') %}\n            <option value=\"lb_{{ epgroup['id'] }}_{{ epgroup['description']|attrFilter('lb') }}\">{{ epgroup['description']|attrFilter('name') }} LB</option>\n            {% endif %}\n            {% if epgroup['description']|attrFilter('lb_ext') %}\n            <option value=\"lb_{{ epgroup['id'] }}_{{ epgroup['description']|attrFilter('lb_ext') }}\">{{ epgroup['description']|attrFilter('name') }} LB (External)</option>\n            {% endif %}\n          {% endfor %}\n        </select>\n      </div>\n\n\n      <div class=\"form-group\">\n        <div class=\"checkbox\">\n          <label class=\"label-toggle\">\n            <input class=\"toggle-hardfwd\" type=\"checkbox\" data-toggle=\"toggle\" value=\"1\"\n                   data-on=\"<span class='icon-call_hardfwd'></span> Enabled\"\n                   data-off=\"<span class='icon-call_hardfwd'></span> Disabled\"\n                   data-width=\"125px\">\n            Hard Forwarding\n          </label>\n        </div>\n        <input class=\"hardfwd_enabled\" type=\"hidden\" name=\"hardfwd_enabled\" value=\"0\">\n      </div>\n\n      <div class=\"hardfwd-options hidden\">\n        <div class=\"form-group\">\n          {% if imported_dids|length > 0 %}\n            <div class=\"combobox-wrapper\">\n              <div class=\"did-combobox\" role=\"combobox\" aria-expanded=\"false\" aria-owns=\"did-listbox\"\n                   aria-haspopup=\"listbox\">\n                <input class=\"did-combobox-input hf_fwddid form-control\" type=\"text\" name=\"hf_fwddid\"\n                       placeholder=\"Forwarded DID (default is unchanged)\" aria-autocomplete=\"both\"\n                       aria-controls=\"did-listbox\">\n                <div class=\"did-combobox-arrow combobox-dropdown wrapper-vertical\" tabindex=\"-1\" role=\"button\"\n                     aria-label=\"Toggle DIDs Shown\">\n                  <span class=\"did-combobox-span icon-circle-down centered\"></span>\n                </div>\n              </div>\n              <ul class=\"did-listbox listbox hidden\" role=\"listbox\"></ul>\n            </div>\n          {% else %}\n            <input class=\"hf_fwddid form-control\" type=\"text\" name=\"hf_fwddid\"\n                   placeholder=\"Forwarded DID (default is unchanged)\">\n          {% endif %}\n        </div>\n\n        <div class=\"form-group\">\n          <select class=\"hf_gwgroupid form-control\" name=\"hf_gwgroupid\" title=\"gwgroupid\">\n            <option value=\"\" selected=\"selected\">Carrier/Endpoint Group (default is route via DID)</option>\n            {% for gwgroup in gwgroups %}\n              <option value=\"{{ gwgroup['id'] }}\">{{ gwgroup['description']|attrFilter('name') }}</option>\n            {% endfor %}\n          </select>\n        </div>\n      </div>\n\n\n      <div class=\"form-group\">\n        <div class=\"checkbox\">\n          <label class=\"label-toggle\">\n            <input class=\"toggle-failfwd\" type=\"checkbox\" data-toggle=\"toggle\" value=\"1\"\n                   data-on=\"<span class='icon-call_failfwd'></span> Enabled\"\n                   data-off=\"<span class='icon-call_failfwd'></span> Disabled\"\n                   data-width=\"125px\">\n            Failover Forwarding\n          </label>\n        </div>\n        <input class=\"failfwd_enabled\" type=\"hidden\" name=\"failfwd_enabled\" value=\"0\">\n      </div>\n\n      <div class=\"failfwd-options hidden\">\n        <div class=\"form-group\">\n          {% if imported_dids|length > 0 %}\n            <div class=\"combobox-wrapper\">\n              <div class=\"did-combobox\" role=\"combobox\" aria-expanded=\"false\" aria-owns=\"did-listbox\"\n                   aria-haspopup=\"listbox\">\n                <input class=\"did-combobox-input ff_fwddid form-control\" type=\"text\" name=\"ff_fwddid\"\n                       placeholder=\"Forwarded DID (default is unchanged)\" aria-autocomplete=\"both\"\n                       aria-controls=\"did-listbox\">\n                <div class=\"did-combobox-arrow combobox-dropdown wrapper-vertical\" tabindex=\"-1\" role=\"button\"\n                     aria-label=\"Toggle DIDs Shown\">\n                  <span class=\"did-combobox-span icon-circle-down centered\"></span>\n                </div>\n              </div>\n              <ul class=\"did-listbox listbox hidden\" role=\"listbox\"></ul>\n            </div>\n          {% else %}\n            <input class=\"ff_fwddid form-control\" type=\"text\" name=\"ff_fwddid\"\n                   placeholder=\"Forwarded DID (default is unchanged)\">\n          {% endif %}\n        </div>\n\n        <div class=\"form-group\">\n          <select class=\"ff_gwgroupid form-control\" name=\"ff_gwgroupid\" title=\"gwgroupid\">\n            <option value=\"\" selected=\"selected\">Carrier/Endpoint Group (default is route via DID)</option>\n            {% for gwgroup in gwgroups %}\n              <option value=\"{{ gwgroup['id'] }}\">{{ gwgroup['description']|attrFilter('name') }}</option>\n            {% endfor %}\n          </select>\n        </div>\n      </div>\n\n      <div class=\"modal-footer \">\n        <button type=\"submit\" class=\"btn btn-warning btn-lg\" style=\"width: 100%;\"><span\n            class=\"glyphicon glyphicon-ok-sign\"></span> Update\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n\n{% block add_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Add New Inbound Route</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form action=\"/inboundmapping\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n      <input class=\"ruleid\" type=\"hidden\" name=\"ruleid\">\n\n      <div class=\"form-group\">\n        <input class=\"form-control rulename\" type=\"text\" name=\"rulename\" placeholder=\"Friendly Name (Optional)\"\n               autofocus=\"autofocus\">\n      </div>\n\n      <div class=\"form-group\">\n        {% if imported_dids|length > 0 %}\n          <div class=\"combobox-wrapper\">\n            <div class=\"did-combobox\" role=\"combobox\" aria-expanded=\"false\" aria-owns=\"did-listbox\"\n                 aria-haspopup=\"listbox\">\n              <input class=\"did-combobox-input prefix form-control\" type=\"text\" name=\"prefix\" placeholder=\"DID\"\n                     aria-autocomplete=\"both\" aria-controls=\"did-listbox\">\n              <div class=\"did-combobox-arrow combobox-dropdown wrapper-vertical\" tabindex=\"-1\" role=\"button\"\n                   aria-label=\"Toggle DIDs Shown\">\n                <span class=\"did-combobox-span icon-circle-down centered\"></span>\n              </div>\n            </div>\n            <ul class=\"did-listbox listbox hidden\" role=\"listbox\"></ul>\n          </div>\n        {% else %}\n          <input class=\"prefix form-control\" type=\"text\" name=\"prefix\" placeholder=\"DID\">\n        {% endif %}\n      </div>\n\n      <div class=\"form-group\">\n        <select class=\"gwgroupid form-control\" name=\"gwgroupid\" title=\"gwgroupid\" required=\"required\">\n          <option class=\"hidden\" value=\"\" selected disabled>Endpoint Group</option>\n          {% for epgroup in epgroups %}\n            <option value=\"{{ epgroup['id'] }}\">{{ epgroup['description']|attrFilter('name') }}</option>\n      \t    {% if epgroup['description']|attrFilter('lb') %}\n      \t    <option value=\"lb_{{ epgroup['id'] }}_{{ epgroup['description']|attrFilter('lb') }}\">{{ epgroup['description']|attrFilter('name') }} LB</option>\n      \t    {% endif %}\n      \t    {% if epgroup['description']|attrFilter('lb_ext') %}\n      \t    <option value=\"lb_{{ epgroup['id'] }}_{{ epgroup['description']|attrFilter('lb_ext') }}\">{{ epgroup['description']|attrFilter('name') }} LB (External)</option>\n      \t    {% endif %}\n          {% endfor %}\n        </select>\n      </div>\n\n\n      <div class=\"form-group\">\n        <div class=\"checkbox\">\n          <label class=\"label-toggle\">\n            <input class=\"toggle-hardfwd\" type=\"checkbox\" data-toggle=\"toggle\" value=\"1\"\n                   data-on=\"<span class='icon-call_hardfwd'></span> Enabled\"\n                   data-off=\"<span class='icon-call_hardfwd'></span> Disabled\"\n                   data-width=\"125px\">\n            Hard Forwarding\n          </label>\n        </div>\n        <input class=\"hardfwd_enabled\" type=\"hidden\" name=\"hardfwd_enabled\" value=\"0\">\n      </div>\n\n      <div class=\"hardfwd-options form-group hidden\">\n        <div class=\"form-group\">\n          {% if imported_dids|length > 0 %}\n            <div class=\"combobox-wrapper\">\n              <div class=\"did-combobox\" role=\"combobox\" aria-expanded=\"false\" aria-owns=\"did-listbox\"\n                   aria-haspopup=\"listbox\">\n                <input class=\"did-combobox-input hf_fwddid form-control\" type=\"text\" name=\"hf_fwddid\"\n                       placeholder=\"Forwarded DID (default is unchanged)\" aria-autocomplete=\"both\"\n                       aria-controls=\"did-listbox\">\n                <div class=\"did-combobox-arrow combobox-dropdown wrapper-vertical\" tabindex=\"-1\" role=\"button\"\n                     aria-label=\"Toggle DIDs Shown\">\n                  <span class=\"did-combobox-span icon-circle-down centered\"></span>\n                </div>\n              </div>\n              <ul class=\"did-listbox listbox hidden\" role=\"listbox\"></ul>\n            </div>\n          {% else %}\n            <input class=\"hf_fwddid form-control\" type=\"text\" name=\"hf_fwddid\"\n                   placeholder=\"Forwarded DID (default is unchanged)\">\n          {% endif %}\n        </div>\n\n        <div class=\"form-group\">\n          <select class=\"hf_gwgroupid form-control\" name=\"hf_gwgroupid\" title=\"gwgroupid\">\n            <option value=\"\" selected=\"selected\">Carrier/Endpoint Group (default is route via DID)</option>\n            {% for gwgroup in gwgroups %}\n              <option value=\"{{ gwgroup['id'] }}\">{{ gwgroup['description']|attrFilter('name') }}</option>\n            {% endfor %}\n          </select>\n        </div>\n      </div>\n\n\n      <div class=\"form-group\">\n        <div class=\"checkbox\">\n          <label class=\"label-toggle\">\n            <input class=\"toggle-failfwd\" type=\"checkbox\" data-toggle=\"toggle\" value=\"1\"\n                   data-on=\"<span class='icon-call_failfwd'></span> Enabled\"\n                   data-off=\"<span class='icon-call_failfwd'></span> Disabled\"\n                   data-width=\"125px\">\n            Failover Forwarding\n          </label>\n        </div>\n        <input class=\"failfwd_enabled\" type=\"hidden\" name=\"failfwd_enabled\" value=\"0\">\n      </div>\n\n      <div class=\"failfwd-options form-group hidden\">\n        <div class=\"form-group\">\n          {% if imported_dids|length > 0 %}\n            <div class=\"combobox-wrapper\">\n              <div class=\"did-combobox\" role=\"combobox\" aria-expanded=\"false\" aria-owns=\"did-listbox\"\n                   aria-haspopup=\"listbox\">\n                <input class=\"did-combobox-input ff_fwddid form-control\" type=\"text\" name=\"ff_fwddid\"\n                       placeholder=\"Forwarded DID (default is unchanged)\" aria-autocomplete=\"both\"\n                       aria-controls=\"did-listbox\">\n                <div class=\"did-combobox-arrow combobox-dropdown wrapper-vertical\" tabindex=\"-1\" role=\"button\"\n                     aria-label=\"Toggle DIDs Shown\">\n                  <span class=\"did-combobox-span icon-circle-down centered\"></span>\n                </div>\n              </div>\n              <ul class=\"did-listbox listbox hidden\" role=\"listbox\"></ul>\n            </div>\n          {% else %}\n            <input class=\"ff_fwddid form-control\" type=\"text\" name=\"ff_fwddid\"\n                   placeholder=\"Forwarded DID (default is unchanged)\">\n          {% endif %}\n        </div>\n\n        <div class=\"form-group\">\n          <select class=\"ff_gwgroupid form-control\" name=\"ff_gwgroupid\" title=\"gwgroupid\">\n            <option value=\"\" selected=\"selected\">Carrier/Endpoint Group (default is route via DID)</option>\n            {% for gwgroup in gwgroups %}\n              <option value=\"{{ gwgroup['id'] }}\">{{ gwgroup['description']|attrFilter('name') }}</option>\n            {% endfor %}\n          </select>\n        </div>\n      </div>\n\n      <div class=\"modal-footer \">\n        <button type=\"submit\" class=\"btn btn-success btn-lg\" style=\"width: 100%;\"><span\n            class=\"glyphicon glyphicon-ok-sign\"></span> Add\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n\n{% block delete_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Delete this entry</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form action=\"/inboundmappingdelete\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n      <input class=\"ruleid\" type=\"hidden\" name=\"ruleid\">\n      <input class=\"hf_ruleid\" type=\"hidden\" name=\"hf_ruleid\">\n      <input class=\"ff_ruleid\" type=\"hidden\" name=\"ff_ruleid\">\n      <input class=\"hf_groupid\" type=\"hidden\" name=\"hf_groupid\">\n      <input class=\"ff_groupid\" type=\"hidden\" name=\"ff_groupid\">\n\n      <div class=\"alert alert-danger\"><span class=\"glyphicon glyphicon-warning-sign\"></span> Are you sure you want\n        to delete this Record?\n      </div>\n\n      <div class=\"modal-footer \">\n        <button type=\"submit\" class=\"btn btn-success\"><span class=\"glyphicon glyphicon-ok-sign\"></span> Yes</button>\n        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\"><span\n            class=\"glyphicon glyphicon-remove\"></span> No\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n{% block import_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Import DID's</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form action=\"/inboundmappingimport\" method=\"POST\" role=\"form\" enctype=\"multipart/form-data\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n      <input class=\"ruleid\" type=\"hidden\" name=\"ruleid\">\n      <input class=\"hf_ruleid\" type=\"hidden\" name=\"hf_ruleid\">\n      <input class=\"ff_ruleid\" type=\"hidden\" name=\"ff_ruleid\">\n      <input class=\"hf_groupid\" type=\"hidden\" name=\"hf_groupid\">\n      <input class=\"ff_groupid\" type=\"hidden\" name=\"ff_groupid\">\n\n      <div class=\"form-group\">\n        <label for=\"importFile\">CSV File with DID's (<a href=\"/static/template/DID_example.csv\" target=\"_blank\">Download Example CSV</a>)</label>\n        <input type=\"file\" name=\"file\" class=\"form-control-file\" id=\"importFile\">\n      </div>\n\n      <div class=\"form-group\">\n        <select class=\"gwgroupid form-control\" name=\"gwgroupid\" title=\"gwgroupid\">\n          <option class=\"hidden\" value=\"\" selected disabled>Override Endpoint Group (Optional)</option>\n          {% for epgroup in epgroups %}\n            <option value=\"#{{ epgroup['id'] }}\">{{ epgroup['description']|attrFilter('name') }}</option>\n          {% endfor %}\n        </select>\n      </div>\n\n      <div class=\"modal-footer \">\n        <button type=\"submit\" class=\"btn btn-success btn-lg\" style=\"width: 100%;\"><span\n            class=\"glyphicon glyphicon-ok-sign\"></span> Import\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n{% block custom_js %}\n  {{ script_tag('combobox') }}\n  <script type=\"application/javascript\">\n    var DID_LIST = JSON.parse(\"{{ imported_dids }}\") || [];\n\n    var gw_mapping = [];\n    {% for gw in gatewayList %}\n      gw_mapping.push({\n        id: {{ gw['gwid'] }},\n        group: \"{{ gw['description']|attrFilter('gwgroup') }}\",\n        option_value: \"lb_{{ gw['description']|attrFilter('gwgroup') }}_{{ gw['description']|attrFilter('gwgroup')  }}\",\n      });\n    {% endfor %}\n  </script>\n  {{ script_tag('inboundmapping') }}\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/includes/overrides.js",
    "content": ";(function(window, document) {\n  'use strict';\n\n  // note that the ajax/fetch overrides do not effect XMLHttpRequest (a.k.a the XHR API)\n  // external libs using the XHR functions will not be effected (unknown side effects)\n  // in the future we may switch to overriding XMLHttpRequest instead of using ajax methods\n  // ref: https://stackoverflow.com/questions/14527360/can-i-set-a-global-header-for-all-ajax-requests\n  // ref: https://jsfiddle.net/cferdinandi/2mc2wnc7/\n  // ref: https://jsfiddle.net/0bjfLey9/1\n  // note that in the ajax implementation the global error handler runs AFTER any locally set error handlers\n  // the inverse is true for the fetch implementation, the global error handler runs BEFORE user catch blocks\n\n  // throw an error if required functions not defined\n  if (typeof showNotification === \"undefined\") {\n    throw new Error(\"showNotification() is required and is not defined\");\n  }\n  // if (typeof reloadKamRequired === \"undefined\") {\n  //   throw new Error(\"reloadKamRequired() is required and is not defined\");\n  // }\n\n  // throw an error if required globals not defined\n  if (typeof GUI_BASE_URL === \"undefined\") {\n    throw new Error(\"GUI_BASE_URL is required and is not defined\");\n  }\n\n  // global variables/constants for this script\n  const NOCSRF_REQUEST_METHOD_REGEX = new RegExp(/^(GET|HEAD|OPTIONS|TRACE)$/, 'i');\n  const OLD_FETCH = window.fetch;\n\n  function requestErrorHandler(status, error_msg, error_type=\"http\") {\n    if (status < 400) {\n      // not an error, likely a direct call to error handler\n    }\n    else if (status === 400) {\n      // bad input show error in page\n      console.error('requestErrorHandler(): ' + status.toString() + ' ' + error_msg)\n      showNotification(error_msg, true);\n    }\n    else if (status === 401) {\n      // unauthorized goto index for login\n      console.error('requestErrorHandler(): ' + status.toString() + ' ' + error_msg)\n      window.location.href = GUI_BASE_URL;\n    }\n    else if (status === 403) {\n      // forbidden show error in page\n      console.error('requestErrorHandler(): ' + status.toString() + ' ' + error_msg)\n      showNotification(error_msg, true);\n    }\n    else if (status === 404) {\n      // not found show error in page\n      console.error('requestErrorHandler(): ' + status.toString() + ' ' + error_msg)\n      showNotification(error_msg, true);\n    }\n    else {\n      // unhandled error goto error page\n      console.error('requestErrorHandler(): ' + status.toString() + ' ' + error_msg)\n      window.location.href = GUI_BASE_URL + \"error?type=\" + error_type + \"&code=\" +\n          status.toString() + \"&msg=\" + error_msg;\n    }\n  }\n  window.requestErrorHandler = requestErrorHandler;\n\n  // override ajax defaults\n  $.ajaxSetup({\n    // set anti-CSRF token\n    beforeSend: function(xhr, settings) {\n      if (!NOCSRF_REQUEST_METHOD_REGEX.test(settings.type) && !this.crossDomain) {\n        xhr.setRequestHeader(\"X-CSRF-Token\", \"{{ csrf_token() }}\");\n      }\n    }\n  });\n  /*\n      TODO: marked for review in v0.74\n      disabled the error handler / reload button handler\n      too hard to program with this abstraction\n  */\n  // $(document).ajaxError(function(event, xhr, settings, error_msg) {\n  //   // handle HTTP errors, may redirect\n  //   try {\n  //     // try updating error message and type\n  //     var data = JSON.parse(xhr.responseText);\n  //     var error_type = data[\"error\"];\n  //     error_msg = data[\"msg\"];\n  //     requestErrorHandler(xhr.status, error_msg, error_type);\n  //   }\n  //   catch(error) {\n  //     // non-JSON response or no error info in response, use defaults\n  //     requestErrorHandler(xhr.status, xhr.statusText);\n  //   }\n  // });\n  // $(document).ajaxComplete(function(event, xhr, settings) {\n  //   try {\n  //     // try updating kam reload button\n  //     reloadKamRequired(JSON.parse(xhr.responseText)[\"kamreload\"]);\n  //   }\n  //   catch(error) {\n  //     // non-JSON response or no kamreload in response, continue\n  //   }\n  // });\n\n  /*\n      TODO: marked for review in v0.74\n      disabled the error handler / reload button handler\n      too hard to program with this abstraction\n  */\n  // override fetch defaults\n  window.fetch = function(resource, init) {\n    // set anti-CSRF token\n    // if init is undefined then method is GET and no anti-CSRF token needed\n    if (init !== undefined && init.hasOwnProperty('method') && !NOCSRF_REQUEST_METHOD_REGEX.test(init.method)) {\n      if (!(init.hasOwnProperty('mode') && init.mode.toLowerCase() === 'cors')) {\n        init.headers = init.headers || {};\n        init.headers[\"X-CSRF-Token\"] = \"{{ csrf_token() }}\";\n      }\n    }\n    return OLD_FETCH.call(this, resource, init).then(function(response) {\n      // // handle HTTP errors, may redirect\n      // try {\n      //   var data = JSON.parse(response.text());\n      //   // try updating reload buttons\n      //   if (data.hasOwnProperty(\"kamreload\")) {\n      //     reloadKamRequired(data[\"kamreload\"]);\n      //   }\n      //   // try updating error message and type\n      //   var error_type = data[\"error\"];\n      //   var error_msg = data[\"msg\"];\n      //   requestErrorHandler(response.status, error_msg, error_type);\n      // }\n      // catch(error) {\n      //   // non-JSON response or no error info in response, use defaults\n      //   requestErrorHandler(response.status, response.statusText);\n      // }\n\n      // pass on the response\n      return response;\n    }).catch(function(error) {\n      // requestErrorHandler(500, error.message);\n      // if error handler doesn't redirect, reject the promise\n      return Promise.reject(error);\n    });\n  };\n\n  /*\n   * IE/Safari polyfill for reportValidity() compatibility\n   * credit: https://stackoverflow.com/questions/17550317/how-to-manually-show-a-html5-validation-message-from-a-javascript-function\n   */\n  if (!HTMLFormElement.prototype.reportValidity) {\n    HTMLFormElement.prototype.reportValidity = function() {\n      if (this.checkValidity()) return true;\n      var btn = document.createElement('button');\n      this.appendChild(btn);\n      btn.click();\n      this.removeChild(btn);\n      return false;\n    }\n    window.HTMLFormElement.prototype.reportValidity = HTMLFormElement.prototype.reportValidity;\n  }\n\n})(window, document);\n"
  },
  {
    "path": "gui/templates/index.html",
    "content": "{% extends 'login_layout.html' %}\n\n{% block title %}Login{% endblock %}\n\n{% block custom_css %}\n{% endblock %}\n\n{% block body %}\n  <div class=\"container\">\n    <div class=\"left-half\">\n      <img class=\"logon-image\" src=\"/static/images/login-splash.jpg\">\n    </div>\n\n    <div class=\"right-half\">\n      <div id=\"loginbox\" style=\"margin-top:100px;\" class=\"mainbox col-md-6 col-md-offset-3 col-sm-8 col-sm-offset-2\">\n        <div id=\"loginlogo\" class=\"loginlogo\">\n          <img style=\"padding:20px 20px;\" src=\"{{ url_for('static', filename='images/dsiprouter_x50px.png') }}\">\n        </div>\n\n        <div class=\"panel panel-info\">\n          <div style=\"padding-top:30px\" class=\"panel-body\">\n            {% with messages = get_flashed_messages() %}\n              {% if messages %}\n                <ul class=\"alert alert-danger col-sm-12\">\n                  {% for message in messages %}\n                    <li>{{ message }}</li>\n                  {% endfor %}\n                </ul>\n              {% endif %}\n            {% endwith %}\n\n            <div style=\"display:none\" id=\"login-alert\" class=\"alert alert-danger col-sm-12\"></div>\n\n            <form id=\"loginform\" class=\"form-horizontal\" role=\"form\" action=\"/login\" method=\"POST\">\n              <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n\n              <div style=\"margin-bottom: 25px\" class=\"input-group\">\n                <span class=\"input-group-addon\"><i class=\"glyphicon glyphicon-user\"></i></span>\n                <input id=\"login-username\" type=\"text\" class=\"form-control\" name=\"username\" value=\"\"\n                       placeholder=\"username or email\" autofocus=\"autofocus\">\n              </div>\n\n              <div style=\"margin-bottom: 25px\" class=\"input-group\">\n                <span class=\"input-group-addon\"><i class=\"glyphicon glyphicon-lock\"></i></span>\n                <input id=\"login-password\" type=\"password\" class=\"form-control\" name=\"password\" placeholder=\"password\">\n              </div>\n\n              <input id=\"nextpage\" type=\"hidden\" name=\"nextpage\" value=\"{{ nextpage }}\">\n              <div style=\"margin-top:10px\" class=\"form-group\">\n                <!-- Button -->\n\n                <div class=\"col-sm-12 controls\">\n                  <button class=\"btn btn-success\" type=\"submit\">Login</button>\n\n                </div>\n              </div>\n\n              <div style=\"border-top: 1px solid#888; padding-top:15px; font-size:85%\">\n                Built in Detroit | Version {{ version }}\n              </div>\n            </form>\n\n          </div>\n        </div>\n      </div>\n    </div> <!-- End of right side -->\n  </div>\n\n{% endblock %}\n\n{% block custom_js %}\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/license_manager.html",
    "content": "{% extends 'table_layout.html' %}\n\n{% block title %}License Manager{% endblock %}\n\n{% block custom_css %}\n  <style>\n    #licensing tr > th {\n      height: 2rem;\n    }\n\n    #licensing tr > td {\n      height: 4rem;\n    }\n\n    #licensing > tbody > tr:hover {\n      background-color: #acbad4;\n    }\n\n    #licensing > tbody > tr > td:has(.dt-resize-height) {\n      padding: 0;\n    }\n\n    #licensing > tbody > tr > td > .dt-resize-height {\n      display: -webkit-box;\n      display: -ms-flexbox;\n      display: flex;\n      -webkit-box-orient: vertical;\n      -webkit-box-direction: normal;\n      -ms-flex-direction: column;\n      flex-direction: column;\n    }\n\n    #licensing > tbody > tr > td > .dt-resize-height > * {\n      -webkit-box-flex: 1;\n      -ms-flex-positive: 1;\n      flex-grow: 1;\n    }\n\n    #licensing > tbody > tr > td > .dt-resize-height span.toggle-password {\n      position: absolute;\n      top: 1rem;\n      right: 1rem;\n      left: inherit;\n      bottom: inherit;\n      cursor: pointer;\n      line-height: inherit;\n    }\n\n    #licensing > tbody > tr > td > .dt-resize-height input {\n      background-color: rgba(0, 0, 0, 0);\n      border: 0;\n      text-align: center;\n      padding: 0;\n    }\n\n    #licensing > tbody > tr > td > .dt-resize-height input.key {\n      padding: 0 2em 0 0;\n      box-sizing: border-box;\n    }\n\n    #licensing > tbody > tr > td > .dt-resize-height textarea {\n      background-color: rgba(0, 0, 0, 0);\n      border: 0;\n      text-align: center;\n      padding: 0;\n      resize: none;\n    }\n\n    #licensing > tbody > tr > td > .dt-resize-height input:focus,\n    #licensing > tbody > tr > td > .dt-resize-height input:focus-visible {\n      outline: none;\n      -webkit-box-shadow: none;\n      box-shadow: none;\n    }\n\n    #licensing > tbody > tr > td > .dt-resize-height button {\n      margin: 1rem 2rem;\n    }\n  </style>\n{% endblock %}\n\n{% block table_headers %}\n  <div>\n    <h3>List of Licenses for this Node</h3>\n  </div>\n\n  <div class=\"tableAddButton\">\n    <div class=\"btn-group mr-2\">\n      <button id='open-LicenseAdd' class='btn btn-success btn-md' data-title=\"Activate\" data-toggle=\"modal\"\n              data-target=\"#add\">\n        Add\n      </button>\n    </div>\n  </div>\n{% endblock %}\n\n\n{% block table %}\n  <table id=\"licensing\" class=\"table table-striped table-centered\">\n    <thead>\n    <tr class='element-row'>\n      <th data-field=\"tags\">License Type</th>\n      <th data-field=\"key\">License Key</th>\n      <th data-field=\"active\">Active</th>\n      <th data-field=\"active\">Valid</th>\n      <th data-field=\"expires\">Expiration Date</th>\n      <th class=\"hidden\"></th>\n    </tr>\n    </thead>\n  </table>\n{% endblock %}\n\n\n{% block edit_modal %}\n\n{% endblock %}\n\n\n{% block add_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Activate License</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form action=\"/licensing\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n\n      <div class=\"form-group wrapper-fieldicon-right\">\n        <input id=\"add-key\" class=\"form-control key\" type=\"password\" name=\"key\"\n               placeholder=\"License Key\" autofocus=\"autofocus\">\n        <span class=\"field-icon toggle-password glyphicon glyphicon-eye-close\" data-toggle=\"#add-key\"></span>\n      </div>\n\n      <div class=\"modal-footer\">\n        <button type=\"submit\" id=\"addButton\" class=\"btn btn-success btn-lg\" style=\"width: 100%;\"><span\n            class=\"glyphicon glyphicon-ok-sign\"></span> Add\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n\n{% block delete_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Deactivate License</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form action=\"/licensing\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n      <input class=\"key\" type=\"hidden\" name=\"key\">\n\n      <div class=\"alert alert-danger\"><span class=\"glyphicon glyphicon-warning-sign\"></span>\n        Are you sure you want to deactivate this License?\n      </div>\n\n      <div class=\"modal-footer \">\n        <button id=\"deleteButton\" type=\"button\" class=\"btn btn-success\"><span\n            class=\"glyphicon glyphicon-ok-sign\" autofocus=\"autofocus\"></span> Yes\n        </button>\n        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\"><span\n            class=\"glyphicon glyphicon-remove\"></span> No\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n{% block custom_js %}\n  {{ script_tag('jquery.tabledit') }}\n  {{ script_tag('license_manager') }}\n{% endblock %}\n\n"
  },
  {
    "path": "gui/templates/license_required.html",
    "content": "{% extends 'fullwidth_layout.html' %}\n\n{% block title %}License Required{% endblock %}\n\n{% block custom_css %}\n{% endblock %}\n\n{% block body %}\n\n  {% set dopensource_url = 'https://dopensource.com' %}\n  {% set dsiprouter_licenses_url = dopensource_url + \"/product-category/dsiprouter/\" %}\n\n  <div class=\"wrapper-vertical centered\">\n    <div class=\"alert alert-danger container\">\n      <h2>Acesss Denied: License Required</h2>\n    </div>\n\n    {% if msg is not none %}\n      <div class=\"alert alert-info container\" style=\"padding: 10px 10px\">\n        <p>Detailed Information: <b>{{ msg }}</b></p>\n      </div>\n    {% endif %}\n\n    <br>\n\n    <div class=\"alert container\">\n      <p>A license is required to access this resource.</p>\n      <p>You can purchase licenses at <a href=\"{{ dsiprouter_licenses_url }}\">dopensource.com</a>.</p>\n      <p>Once purchased, activate the license key using the dSIPRouter <a href=\"/licensing\">license manager</a>.</p>\n    </div>\n  </div>\n\n{% endblock %}\n\n{% block custom_js %}\n  <script type=\"application/javascript\">\n    /* fix section padding */\n    $('.wrap .content .content-inner').css({\n      'padding': 0\n    });\n    /* fix alert text color */\n    $('.alert > *').css({\n      'color': '#000000'\n    });\n  </script>\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/login_layout.html",
    "content": "{% from 'util.jinja2.html' import link_tag, script_tag, tracked_link %}\n<!DOCTYPE html>\n<html lang=\"en\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n<head>\n  <meta charset=\"utf-8\">\n  <title>dSIPRouter {% block title %}{% endblock %}</title>\n\n  {{ link_tag('bootstrap') }}\n  {{ link_tag('main') }}\n\n  {% block custom_css %}\n  {% endblock %}\n\n</head>\n\n<body>\n\n  <div class=\"container\">\n    {% block body %}\n    {% endblock %}\n  </div>\n\n  {{ script_tag('jquery') }}\n\n  {% block custom_js %}\n  {% endblock %}\n\n</body>\n</html>\n"
  },
  {
    "path": "gui/templates/msteams.html",
    "content": "{% extends 'table_layout.html' %}\n\n{% block title %}MSteams Configuration{% endblock %}\n\n{% block custom_css %}\n  {{ link_tag('msteams') }}\n{% endblock %}\n\n\n{% block table_headers %}\n  <div>\n    <h3>Microsoft Direct Routing Configuration for <span\n        style=\"color:#1E8DB5;font-weight:bold\">{{ domain.domain }}</span></h3>\n  </div>\n\n  <div class=\"tableAddButton\">\n    <button id=\"testConnectivity\" class=\"btn btn-success btn-md\" value='{{ domain.domain }}'>\n      Test Connectivity\n    </button>\n  </div>\n{% endblock %}\n\n\n{% block table %}\n  <div class=\"container-fluid\">\n    <div class=\"col col-md-3\">\n      <div class=\"panel-group\" id=\"accordion\">\n        <div class=\"panel panel-default\">\n          <div class=\"panel-heading\">\n            <h4 class=\"panel-title\">\n              <a data-toggle=\"collapse\" data-parent=\"#accordion\" href=\"#collapse1\">\n                Connectivity Status</a>\n            </h4>\n          </div>\n          <div id=\"collapse1\" class=\"panel-collapse collapse in\">\n            <ul class=\"list-group\">\n              <li class=\"list-group-item\">Valid DNS Hostname <span id=\"hostname_check\"\n                                                                   class=\"glyphicon glyphicon-remove\"\n                                                                   style=\"color:gray\"></span></li>\n              <li id=\"tls_check_row\" class=\"list-group-item\">TLS Certificates <span id=\"tls_check\"\n                                                                                    class=\"glyphicon glyphicon-remove\"\n                                                                                    style=\"color:gray\"></span></li>\n              <li class=\"list-group-item\">Option Messages <span id=\"option_check\" class=\"glyphicon glyphicon-remove\"\n                                                                style=\"color:gray\"></span></li>\n\n            </ul>\n          </div>\n        </div>\n      </div>\n    </div>\n    <div class=\"col col-md-9 hidden\" id=\"configurationPanel\">\n\n    </div>\n  </div>\n{% endblock %}\n\n{% block custom_js %}\n  {{ script_tag('msteams') }}\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/outboundroutes.html",
    "content": "{% extends 'table_layout.html' %}\n\n{% block title %}Outbound Routes{% endblock %}\n\n{% block table_headers %}\n  <div>\n    <h3>List of Outbound Routes</h3>\n  </div>\n\n  <div class=\"tableAddButton btn-toolbar\">\n    <button id='open-CarrierAdd' class='btn btn-success btn-md' data-title=\"Add\" data-toggle=\"modal\"\n            data-target=\"#add\">Add\n    </button>\n  </div>\n{% endblock %}\n\n\n{% block table %}\n\n  <table id=\"outboundmapping\" class=\"table table-striped table-centered\">\n    <thead>\n    <tr class='element-row'>\n      <th></th>\n      <th data-field=\"ruleid\">Rule ID</th>\n      <th data-field=\"groupid\" class=\"hidden\">Group ID</th>\n      <th data-field=\"from_prefix\">From Prefix</th>\n      <th data-field=\"prefix\">To Prefix</th>\n      <th data-field=\"timerec\">Recurrence</th>\n      <th data-field=\"priority\">Priority</th>\n      <th data-field=\"routeid\">Custom Route</th>\n      <th data-field=\"gwgroupid\" class=\"hidden\"></th>\n      <th data-field=\"gwgroupname\">Carrier Group</th>\n      <th data-field=\"description\">Name</th>\n      <th></th>\n      <th></th>\n    </tr>\n    </thead>\n    <tbody>\n    {% for row in rows %}\n      <tr class='element-row'>\n        <td><input type=\"checkbox\" class=\"checkthis\" value=\"1\"/></td>\n        <td class='ruleid'>{{ row.ruleid }}</td>\n        <td class='groupid hidden'>{{ row.dr_groupid }}</td>\n        <td class='from_prefix'>{{ row.from_prefix|noneFilter() }}</td>\n        <td class='prefix'>{{ row.prefix }}</td>\n        <td class=\"timerec\">{{ row.timerec }}</td>\n        <td class=\"priority\">{{ row.priority }}</td>\n        <td class=\"routeid\">{{ row.routeid }}</td>\n        <td class=\"gwgroupid hidden\">{{ row.gwgroupid }}</td>\n        <td class='gwgroupname'>{{ row.gwgroup_description|attrFilter('name') }}</td>\n        <td class=\"description\">{{ row.description|attrFilter('name') }}</td>\n        <td>\n          <p data-placement=\"top\" data-toggle=\"tooltip\" title=\"Edit\">\n            <button id=\"open-Update\" class=\"open-Update btn btn-primary btn-xs\" data-title=\"Edit\"\n                    data-toggle=\"modal\" data-target=\"#edit\"><span class=\"glyphicon glyphicon-pencil\"></span>\n            </button>\n          </p>\n        </td>\n        <td>\n          <p data-placement=\"top\" data-toggle=\"tooltip\" title=\"Delete\">\n            <button id=\"open-Delete\" class=\"open-Delete btn btn-danger btn-xs\" data-title=\"Delete\"\n                    data-toggle=\"modal\" data-target=\"#delete\"><span class=\"glyphicon glyphicon-trash\"></span>\n            </button>\n          </p>\n        </td>\n      </tr>\n    {% endfor %}\n    </tbody>\n  </table>\n\n{% endblock %}\n\n\n{% block edit_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Edit Your Outbound Route</h4>\n  </div>\n\n\n  <div class=\"modal-body\">\n    <form action=\"/outboundroutes\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n      <input class=\"ruleid \" type=\"hidden\" name=\"ruleid\">\n      <input class=\"groupid \" type=\"hidden\" name=\"groupid\">\n\n      <div class=\"form-group\">\n        <input class=\"name form-control\" type=\"text\" name=\"name\" placeholder=\"Friendly Name (Optional)\"\n               autofocus=\"autofocus\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"from_prefix form-control\" type=\"text\" name=\"from_prefix\"\n               placeholder=\"From Prefix Matching (Optional)\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"prefix form-control\" type=\"text\" name=\"prefix\"\n               placeholder=\"To Prefix Matching (Optional)\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"timerec form-control\" type=\"text\" name=\"timerec\"\n               placeholder=\"Recurring Time (Optional)\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"priority form-control\" type=\"text\" name=\"priority\"\n               placeholder=\"Priority (Optional: higher priorities routed first)\">\n      </div>\n      <div class=\"form-group\">\n        <select class=\"routeid form-control\" name=\"routeid\" title=\"routeid\">\n          <option value=\"\" selected=\"selected\">Custom Kamailio Route (default none)</option>\n          {% for routeid in custom_routes %}\n            <option value='{{ routeid }}'>{{ routeid }}</option>\n          {% endfor %}\n        </select>\n      </div>\n      <div class=\"form-group\">\n        <select class=\"gwgroupid form-control\" name=\"gwgroupid\" title=\"gwgroupid\" required=\"required\">\n          <option class=\"hidden\" value=\"\" selected=\"selected\" disabled>Carrier Group</option>\n          {% for cgroup in cgroups %}\n            <option value=\"{{ cgroup['id'] }}\">{{ cgroup['description']|attrFilter('name') }}</option>\n          {% endfor %}\n        </select>\n      </div>\n\n      <div class=\"modal-footer \">\n        <button type=\"submit\" class=\"btn btn-warning btn-lg\" style=\"width: 100%;\"><span\n            class=\"glyphicon glyphicon-ok-sign\"></span> Update\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n\n{% block add_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Add an Outbound Route</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form id=\"addOutboundRoutes\" action=\"/outboundroutes\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n      <input class=\"ruleid \" type=\"hidden\" name=\"ruleid\">\n\n      <div class=\"form-group\">\n        <input class=\"name form-control\" type=\"text\" name=\"name\" placeholder=\"Friendly Name (Optional)\"\n               autofocus=\"autofocus\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"from_prefix form-control\" type=\"text\" name=\"from_prefix\"\n               placeholder=\"From Prefix Matching (Optional)\" data-custom=\"tocheck\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"prefix form-control\" type=\"text\" name=\"prefix\"\n               placeholder=\"To Prefix Matching (Optional)\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"timerec form-control\" type=\"text\" name=\"timerec\"\n               placeholder=\"Recurring Time (Optional)\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"priority form-control\" type=\"text\" name=\"priority\"\n               placeholder=\"Priority (Optional: higher priorities routed first)\">\n      </div>\n      <div class=\"form-group\">\n        <select class=\"routeid form-control\" name=\"routeid\" title=\"routeid\">\n          <option value=\"\" selected=\"selected\">Custom Kamailio Route (default none)</option>\n          {% for routeid in custom_routes %}\n            <option value='{{ routeid }}'>{{ routeid }}</option>\n          {% endfor %}\n        </select>\n      </div>\n      <div class=\"form-group\">\n        <select class=\"gwgroupid form-control\" name=\"gwgroupid\" title=\"gwgroupid\" required=\"required\">\n          <option class=\"hidden\" value=\"\" selected=\"selected\" disabled>Carrier Group</option>\n          {% for cgroup in cgroups %}\n            <option value=\"{{ cgroup['id'] }}\">{{ cgroup['description']|attrFilter('name') }}</option>\n          {% endfor %}\n        </select>\n        <div class=\"help-block with-errors\"></div>\n      </div>\n\n      <div class=\"modal-footer \">\n        <button type=\"submit\" class=\"btn btn-success btn-lg\" style=\"width: 100%;\"><span\n            class=\"glyphicon glyphicon-ok-sign\"></span> Add\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n\n{% block delete_modal %}\n\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><span\n        class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span></button>\n    <h4 class=\"modal-title custom_align\" id=\"Heading\">Delete this entry</h4>\n  </div>\n\n  <div class=\"modal-body\">\n    <form action=\"/outboundroutesdelete\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n\n      <div class=\"modal-body\">\n        <div class=\"form-group\">\n          <input class=\"ruleid form-control\" type=\"hidden\" name=\"ruleid\">\n          <input class=\"groupid form-control\" type=\"hidden\" name=\"groupid\">\n        </div>\n\n        <div class=\"alert alert-danger\">\n          <span class=\"glyphicon glyphicon-warning-sign\"></span> Are you sure you want\n          to delete this Record?\n        </div>\n      </div>\n\n      <div class=\"modal-footer \">\n        <button type=\"submit\" class=\"btn btn-success\"><span class=\"glyphicon glyphicon-ok-sign\"></span> Yes\n        </button>\n        <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\"><span\n            class=\"glyphicon glyphicon-remove\"></span> No\n        </button>\n      </div>\n    </form>\n  </div>\n\n{% endblock %}\n\n{% block custom_js %}\n  {{ script_tag('outboundroutes') }}\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/stirshaken.html",
    "content": "{% extends 'fullwidth_layout.html' %}\n\n{% block title %}STIR/SHAKEN Configuration{% endblock %}\n\n{% block custom_css %}\n{% endblock %}\n\n{% block body %}\n  <div class=\"col-md-12\">\n    <form action=\"/stirshaken\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n      <div class=\"saveTeleblock\">\n        <h4>STIR/SHAKEN Settings</h4>\n        <button name='save' id='save' class='btn btn-success btn-md' data-title=\"Add\" data-toggle=\"modal\"\n                data-target=\"#add\">Save\n        </button>\n      </div>\n\n      <hr>\n\n      <div class=\"form-group\">\n        <label class=\"label-toggle\">STIR/SHAKEN Service </label>\n        <input id=\"toggleStirShaken\" name=\"stir_shaken_enabled\" type=\"checkbox\"\n               title=\"Toggle STIR/SHAKEN\" {{ toggle_checked }}\n               data-toggle=\"toggle\"\n               value=\"{{ settings.STIR_SHAKEN_ENABLED }}\"\n               data-on=\"<span class='icon-gryphon'></span> Enabled\"\n               data-off=\"<span class='icon-gryphon'></span> Disabled\"\n               data-width=\"120px\">\n      </div>\n\n      <div id=\"stirShakenOptions\" class=\"form-group {{ options_hidden }}\">\n\n        <div class=\"form-group\">\n          <input class=\"form-control \" type=\"text\" id=\"stir_shaken_prefix_a\" name=\"stir_shaken_prefix_a\"\n                 placeholder=\"Caller ID Prefix A Validated Calls\"\n                 value=\"{{ settings.STIR_SHAKEN_PREFIX_A }}\">\n        </div>\n\n        <div class=\"form-group\">\n          <input class=\"form-control \" type=\"text\" id=\"stir_shaken_prefix_b\" name=\"stir_shaken_prefix_b\"\n                 placeholder=\"Caller ID Prefix B Validated Calls\"\n                 value=\"{{ settings.STIR_SHAKEN_PREFIX_B }}\">\n        </div>\n\n        <div class=\"form-group\">\n          <input class=\"form-control \" type=\"text\" id=\"stir_shaken_prefix_c\" name=\"stir_shaken_prefix_c\"\n                 placeholder=\"Caller ID Prefix C Validated Calls\"\n                 value=\"{{ settings.STIR_SHAKEN_PREFIX_C }}\">\n        </div>\n\n        <div class=\"form-group\">\n          <input class=\"form-control \" type=\"text\" id=\"stir_shaken_prefix_invalid\"\n                 name=\"stir_shaken_prefix_invalid\"\n                 placeholder=\"Caller ID Prefix Invalid Calls\"\n                 value=\"{{ settings.STIR_SHAKEN_PREFIX_INVALID }}\">\n        </div>\n\n        <div class=\"form-group\">\n          <input class=\"form-control \" type=\"text\" id=\"stir_shaken_cert_url\"\n                 name=\"stir_shaken_cert_url\"\n                 placeholder=\"Certificate URL\"\n                 value=\"{{ settings.STIR_SHAKEN_CERT_URL }}\">\n        </div>\n\n        <div class=\"form-group\">\n          <input class=\"form-control \" type=\"text\" id=\"stir_shaken_key_path\"\n                 name=\"stir_shaken_key_path\"\n                 placeholder=\"Key Path\"\n                 value=\"{{ settings.STIR_SHAKEN_KEY_PATH }}\">\n        </div>\n\n        <div class=\"checkbox\">\n          <label>\n            <input type=\"checkbox\"\n                   name=\"stir_shaken_block_invalid\" {{ 'checked' if (settings.STIR_SHAKEN_BLOCK_INVALID == 1) else '' }}>\n            Block Invalidated Calls\n          </label>\n        </div>\n      </div> <!--End of STIR/SHAKEN settings-->\n    </form>\n  </div>\n{% endblock %}\n\n{% block custom_js %}\n  {{ script_tag('stirshaken') }}\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/table_layout.html",
    "content": "{% extends 'fullwidth_layout.html' %}\n\n{% block title %}Dashboard{% endblock %}\n\n{% block custom_css %}\n{% endblock %}\n\n{% block body %}\n\n  <div class=\"wrapper-horizontal edge-centered children-align-inherit content-header\">\n    {% block table_headers %}\n    {% endblock %}\n  </div>\n\n  <div class=\"table-responsive content-section\">\n    {% block table %}\n    {% endblock %}\n    <img id=\"loading-spinner\" class=\"hidden\" src=\"{{ url_for('static', filename='images/spinner.svg') }}\" alt=\"\"/>\n  </div>\n\n\n  <div class=\"modal fade\" id=\"edit\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"Edit\" aria-hidden=\"true\">\n    <div class=\"modal-dialog\">\n      <div class=\"modal-content\">\n        {% block edit_modal %}\n        {% endblock %}\n      </div><!-- /.modal-content -->\n    </div><!-- /.modal-dialog -->\n  </div><!-- /.modal -->\n\n\n  <div class=\"modal fade\" id=\"add\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"Add\" aria-hidden=\"true\">\n    <div class=\"modal-dialog\">\n      <div class=\"modal-content\">\n        {% block add_modal %}\n        {% endblock %}\n      </div><!-- /.modal-content -->\n    </div><!-- /.modal-dialog -->\n  </div><!-- /.modal -->\n\n\n  <div class=\"modal fade\" id=\"delete\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"Delete\" aria-hidden=\"true\">\n    <div class=\"modal-dialog\">\n      <div class=\"modal-content\">\n        {% block delete_modal %}\n        {% endblock %}\n      </div><!-- /.modal-content -->\n    </div><!-- /.modal-dialog -->\n  </div><!-- /.modal -->\n\n  <div class=\"modal fade\" id=\"import\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"Import\" aria-hidden=\"true\">\n    <div class=\"modal-dialog\">\n      <div class=\"modal-content\">\n        {% block import_modal %}\n        {% endblock %}\n      </div><!-- /.modal-content -->\n    </div><!-- /.modal-dialog -->\n  </div><!-- /.modal -->\n\n  {% block custom_modals %}\n  {% endblock %}\n\n{% endblock %}\n\n{% block custom_js %}\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/teleblock.html",
    "content": "{% extends 'fullwidth_layout.html' %}\n\n{% block title %}Teleblock Configuration{% endblock %}\n\n{% block custom_css %}\n{% endblock %}\n\n{% block body %}\n\n  <div class=\"col-md-12\">\n    <form action=\"/teleblock\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n\n      <div class=\"saveTeleblock\">\n        <h4>Teleblock Settings</h4>\n        <button name='save' id='save' class='btn btn-success btn-md' data-title=\"Add\" data-toggle=\"modal\"\n                data-target=\"#add\">Save\n        </button>\n      </div>\n      <hr>\n      <div class=\"form-group\">\n      <label class=\"label-toggle\">Global Gryphon Teleblock Support </label>\n        {% if teleblock[\"gw_enabled\"] == 1 %}\n          <input id=\"toggleTeleblock\" name =\"gw_enabled\" type=\"checkbox\" checked title=\"Toggle Teleblock\"\n                 data-toggle=\"toggle\"\n                 value=\"{{ teleblock[\"gw_enabled\"] }}\"\n                 data-on=\"<span class='icon-gryphon'></span> Enabled\"\n                 data-off=\"<span class='icon-gryphon'></span> Disabled\"\n                 data-width=\"120px\">\n        {% else %}\n          <input id=\"toggleTeleblock\" name=\"gw_enabled\" type=\"checkbox\" title=\"Toggle Teleblock\"\n                 data-toggle=\"toggle\"\n                 value=\"{{ teleblock[\"gw_enabled\"] }}\"\n                 data-on=\"<span class='icon-gryphon'></span> Enabled\"\n                 data-off=\"<span class='icon-gryphon'></span> Disabled\"\n                 data-width=\"120px\">\n        {% endif %}\n      </div>\n      {% if teleblock[\"gw_enabled\"]  == 1 %}\n        <div id=\"teleblockOptions\" class=\"form-group\">\n      {% else %}\n        <div id=\"teleblockOptions\" class=\"form-group hidden\">\n      {% endif %}\n      <div class=\"form-group\">\n        <input class=\"form-control \" type=\"text\" id=\"gw_ip\" name=\"gw_ip\" placeholder=\"Teleblock Gateway IP\"\n               value=\"{{ teleblock[\"gw_ip\"] }}\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"form-control \" type=\"text\" id=\"gw_port\" name=\"gw_port\" placeholder=\"Teleblock Gateway Port\"\n               value=\"{{ teleblock[\"gw_port\"] }}\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"form-control \" type=\"text\" id=\"media_ip\" name=\"media_ip\"\n               placeholder=\"Media IP for Teleblock Messages\" value=\"{{ teleblock[\"media_ip\"] }}\">\n      </div>\n      <div class=\"form-group\">\n        <input class=\"form-control \" type=\"text\" id=\"media_port\" name=\"media_port\"\n               placeholder=\"Media Port for Teleblock Messages\" value=\"{{ teleblock[\"media_port\"] }}\">\n      </div>\n      </div> <!--End of Teleblock settings-->\n    </form>\n  </div>\n\n\n{% endblock %}\n\n{% block custom_js %}\n  {{ script_tag('teleblock') }}\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/transnexus.html",
    "content": "{% extends 'fullwidth_layout.html' %}\n\n{% block title %}TransNexus Configuration{% endblock %}\n\n{% block custom_css %}\n{% endblock %}\n\n{% block body %}\n  <div class=\"col-md-12\">\n    <form action=\"/transnexus\" method=\"POST\" role=\"form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n\n      <div>\n        <h4>TransNexus Settings</h4>\n        <button name='save' id='save' class='btn btn-success btn-md' data-title=\"Add\" data-toggle=\"modal\"\n                data-target=\"#add\">Save\n        </button>\n      </div>\n\n      <hr>\n\n      <div class=\"form-group\">\n        <label class=\"label-toggle\">STIR/SHAKEN Authentication (outbound calls)</label>\n        <input id=\"toggle_auth_settings\" name=\"authservice_enabled\" type=\"checkbox\" {% if settings.TRANSNEXUS_AUTHSERVICE_ENABLED == 1 %}checked{% endif %}\n               title=\"Toggle Auth Service\"\n               data-toggle=\"toggle\"\n               value=\"{{ settings.TRANSNEXUS_AUTHSERVICE_ENABLED }}\"\n               data-on=\"<span class='icon-transnexus'></span> Enabled\"\n               data-off=\"<span class='icon-transnexus'></span> Disabled\"\n               data-width=\"120px\">\n      </div>\n\n      <div id=\"authservice_settings\" class=\"form-group {% if settings.TRANSNEXUS_AUTHSERVICE_ENABLED != 1 %}hidden{% endif %}\">\n        <div class=\"form-group\">\n          <input class=\"form-control \" type=\"text\" id=\"authservice_host\" name=\"authservice_host\"\n                 placeholder=\"Auth Service Host\"\n                 value=\"{{ settings.TRANSNEXUS_AUTHSERVICE_HOST }}\">\n        </div>\n      </div>\n\n      <div class=\"form-group\">\n        <label class=\"label-toggle\">STIR/SHAKEN Verification (inbound calls)</label>\n        <input id=\"toggle_verify_settings\" name=\"verifyservice_enabled\" type=\"checkbox\" {% if settings.TRANSNEXUS_VERIFYSERVICE_ENABLED == 1 %}checked{% endif %}\n               title=\"Toggle Verify Service\"\n               data-toggle=\"toggle\"\n               value=\"{{ settings.TRANSNEXUS_VERIFYSERVICE_ENABLED }}\"\n               data-on=\"<span class='icon-transnexus'></span> Enabled\"\n               data-off=\"<span class='icon-transnexus'></span> Disabled\"\n               data-width=\"120px\">\n      </div>\n\n      <div id=\"verifyservice_settings\" class=\"form-group {% if settings.TRANSNEXUS_VERIFYSERVICE_ENABLED != 1 %}hidden{% endif %}\">\n        <div class=\"form-group\">\n          <input class=\"form-control \" type=\"text\" id=\"verifyservice_host\" name=\"verifyservice_host\"\n                 placeholder=\"Verify Service Host\"\n                 value=\"{{ settings.TRANSNEXUS_VERIFYSERVICE_HOST }}\">\n        </div>\n      </div>\n    </form>\n  </div>\n\n\n{% endblock %}\n\n{% block custom_js %}\n  {{ script_tag('transnexus') }}\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/upgrade.html",
    "content": "{% extends 'fullwidth_layout.html' %}\n\n{% block title %}dSIPRouter Upgrade{% endblock %}\n\n{% block custom_css %}\n{% endblock %}\n\n{% block body %}\n  <div>\n    {% if msg %}\n      <div class=\"alert alert-danger container\">\n        <h2 style=\"color: rgb(0, 0, 0);\"><strong>{{ msg }}</strong></h2>\n      </div>\n    {% endif %}\n  </div>\n  <div class=\"col-md-12\">\n\n    <div class=\"wrapper-horizontal edge-centered children-align-inherit content-header\">\n      <div>\n        <h3>dSIPRouter Upgrade</h3>\n      </div>\n      <div>\n        <button class=\"btn btn-med btn-info\" id=\"btnShowLog\">Show Previous Log</button>\n      </div>\n    </div>\n\n    <div class=\"row\">\n      <div class=\"col-sm-12\">\n        <p>Current Version: {{ upgrade_settings[\"current_version\"] }}</p>\n        <p>Latest Version: {{ upgrade_settings[\"latest_version\"] }}</p>\n      </div>\n    </div>\n\n\n    <form action=\"/upgrade\" method=\"POST\" role=\"form\" id=\"upgrade_form\">\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n      <input type=\"hidden\" name=\"current_version\" value=\"{{ upgrade_settings[\"current_version\"] }}\"/>\n      <input type=\"hidden\" name=\"latest_version\" value=\"{{ upgrade_settings[\"latest_version\"] }}\"/>\n\n      {% if upgrade_settings[\"upgrade_available\"] == 1 %}\n        <div class=\"row\">\n          <div class=\"col-sm-12\">\n            {% with messages = get_flashed_messages() %}\n              {% if messages %}\n                <ul class=flashes>\n                  {% for message in messages %}\n                    <li>{{ message }}</li>\n                  {% endfor %}\n                </ul>\n              {% endif %}\n            {% endwith %}\n            <div class=\"alert alert-warning\">\n              <p>Please note that upgrades for cluster installations are not supported at this time.</p>\n            </div>\n\n            <button class=\"btn btn-primary\"\n                    onclick=\"return confirm('Are you sure you want to start the upgrade process. This will temporarily take your server offline.')\">\n              Upgrade Now\n            </button>\n          </div>\n        </div>\n      {% else %}\n        <div class=\"row\">\n          <div class=\"col-sm-12\">\n            <div class=\"alert alert-success\">\n              <p>Your system is up to date.</p>\n            </div>\n          </div>\n        </div>\n      {% endif %}\n    </form>\n\n    <div id=\"upgrade_output_row\" class=\"row\" style=\"display: none\">\n      <div class=\"col-sm-12\" style=\"height: auto; border-radius: 1em; border-width: medium; border-style: ridge;\">\n        <h4>Upgrade Log</h4>\n        <div style=\"padding: 0.5em; border-width: medium 0 0 0; border-style: ridge;\"></div>\n        <div id=\"upgrade_output\"></div>\n        <div style=\"padding: 0.5em;\"></div>\n      </div>\n    </div>\n\n  </div>\n{% endblock %}\n\n{% block custom_js %}\n  {{ script_tag('upgrade') }}\n{% endblock %}\n"
  },
  {
    "path": "gui/templates/util.jinja2.html",
    "content": "{%- macro link_tag(location) -%}\n        <link rel=\"stylesheet\" href=\"/static/css/{{ location }}.css\">\n{%- endmacro -%}\n\n{%- macro script_tag(location) -%}\n        <script src=\"/static/js/{{ location }}.js\"></script>\n{%- endmacro -%}\n\n{%- macro tracked_link(title, url) -%}\n        <a href=\"{{ url_for(\"click\", url=url) }}\">{{ title }}</a>\n{%- endmacro -%}\n\n{%- macro img_tag(location, alt=\"\", style=\"width:auto; height:auto;\") -%}\n        <img src=\"{{ location|imgFilter() }}\" alt=\"{{ alt }}\" style=\"{{ style }}\">\n{%- endmacro -%}\n\n{%- macro modal(id, label='', hidden=\"true\", content=\"\") -%}\n  <div class=\"modal fade\" id=\"{{ id }}\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"{{ label }}\" aria-hidden=\"{{ hidden }}\">\n    <div class=\"modal-dialog\">\n      <div class=\"modal-content\">\n        {{ content }}\n      </div><!-- /.modal-content -->\n    </div><!-- /.modal-dialog -->\n  </div><!-- /.modal -->\n{%- endmacro %}\n\n"
  },
  {
    "path": "gui/util/conversions.py",
    "content": "number_alphabet = '0123456789'\nlc_letter_alphabet = 'abcdefghijklmnopqrstuvwxyz'\nuc_letter_alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'\n\n# process ASTERISK-like regex pattern for a prefix\n# we are not expanding '!' or '.'\n# this is because we don't support length based prefix matching\n# instead we utilize dr_rules longest-to-shortest prefix matching\ndef expand_prefix(prefix, suffix_check=None):\n    finished = False\n    for i in range(len(prefix)):\n        if prefix[i] == 'X' and suffix_check is None:\n            for j in range(0,9+1):\n                yield from expand_prefix(prefix[:i] + str(j) + prefix[i+1:])\n        elif prefix[i] == 'Z' and suffix_check is None:\n            for j in range(1,9+1):\n                yield from expand_prefix(prefix[:i] + str(j) + prefix[i+1:])\n            finished = True\n        elif prefix[i] == 'N' and suffix_check is None:\n            for j in range(2,9+1):\n                yield from expand_prefix(prefix[:i] + str(j) + prefix[i+1:])\n            finished = True\n        elif prefix[i] == '[':\n            tmp = prefix[i+1:]\n            try:\n                dash_idx = tmp.index('-')\n                try:\n                    start,end = number_alphabet.index(tmp[dash_idx-1]),number_alphabet.index(tmp[dash_idx+1])\n                    for j in number_alphabet[start:end+1]:\n                        yield from expand_prefix(prefix[:i] + j + prefix[prefix.index(']')+1:])\n                    finished = True\n                except ValueError:\n                    pass\n                try:\n                    start,end = lc_letter_alphabet.index(tmp[dash_idx-1]),lc_letter_alphabet.index(tmp[dash_idx+1])\n                    for j in lc_letter_alphabet[start:end+1]:\n                        yield from expand_prefix(prefix[:i] + j + prefix[prefix.index(']')+1:])\n                    finished = True\n                except ValueError:\n                    pass\n                try:\n                    start,end = uc_letter_alphabet.index(tmp[dash_idx-1]),uc_letter_alphabet.index(tmp[dash_idx+1])\n                    for j in uc_letter_alphabet[start:end+1]:\n                        yield from expand_prefix(prefix[:i] + j + prefix[prefix.index(']')+1:], suffix_check=prefix[prefix.index(']')+1:])\n                    finished = True\n                except ValueError:\n                    pass\n            except ValueError:\n                for j in tmp[:tmp.index(']')]:\n                    yield from expand_prefix(prefix[:i] + j + prefix[prefix.index(']')+1:])\n        else:\n            if finished:\n                break\n            if suffix_check is None:\n                if not any(c in set('XZN[]') for c in prefix):\n                    yield prefix.replace('.', '').replace('!', '')\n                    break\n            else:\n                if not any(c in set('[]') for c in prefix) and not any(c in set('XZN[]') for c in suffix_check):\n                    yield prefix.replace('.', '').replace('!', '')\n                    break\n\n\ndef expand_prefixs(prefixs):\n    for p in prefixs:\n        yield sorted(list(expand_prefix(p)))\n\n#### Example Usage:\n#example_prefixs = ['[0-9]', '[a-z]', '[A-Z]', '[01]N']\n#for p in expand_prefixs(example_prefixs):\n#    print(p)\n"
  },
  {
    "path": "gui/util/cron.py",
    "content": "from crontab import CronTab, CronSlices\nfrom cron_descriptor import ExpressionDescriptor\n\n# TODO: we should propagate exceptions from here so we can\n#       better handle them at the API level\n\ndef getTaggedCronjob(tag):\n    \"\"\"\n    Get a cronjob from current user's crontab by its tag\n\n    :param tag:     tag specified when creating\n    :type tag:      str|int\n    :return:        cronjob entry or None\n    :rtype:         CronTab|None\n    \"\"\"\n    try:\n        if isinstance(tag, int):\n            tag = str(tag)\n        cron  = CronTab(user=True)\n        return tuple(cron.find_comment(tag))[0]\n    except:\n        return None\n\ndef addTaggedCronjob(tag, interval, cmd):\n    \"\"\"\n    Adds a tagged cronjob to current user's crontab\n\n    :param tag:         tag for new entry\n    :type tag:          str|int\n    :param interval:    crontab interval\n    :type interval:     str\n    :param cmd:         crontab cmd to run\n    :type cmd:          str\n    :return:            whether it succeeded\n    :rtype:             bool\n    \"\"\"\n    try:\n        if isinstance(tag, int):\n            tag = str(tag)\n        if not CronSlices.is_valid(interval):\n            return False\n\n        cron  = CronTab(user=True)\n\n        matching_jobs = tuple(cron.find_comment(tag))\n        if len(matching_jobs) == 0:\n            job = cron.new(command=cmd, comment=tag)\n        else:\n            job = matching_jobs[0]\n            job.set_command(cmd)\n        job.setall(interval)\n\n        if not job.is_valid():\n            return False\n\n        cron.write()\n        return True\n    except:\n        return False\n\ndef updateTaggedCronjob(tag, interval='', cmd='', new_tag=''):\n    \"\"\"\n    Update a tagged cronjob in the current user's crontab\n\n    :param tag:         tag of existing entry\n    :type tag:          str|int\n    :param interval:    new crontab interval\n    :type interval:     str\n    :param cmd:         new crontab cmd to run\n    :type cmd:          str\n    :param new_tag:     new tag for entry\n    :type new_tag:      str|int\n    :return:            whether it succeeded\n    :rtype:             bool\n    \"\"\"\n    try:\n        if isinstance(tag, int):\n            tag = str(tag)\n        if isinstance(new_tag, int):\n            new_tag = str(new_tag)\n\n        cron = CronTab(user=True)\n\n        matching_jobs = tuple(cron.find_comment(tag))\n        if len(matching_jobs) == 0:\n            job = cron.new(comment=tag)\n        else:\n            job = tuple(cron.find_comment(tag))[0]\n\n        if len(interval) > 0:\n            if not CronSlices.is_valid(interval):\n                return False\n            job.setall(interval)\n\n        if len(cmd) > 0:\n            job.set_command(cmd)\n\n        if len(new_tag) > 0:\n            job.set_comment(new_tag)\n\n        if not job.is_valid():\n            return False\n\n        cron.write()\n        return True\n    except:\n        return False\n\ndef deleteTaggedCronjob(tag):\n    \"\"\"\n    Delete a tagged cronjob from the existing user's crontab\n\n    :param tag:     tag of existing entry\n    :type tag:      str|int\n    :return:        whether it succeeded\n    :rtype:         bool\n    \"\"\"\n    try:\n        if isinstance(tag, int):\n            tag = str(tag)\n\n        cron  = CronTab(user=True)\n\n        matching_jobs = tuple(cron.find_comment(tag))\n        if len(matching_jobs) == 0:\n            return True\n\n        job = tuple(cron.find_comment(tag))[0]\n        cron.remove(job)\n\n        cron.write()\n        return True\n    except:\n        return False\n\ndef cronIntervalToDescription(interval):\n    \"\"\"\n    Convert a crontab interval to a human readable format\n\n    :param interval:        crontab interval\n    :type interval:         str\n    :return:                readable format or None\n    :rtype:                 str|None\n    \"\"\"\n    try:\n        descriptor = ExpressionDescriptor(interval, use_24hour_time_format=True)\n        return descriptor.get_description()\n    except:\n        return None\n"
  },
  {
    "path": "gui/util/file_handling.py",
    "content": "import os\nimport grp, pwd\nfrom werkzeug.utils import secure_filename\n\n# TODO: files should be validated by magic bytes header as well as extension\n\nVALID_IMAGE_EXTENSIONS = {'jpg', 'jpe', 'jpeg', 'png', 'gif', 'svg', 'bmp'}\nVALID_VIDEO_EXTENSIONS = {'avi' 'flv' 'wmv' 'mov' 'mp4'}\nVALID_AUDIO_EXTENSIONS = {'wav' 'mp3' 'aac' 'ogg' 'oga' 'flac'}\nVALID_DOC_EXTENSIONS = {'txt' 'csv', 'rtf' 'odf' 'ods' 'gnumeric' 'abw' 'doc' 'docx' 'xls' 'xlsx'}\nVALID_LOG_EXTENSIONS = {'log','pcap','pcapng'}\n\ndef isValidFile(filename, filetype='any'):\n    \"\"\"\n    Verifies file type based on extension and type to verify against\n    Returns true if extension is valid for filetype, false otherwise\n    Throws ValueError when filetype can not be handled\n    Filetypes currently supported are: [image | video | audio | doc | log | any]\n    \"\"\"\n\n    if filetype == 'any':\n        return '.' in filename and filename.rsplit('.', 1)[1] in VALID_IMAGE_EXTENSIONS.union(\n            VALID_VIDEO_EXTENSIONS).union(VALID_AUDIO_EXTENSIONS).union(VALID_DOC_EXTENSIONS).union(VALID_LOG_EXTENSIONS)\n    elif filetype == 'image':\n        return '.' in filename and filename.rsplit('.', 1)[1] in VALID_IMAGE_EXTENSIONS\n    elif filetype == 'video':\n        return '.' in filename and filename.rsplit('.', 1)[1] in VALID_VIDEO_EXTENSIONS\n    elif filetype == 'audio':\n        return '.' in filename and filename.rsplit('.', 1)[1] in VALID_AUDIO_EXTENSIONS\n    elif filetype == 'doc':\n        return '.' in filename and filename.rsplit('.', 1)[1] in VALID_DOC_EXTENSIONS\n    elif filetype == 'log':\n        return '.' in filename and filename.rsplit('.', 1)[1] in VALID_LOG_EXTENSIONS\n    else:\n        raise ValueError('Validation of filetype: ' + filetype + ' is not supported')\n\ndef saveUpload(file, savedir, filetype):\n    \"\"\"\n    Saves file from current request context to provided savedir\n    Flashes message indicating success or fail\n    Returns a Dict {status=[error|warning|success], message=[None|<message>], file=[None|<filename>]}\n    \"\"\"\n\n    result = {'status': 'error', 'message': None, 'file': None}\n\n    if not file or file.filename == '':\n        result['status'] = 'warning'\n        result['message'] = 'No file has been selected'\n        return result\n\n    # make dirs in path if they don't exist\n    if not os.path.exists(savedir):\n        os.makedirs(savedir)\n\n    if not isValidFile(file.filename, filetype):\n        result['status'] = 'error'\n        result['message'] = 'Unable to save file, invalid file type'\n        return result\n    else:\n        # save the file\n        filename = secure_filename(file.filename)\n        filepath = os.path.join(savedir, filename)\n        file.save(filepath)\n\n        # store the results\n        result['status'] = 'success'\n        result['message'] = 'File successfully uploaded'\n        result['file'] = filename\n\n    return result\n\ndef change_permissions_recursive(path, mode):\n    for root, dirs, files in os.walk(path, topdown=False):\n        for dir in [os.path.join(root,d) for d in dirs]:\n            os.chmod(dir, mode)\n    for file in [os.path.join(root, f) for f in files]:\n            os.chmod(file, mode)\n\ndef change_owner(path,user,group):\n    uid = pwd.getpwnam(user).pw_uid\n    gid = grp.getgrnam(group).gr_gid\n    os.chown(path, uid, gid)\n"
  },
  {
    "path": "gui/util/ipc.py",
    "content": "# make sure the generated source files are imported instead of the template ones\nimport sys\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nimport inspect, os, signal\nfrom UltraDict import UltraDict, Exceptions as shmem_exceptions\nimport settings\n\n\nSETTINGS_SHMEM_NAME = 'shmem_settings'\nSTATE_SHMEM_NAME = 'shmem_state'\n\n\ndef createSharedMemoryDict(val, name):\n    # globals from the top-level module\n    caller_globals = dict(inspect.getmembers(inspect.stack()[-1][0]))[\"f_globals\"]\n\n    try:\n        caller_globals[name] = UltraDict(val, name=name, create=True, auto_unlink=False, recurse=True)\n    except shmem_exceptions.AlreadyExists:\n        # we always want fresh memory, even if the memory was not deallocated properly\n        UltraDict(name=name, create=False).unlink()\n        caller_globals[name] = UltraDict(val, name=name, create=True, auto_unlink=False, recurse=True)\n\n    return caller_globals[name]\n\ndef getSharedMemoryDict(name):\n    # globals from the top-level module\n    caller_globals = dict(inspect.getmembers(inspect.stack()[-1][0]))[\"f_globals\"]\n\n    if name in caller_globals:\n        return caller_globals[name]\n\n    caller_globals[name] = UltraDict(name=name, create=False)\n    return caller_globals[name]\n\n\ndef sendSyncSettingsSignal(pid_file=settings.DSIP_PID_FILE, load_shared_settings=False):\n    \"\"\"\n    Send signals to GUI process to synchronize settings from disk or shared memory\n\n    :param pid_file:                path to PID file for process to send to\n    :type pid_file:                 str\n    :param load_shared_settings:    whether to load settings from shared memory or not\n    :type load_shared_settings:     bool\n    :return:                        None\n    :rtype:                         None\n    :except:                        OSError ProcessLookupError ValueError\n    \"\"\"\n    with open(pid_file, 'r') as f:\n        pid = int(f.read())\n        if not load_shared_settings:\n            os.kill(pid, signal.SIGUSR1)\n        else:\n            os.kill(pid, signal.SIGUSR2)\n"
  },
  {
    "path": "gui/util/kamtls.py",
    "content": "# make sure the generated source files are imported instead of the template ones\nimport sys\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nimport re, socket\nimport settings\nfrom util.networking import ipv6Test, hostToIP\n\n# Server name matching options\nKAM_TLS_SNI_DOMAIN = 0\nKAM_TLS_SNI_ALL = 1\nKAM_TLS_SNI_SUBDOMAINS = 2\n\n# header formats per domain profile\nDOMAIN_START_HEADER = '#========== {domain}_start ==========#'\nDOMAIN_END_HEADER = '#========== {domain}_end ==========#'\n\n\ndef createCustomTLSConfig(domain, ip, port, server_name_mode):\n    \"\"\"\n    Create a Domain TLS Profile string for the Kamailio TLS Config\\n\n    Reference: https://kamailio.org/docs/modules/5.5.x/modules/tls.html\n\n    :param domain:              domain profile to create\n    :type domain:               str\n    :param ip:                  ip kamailio will match for profile\n    :type ip:                   str\n    :param port:                port kamailio will match for profile\n    :type port:                 int|str\n    :param server_name_mode:    server name matching mode\n    :type server_name_mode:     int|str\n    :return:                    tls config for a custom domain\n    :rtype:                     str\n    \"\"\"\n\n    port = str(port)\n    server_name_mode = str(server_name_mode)\n\n    return (\n        DOMAIN_START_HEADER + '\\n' + (\n            '[server:{ip}:{port}]\\n'\n            'method = TLSv1.2+\\n'\n            'verify_certificate = yes\\n'\n            'require_certificate = yes\\n'\n            'private_key = {certs_dir}/{domain}/dsiprouter-key.pem\\n'\n            'certificate = {certs_dir}/{domain}/dsiprouter-cert.pem\\n'\n            'ca_list = {certs_dir}/ca-list.pem\\n'\n            'server_name = {domain}\\n'\n            'server_name_mode = {name_mode}\\n'\n            '\\n'\n            '[client:{ip}:{port}]\\n'\n            'method = TLSv1.2+\\n'\n            'verify_certificate = yes\\n'\n            'require_certificate = yes\\n'\n            'private_key = {certs_dir}/{domain}/dsiprouter-key.pem\\n'\n            'certificate = {certs_dir}/{domain}/dsiprouter-cert.pem\\n'\n            'ca_list = {certs_dir}/ca-list.pem\\n'\n            'server_name = {domain}\\n'\n            'server_name_mode = {name_mode}\\n'\n            'server_id = {domain}\\n'\n        ) + DOMAIN_END_HEADER + '\\n\\n'\n    ).format(ip=ip, port=port, certs_dir=settings.DSIP_CERTS_DIR, domain=domain, name_mode=server_name_mode)\n\n\ndef getCustomTLSConfigs(domain_filter=None):\n    \"\"\"\n    Get kamailio TLS configs for additional domains\\n\n    Server and client domain/ip/port are assumed to be the same\\n\n    The optional domain filter returns domains & subdomains that match\n\n    Returned Data Format:\n\n    .. code-block:: python\n\n        {\n            '<domain>': {\n                'ip': '<str>',\n                'port': '<int>'\n                'server': {<server params>},\n                'client': {<client params>}\n            },\n            ...\n        }\n\n    :param domain_filter:   filter on this domain / subdomains\n    :type domain_filter:    str\n    :return:                TLS configs for custom domain profiles\n    :rtype:                 dict|None\n    \"\"\"\n\n    custom_configs = {}\n\n    try:\n        with open(settings.KAM_TLSCFG_PATH, 'rb') as kamtlscfg_file:\n            kamtlscfg_bytes = kamtlscfg_file.read()\n\n            if domain_filter is None:\n                regex = DOMAIN_START_HEADER.format(domain=r'(?P<domain>.*?)').encode('utf-8') + \\\n                        rb'''\\n(?:\\[server\\:(?!default)\\[?(?P<server_ip>.*?)\\]?\\:(?P<server_port>[0-9]+)\\])\\n(?P<server_params>.*?)\\n(?=\\[|#)\\n*''' + \\\n                        rb'''(?:\\[client\\:(?!default)\\[?(?P<client_ip>.*?)\\]?\\:(?P<client_port>[0-9]+)\\])\\n(?P<client_params>.*?)(?=\\[|#)''' + \\\n                        DOMAIN_END_HEADER.format(domain=r'(?P=domain)').encode('utf-8')\n            else:\n                regex = DOMAIN_START_HEADER.format(domain=r'(?P<domain>(?:.*?\\.)*{})'.format(domain_filter)).encode('utf-8') + \\\n                        rb'''\\n(?:\\[server\\:(?!default)\\[?(?P<server_ip>.*?)\\]?\\:(?P<server_port>[0-9]+)\\])\\n(?P<server_params>.*?)\\n(?=\\[|#)\\n*''' + \\\n                        rb'''(?:\\[client\\:(?!default)\\[?(?P<client_ip>.*?)\\]?\\:(?P<client_port>[0-9]+)\\])\\n(?P<client_params>.*?)(?=\\[|#)''' + \\\n                        DOMAIN_END_HEADER.format(domain=r'(?P=domain)').encode('utf-8')\n            matches = [x.groupdict() for x in re.finditer(regex, kamtlscfg_bytes, flags=re.DOTALL | re.MULTILINE)]\n\n            for match in matches:\n                domain = match['domain'].decode('utf-8')\n                ip = match['server_ip'].decode('utf-8')\n                server_config_strs = match['server_params'].decode('utf-8').rstrip().split('\\n')\n                client_config_strs = match['client_params'].decode('utf-8').rstrip().split('\\n')\n                custom_configs[domain] = {\n                    'ip': ip if not ipv6Test(ip) else '[' + ip + ']',\n                    'port': int(match['server_port'].decode('utf-8')),\n                    'server': {x.split(' = ')[0]: x.split(' = ')[1] for x in server_config_strs},\n                    'client': {x.split(' = ')[0]: x.split(' = ')[1] for x in client_config_strs}\n                }\n\n        return custom_configs\n    except:\n        return None\n\n\ndef addCustomTLSConfig(domain, ip='', port=5061, server_name_mode=KAM_TLS_SNI_DOMAIN):\n    \"\"\"\n    Add an additional domain to kamailio TLS configs\n\n    :param domain:              domain profile to create\n    :type domain:               str\n    :param ip:                  ip kamailio will match for profile\n    :type ip:                   str\n    :param port:                port kamailio will match for profile\n    :type port:                 int|str\n    :param server_name_mode:    server name matching mode\n    :type server_name_mode:     int|str\n    :return:                    whether addition succeeded\n    :rtype:                     bool\n    \"\"\"\n\n    try:\n        # TLS config does not accept hostnames, resolve IP if needed\n        if len(ip) == 0:\n            ip = hostToIP(domain)\n        # IPv6 addresses require square brackets\n        elif ipv6Test(ip):\n            ip = '[' + ip + ']'\n        domain_config_bytes = createCustomTLSConfig(domain, ip, port, server_name_mode).encode('utf-8')\n\n        with open(settings.KAM_TLSCFG_PATH, 'r+b') as kamtlscfg_file:\n            kamtlscfg_file.seek(0, 2)\n            kamtlscfg_file.write(domain_config_bytes)\n\n        return True\n    except:\n        return False\n\n\ndef updateCustomTLSConfig(domain, ip=None, port=None, server_name_mode=None):\n    \"\"\"\n    Add an additional domain to kamailio TLS configs\n\n    :param domain:              domain profile to update\n    :type domain:               str\n    :param ip:                  ip kamailio will match for profile\n    :type ip:                   str\n    :param port:                port kamailio will match for profile\n    :type port:                 int|str\n    :param server_name_mode:    server name matching mode\n    :type server_name_mode:     int|str\n    :return:                    whether update succeeded\n    :rtype:                     bool\n    \"\"\"\n\n    try:\n        domain_configs = getCustomTLSConfigs(domain)\n        if domain not in domain_configs:\n            return False\n        else:\n            domain_data = domain_configs[domain]\n\n        ip = ip if ip is not None else domain_data['ip']\n        port = port if port is not None else domain_data['port']\n        server_name_mode = server_name_mode if server_name_mode is not None \\\n            else domain_data['server']['server_name_mode']\n\n        if not deleteCustomTLSConfig(domain):\n            return False\n        if not addCustomTLSConfig(domain, ip, port, server_name_mode):\n            return False\n\n        return True\n    except:\n        return False\n\n\ndef deleteCustomTLSConfig(domain):\n    \"\"\"\n    Delete an additional domain in kamailio TLS configs\n\n    :param domain:          domain profile to delete\n    :type domain:           str\n    :return:                whether delete succeeded\n    :rtype:                 bool\n    \"\"\"\n\n    try:\n        with open(settings.KAM_TLSCFG_PATH, 'r+b') as kamtlscfg_file:\n            kamtlscfg_bytes = kamtlscfg_file.read()\n\n            regex = DOMAIN_START_HEADER.format(domain=domain).encode() + rb'.*?' + \\\n                    DOMAIN_END_HEADER.format(domain=domain).encode() + rb'\\n*'\n            custom_tls_bytes = re.sub(regex, b'', kamtlscfg_bytes, flags=re.DOTALL | re.MULTILINE)\n            kamtlscfg_file.truncate(0)\n            kamtlscfg_file.seek(0, 0)\n            kamtlscfg_file.write(custom_tls_bytes)\n\n        return True\n    except:\n        return False\n"
  },
  {
    "path": "gui/util/letsencrypt.py",
    "content": "\"\"\"\nThe workflow consists of:\n(Account creation)\n- Create account key\n- Register account and accept TOS\n(Certificate actions)\n- Select HTTP-01 within offered challenges by the CA server\n- Set up http challenge resource\n- Set up standalone web server\n- Create domain private key and CSR\n- Issue certificate\n\"\"\"\n# make sure the generated source files are imported instead of the template ones\nimport sys\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nimport os, shutil, OpenSSL\nimport josepy as jose\nfrom util.file_handling import change_owner\nfrom contextlib import contextmanager\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom acme import challenges, client, crypto_util, messages, standalone\nimport settings\n\n# This is the ACME Directory URL for `step-ca`\nDIRECTORY_URL_STAGING = 'https://acme-staging-v02.api.letsencrypt.org/directory'\nDIRECTORY_URL_PROD = 'https://acme-v02.api.letsencrypt.org/directory'\n\n# User Agent\nUSER_AGENT = \"dsiprouter\"\n\n# Account key size\nACC_KEY_BITS = 4096\n\n# Certificate private key size\nCERT_PKEY_BITS = 4096\n\n# Port to listen on for `http-01` challenge\nACME_PORT = 80\n\n\ndef new_csr(domain_name, pkey_pem=None):\n    \"\"\"Create certificate signing request.\"\"\"\n    if pkey_pem is None:\n        pkey = OpenSSL.crypto.PKey()\n        pkey.generate_key(OpenSSL.crypto.TYPE_RSA, CERT_PKEY_BITS)\n        pkey_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, pkey)\n    csr_pem = crypto_util.make_csr(pkey_pem, [domain_name])\n    return pkey_pem, csr_pem\n\n\ndef select_http01_chall(orderr):\n    \"\"\"Look for and select `http-01` from the challenges offered by the server.\"\"\"\n    for authz in orderr.authorizations:\n        for i in authz.body.challenges:\n            if isinstance(i.chall, challenges.HTTP01):\n                return i\n    raise Exception('HTTP-01 challenge was not offered by the CA server.')\n\n\n@contextmanager\ndef challenge_server(http_01_resources):\n    \"\"\"Manage standalone server set up and shutdown.\"\"\"\n\n    # Setting up a server that binds at PORT and any address.\n    address = ('', ACME_PORT)\n    servers = None\n    try:\n        servers = standalone.HTTP01DualNetworkedServers(address, http_01_resources)\n        servers.serve_forever()\n        yield servers\n    finally:\n        if servers is not None:\n            servers.shutdown_and_server_close()\n\n\ndef perform_http01(client_acme, challb, orderr):\n    \"\"\"Set up standalone webserver and perform HTTP-01 challenge.\"\"\"\n\n    response, validation = challb.response_and_validation(client_acme.net.key)\n\n    resource = standalone.HTTP01RequestHandler.HTTP01Resource(\n        chall=challb.chall, response=response, validation=validation)\n\n    with challenge_server({resource}):\n        # Let the CA server know that we are ready for the challenge.\n        client_acme.answer_challenge(challb, response)\n\n        # Wait for challenge status and then issue a certificate.\n        # It is possible to set a deadline time.\n        finalized_orderr = client_acme.poll_and_finalize(orderr)\n\n    return finalized_orderr.fullchain_pem\n\n\ndef generateCertificate(domain, notification_email, directory_url=DIRECTORY_URL_PROD, cert_dir=None, debug=None,\n                        default=None):\n    # Create account key\n    acc_key = jose.JWKRSA(\n        key=rsa.generate_private_key(public_exponent=65537,\n                                     key_size=ACC_KEY_BITS,\n                                     backend=default_backend()))\n\n    # Create client configured to use our ACME server and trust our root cert\n    net = client.ClientNetwork(acc_key, user_agent=USER_AGENT)\n    if debug == True:\n        directory_url = DIRECTORY_URL_STAGING\n    directory = messages.Directory.from_json(net.get(directory_url).json())\n    client_acme = client.ClientV2(directory, net=net)\n\n    # Register account and accept TOS\n    regr = client_acme.new_account(\n        messages.NewRegistration.from_data(\n            email=notification_email, terms_of_service_agreed=True))\n\n    # Create domain private key and CSR\n    pkey_pem, csr_pem = new_csr(domain)\n\n    # Issue certificate\n    orderr = client_acme.new_order(csr_pem)\n\n    # Select HTTP-01 within offered challenges by the CA server\n    challb = select_http01_chall(orderr)\n\n    # The certificate is ready to be used in the variable \"fullchain_pem\".\n    fullchain_pem = perform_http01(client_acme, challb, orderr)\n\n    # Store the certificates\n    if cert_dir is None:\n        cert_dir = settings.DSIP_CERTS_DIR\n\n    if default == True:\n        cert_domain_dir = settings.DSIP_CERTS_DIR\n    else:\n        cert_domain_dir = \"{}/{}\".format(cert_dir, domain)\n\n    if not os.path.exists(cert_domain_dir):\n        os.makedirs(cert_domain_dir)\n\n    key_file = os.path.join(cert_domain_dir, 'dsiprouter-key.pem')\n    cert_file = os.path.join(cert_domain_dir, 'dsiprouter-cert.pem')\n\n    with os.fdopen(os.open(key_file, os.O_WRONLY | os.O_CREAT, 0o640), 'w') as keyfile:\n        keyfile.write(pkey_pem.decode('utf-8'))\n    with os.fdopen(os.open(cert_file, os.O_WRONLY | os.O_CREAT, 0o640), 'w') as certfile:\n        certfile.write(fullchain_pem)\n\n    # Change owner to root:kamailio so that Kamailio can load the configurations\n    change_owner(cert_domain_dir, \"dsiprouter\", \"kamailio\")\n    change_owner(key_file, \"dsiprouter\", \"kamailio\")\n    change_owner(cert_file, \"dsiprouter\", \"kamailio\")\n\n    return pkey_pem.decode('utf-8'), fullchain_pem\n\n\ndef deleteCertificate(domain, directory_url=DIRECTORY_URL_PROD, cert_dir=None):\n    # Location of certificates\n    if cert_dir is None:\n        cert_dir = settings.DSIP_CERTS_DIR\n\n    # Location of the certificates\n    cert_domain_dir = \"{}/{}\".format(cert_dir, domain)\n\n    # Remove directory that contains the cert and key for the domain\n    try:\n        shutil.rmtree(cert_domain_dir)\n\n    except Exception as ex:\n        pass\n    # TODO: Potential send a unregister command to Let's Encrypt\n\n\nif __name__ == \"__main__\":\n    directory_url = DIRECTORY_URL_STAGING\n    domain = \"mack.dsiprouter.net\"\n    email = \"mack@dopensource.com\"\n\n    key, cert = generateCertificate(domain, email, directory_url)\n    print(\"Key:\\n{}\\nCert:\\n{}\\n\".format(key, cert))\n"
  },
  {
    "path": "gui/util/networking.py",
    "content": "import socket, binascii, requests, re\nimport requests.packages.urllib3.util.connection as urllib3_conn\nfrom util.pyasync import mtexec\n\n# constants\nRTF_UP = 0x0001  # route usable\nRTF_GATEWAY = 0x0002  # destination is a gateway\nRTF_HOST = 0x0004  # host entry (net otherwise)\nDSIP_DNS_ALIASES = ['local.cluster']  # special purpose dsip hostnames\nRFC2396_USER_TRANSLATION = {\n    0: '%00', 1: '%01', 2: '%02', 3: '%03', 4: '%04', 5: '%05', 6: '%06', 7: '%07', 8: '%08', 9: '%09', 10: '%0a', 11: '%0b', 12: '%0c', 13: '%0d',\n    14: '%0e', 15: '%0f', 16: '%10', 17: '%11', 18: '%12', 19: '%13', 20: '%14', 21: '%15', 22: '%16', 23: '%17', 24: '%18', 25: '%19', 26: '%1a',\n    27: '%1b', 28: '%1c', 29: '%1d', 30: '%1e', 31: '%1f', 32: '%20', 34: '%22', 35: '%23', 37: '%25', 47: '%2f', 60: '%3c', 62: '%3e', 63: '%3f',\n    64: '%40', 91: '%5b', 92: '%5c', 93: '%5d', 94: '%5e', 96: '%60', 123: '%7b', 124: '%7c', 125: '%7d', 127: '%7f', 128: '%80', 129: '%81',\n    130: '%82', 131: '%83', 132: '%84', 133: '%85', 134: '%86', 135: '%87', 136: '%88', 137: '%89', 138: '%8a', 139: '%8b', 140: '%8c', 141: '%8d',\n    142: '%8e', 143: '%8f', 144: '%90', 145: '%91', 146: '%92', 147: '%93', 148: '%94', 149: '%95', 150: '%96', 151: '%97', 152: '%98', 153: '%99',\n    154: '%9a', 155: '%9b', 156: '%9c', 157: '%9d', 158: '%9e', 159: '%9f', 160: '%a0', 161: '%a1', 162: '%a2', 163: '%a3', 164: '%a4', 165: '%a5',\n    166: '%a6', 167: '%a7', 168: '%a8', 169: '%a9', 170: '%aa', 171: '%ab', 172: '%ac', 173: '%ad', 174: '%ae', 175: '%af', 176: '%b0', 177: '%b1',\n    178: '%b2', 179: '%b3', 180: '%b4', 181: '%b5', 182: '%b6', 183: '%b7', 184: '%b8', 185: '%b9', 186: '%ba', 187: '%bb', 188: '%bc', 189: '%bd',\n    190: '%be', 191: '%bf', 192: '%c0', 193: '%c1', 194: '%c2', 195: '%c3', 196: '%c4', 197: '%c5', 198: '%c6', 199: '%c7', 200: '%c8', 201: '%c9',\n    202: '%ca', 203: '%cb', 204: '%cc', 205: '%cd', 206: '%ce', 207: '%cf', 208: '%d0', 209: '%d1', 210: '%d2', 211: '%d3', 212: '%d4', 213: '%d5',\n    214: '%d6', 215: '%d7', 216: '%d8', 217: '%d9', 218: '%da', 219: '%db', 220: '%dc', 221: '%dd', 222: '%de', 223: '%df', 224: '%e0', 225: '%e1',\n    226: '%e2', 227: '%e3', 228: '%e4', 229: '%e5', 230: '%e6', 231: '%e7', 232: '%e8', 233: '%e9', 234: '%ea', 235: '%eb', 236: '%ec', 237: '%ed',\n    238: '%ee', 239: '%ef', 240: '%f0', 241: '%f1', 242: '%f2', 243: '%f3', 244: '%f4', 245: '%f5', 246: '%f6', 247: '%f7', 248: '%f8', 249: '%f9',\n    250: '%fa', 251: '%fb', 252: '%fc', 253: '%fd', 254: '%fe', 255: '%ff'\n}\nRFC2396_USER_ESCAPE_REGEX = re.compile(r'%[a-fA-F0-9][a-fA-F0-9]')\n\n\ndef ipv4Test(address):\n    try:\n        socket.inet_pton(socket.AF_INET, address)\n    except AttributeError:  # no inet_pton here, sorry\n        try:\n            socket.inet_aton(address)\n        except socket.error:\n            return False\n        return address.count('.') == 3\n    except socket.error:  # not a valid address\n        return False\n    return True\n\n\ndef ipv6Test(address):\n    try:\n        socket.inet_pton(socket.AF_INET6, address)\n    except socket.error:  # not a valid address\n        return False\n    return True\n\n\ndef isValidIP(address, ip_ver=''):\n    \"\"\" Determine if ip address is valid \"\"\"\n    if ip_ver == '4':\n        return ipv4Test(address)\n    elif ip_ver == '6':\n        return ipv6Test(address)\n    else:\n        if not ipv4Test(address) and not ipv6Test(address):\n            return False\n        return True\n\n\ndef getInternalIP(ip_ver=''):\n    \"\"\"\n    Get the internally routable ip address of the system\n\n    :param ip_ver:  IP version to fetch '4'=>ipv4,'6'=>ipv6,''=>try both\n    :type ip_ver:   str\n    :return:        Internal IP address\n    :rtype:         str|None\n    \"\"\"\n\n    if ip_ver == '4':\n        sock_types = (socket.AF_INET,)\n    elif ip_ver == '6':\n        sock_types = (socket.AF_INET6,)\n    else:\n        sock_types = (socket.AF_INET, socket.AF_INET6)\n\n    for sock_type in sock_types:\n        try:\n            with socket.socket(sock_type, socket.SOCK_DGRAM) as s:\n                s.connect(('www.google.com', 0))\n                ip = s.getsockname()[0]\n                if isValidIP(ip):\n                    return ip\n        except:\n            pass\n\n    return None\n\n\ndef getExternalIP(ip_ver=''):\n    \"\"\"\n    Get the externally routable ip address of the system\n\n    :param ip_ver:  IP version to fetch '4'=>ipv4,'6'=>ipv6,''=>try both\n    :type ip_ver:   str\n    :return:        External IP address\n    :rtype:         str|None\n    \"\"\"\n\n    task_args = []\n    _allowed_gai_family = urllib3_conn.allowed_gai_family\n\n    # redundancy in case a service provider goes down\n    ipv4_resolvers = (\n        'https://icanhazip.com',\n        'https://ipecho.net/plain',\n        'https://myexternalip.com/raw',\n        'https://api.ipify.org',\n        'https://bot.whatismyipaddress.com',\n    )\n    ipv6_resolvers = (\n        'https://icanhazip.com',\n        'https://bot.whatismyipaddress.com',\n        'https://ifconfig.co',\n        'https://ident.me',\n        'https://api6.ipify.org'\n    )\n\n    # task performed by each thread\n    def getip(sock_type, url):\n        urllib3_conn.allowed_gai_family = lambda: sock_type\n        iptest = ipv6Test if sock_type == socket.AF_INET6 else ipv4Test\n        try:\n            ip = requests.get(url, timeout=2.0).text.strip()\n            if iptest(ip):\n                return ip\n        except:\n            pass\n        return None\n\n    # DNS exceptions can take a while to resolve, so instead we run all requests\n    # in parallel and filter out the first good external IP (ipv4 given priority)\n    if ip_ver == '4' or len(ip_ver) == 0:\n        task_args.extend((socket.AF_INET, resolver) for resolver in ipv4_resolvers)\n    if ip_ver == '6' or len(ip_ver) == 0:\n        task_args.extend((socket.AF_INET6, resolver) for resolver in ipv6_resolvers)\n    results = mtexec(getip, task_args)\n\n    # undo urllib monkey patch\n    urllib3_conn.allowed_gai_family = _allowed_gai_family\n\n    return next((x for x in results if x is not None), None)\n\n\ndef hostToIP(host, ip_ver=''):\n    \"\"\"\n    Convert host to IP Address\\n\n    Supports conversion to IPv4 and IPv6\\n\n    IPv4 takes priority and is tried first\n\n    :param host:        hostname/fqdn to convert to IP\n    :type host:         str\n    :param ip_ver:      IP version to use\n    :type ip_ver:       str\n    :return:            IP address of host\n    :rtype:             str|None\n    \"\"\"\n\n    if ip_ver == '4' or len(ip_ver) == 0:\n        try:\n            return socket.getaddrinfo(host, 0, socket.AF_INET)[0][4][0]\n        except:\n            raise Exception(\"Endpoint hostname/address is malformed or not working:{0}\".format(host))\n    if ip_ver == '6' or len(ip_ver) == 0:\n        try:\n            return socket.getaddrinfo(host, 0, socket.AF_INET6)[0][4][0]\n        except:\n            raise Exception(\"Endpoint hostname/address is malformed or not working:{0}\".format(host))\n    return None\n\n\ndef ipToHost(ip, exclude_dsip_aliases=True):\n    \"\"\"\n    Converts IP Address to hostname\\n\n    Supports conversion from IPv4 and IPv6\n\n    :param ip:      IP address to convert\n    :type ip:       str\n    :return:        hostname for IP\n    :rtype:         str|None\n    \"\"\"\n\n    try:\n        host = socket.gethostbyaddr(ip)[0]\n        if exclude_dsip_aliases and host in DSIP_DNS_ALIASES:\n            return None\n        return host\n    except:\n        return None\n\n\ndef encodeSipUser(user):\n    return user.translate(RFC2396_USER_TRANSLATION)\n\ndef decodeSipUser(user):\n    return RFC2396_USER_ESCAPE_REGEX.sub(lambda m: chr(int(m.group()[1:], base=16)), user)\n\ndef parseSipUri(uri):\n    \"\"\"\n    Parse SIP URI into its components\n\n    components dict format:\n\n    .. code-block:: python\n\n        res = {\n            'proto': <str|None>,\n            'user': <str|None>,\n            'ipv6': <str|None>,\n            'ipv4': <str|None>,\n            'host': <str|None>,\n            'port': <int|None>,\n            'params': <dict|None>\n        }\n\n    :param uri:     SIP URI to parse\n    :type uri:      str\n    :return:        components of SIP URI\n    :rtype:         dict|None\n    \"\"\"\n\n    match = re.search(\n        (r'^'\n         r'(?:(?P<proto>[a-zA-Z]+):)?'\n         r'(?:(?P<user>[a-zA-Z0-9\\-_.]+)@)?'\n         r'(?:'\n         r'(?:\\[?(?P<ipv6>(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\]?)|'\n         r'(?P<ipv4>(?:[0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.(?:[0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.(?:[0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.(?:[0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5]))|'\n         r'(?P<host>(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9])\\.)*(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9]))'\n         r')'\n         r'(?::(?P<port>[1-9]|[1-5]?[0-9]{2,4}|6[1-4][0-9]{3}|65[1-4][0-9]{2}|655[1-2][0-9]|6553[1-5]))?'\n         r'(?:;(?P<params>[^\\s]*))?'\n         r'$'),\n        uri)\n    if match:\n        res = match.groupdict()\n        if res['port'] is not None:\n            res['port'] = int(res['port'])\n        if res['params'] is not None:\n            res['params'] = {x[0]: (x[1] if len(x) == 2 else True) for x in [y.split('=', 1) for y in res['params'].split(';')]}\n        return res\n    return None\n\n\ndef parseGenericUri(uri):\n    \"\"\"\n    Parse Generic URI into its components (as generic as possible)\\n\n    Port is the only part type casted (parse delimited fields accordingly)\n\n    components dict format:\n\n    .. code-block:: python\n\n        res = {\n            'proto': <str|None>,\n            'userpw': <str|None>,\n            'ipv6': <str|None>,\n            'ipv4': <str|None>,\n            'host': <str|None>,\n            'port': <int|None>,\n            'path': <str|None>,\n            'params': <str|None>\n        }\n\n    :param uri:     URI to parse\n    :type uri:      str\n    :return:        components of URI\n    :rtype:         dict|None\n    \"\"\"\n\n    match = re.search(\n        (r'^'\n         r'(?:(?P<proto>[a-zA-Z]+):/?/?)?'\n         r'(?:(?P<userpw>[a-zA-Z0-9\\-_.]+(?::.+)?)(?:@))?'\n         r'(?:'\n         r'(?:\\[?(?P<ipv6>(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\]?)|'\n         r'(?P<ipv4>(?:[0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.(?:[0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.(?:[0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.(?:[0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5]))|'\n         r'(?P<host>(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9])\\.)*(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9]))'\n         r')'\n         r'(?::(?P<port>[1-9]|[1-5]?[0-9]{2,4}|6[1-4][0-9]{3}|65[1-4][0-9]{2}|655[1-2][0-9]|6553[1-5]))?'\n         r'(?P<path>/[^\\s?;]*)?'\n         r'(?:(?:[?;])(?P<params>.*))?'\n         r'$'),\n        uri)\n    if match:\n        res = match.groupdict()\n        if res['port'] is not None:\n            res['port'] = int(res['port'])\n        return res\n    return None\n\n\ndef safeUriToHost(uri, default_port=None):\n    \"\"\"\n    Strips non-host parts and converges on the host\\n\n    Supports IPv4, IPv6, and FQDN addresses as the host\\n\n    Optionally formats with port if default port is set\n\n    :param uri:             uri to extract host from\n    :type uri:              str\n    :param default_port:    default port if not found\n    :type default_port:     int\n    :return:                host or host:port if port set\n    :rtype:                 str|None\n    \"\"\"\n\n    ipv6_port_format = False\n    parts = parseGenericUri(uri)\n    if parts is None:\n        return None\n\n    if parts['ipv4'] is not None:\n        res = parts['ipv4']\n    elif parts['ipv6'] is not None:\n        res = parts['ipv6']\n        ipv6_port_format = True\n    elif parts['host'] is not None:\n        res = parts['host']\n    else:\n        res = None\n\n    if res is not None and default_port is not None:\n        if parts['port'] is not None:\n            port = str(parts['port'])\n        else:\n            port = str(default_port)\n\n        if ipv6_port_format:\n            res = '[{}]'.format(res)\n        res = '{}:{}'.format(res, port)\n    return res\n\n\ndef safeStripPort(address):\n    \"\"\"\n    Strip port from address\\n\n    Properly strips on IPv6, IPv4, and FQDN addresses\n\n    :param address:     address to strip port from\n    :type address:      str\n    :return:            address with port stripped\n    :rtype:             str\n    \"\"\"\n\n    match = re.search(\n        (r'^'\n         r'(?P<host>.*?)'\n         r'(?::(?P<port>[1-9]|[1-5]?[0-9]{2,4}|6[1-4][0-9]{3}|65[1-4][0-9]{2}|655[1-2][0-9]|6553[1-5]))?'\n         r'$'),\n        address)\n    if match:\n        res = match.groupdict()['host']\n        if res[0] == '[' and res[-1] == ']':\n            res = res[1:-1]\n    else:\n        res = address\n    return res\n\n\ndef safeFormatSipUri(uri, default_proto='sip', default_user='', default_port=5060, default_params={}):\n    \"\"\"\n    Given a SIP URI of questionable validity, parse out the good stuff, and fill in the rest\n\n    :param uri:             URI to format / validate\n    :type uri:              str\n    :param default_proto:   default protocol if missing\n    :type default_proto:    str\n    :param default_user:    default user if missing\n    :type default_user:     str\n    :param default_port:    default port if missing\n    :type default_port:     int\n    :param default_params:  default SIP parameters\n    :type default_params:   dict\n    :return:                SIP URI safely formatted\n    :rtype:                 str|None\n    \"\"\"\n\n    parts = parseSipUri(uri)\n    if parts is None:\n        return None\n\n    if parts['ipv4'] is not None:\n        host = parts['ipv4']\n    elif parts['ipv6'] is not None:\n        host = '[{}]'.format(parts['ipv6'])\n    elif parts['host'] is not None:\n        host = parts['host']\n    else:\n        return None\n\n    proto = parts['proto'] if parts['proto'] is not None else default_proto\n    user = parts['user'] if parts['user'] is not None else default_user\n    port = str(parts['port']) if parts['port'] is not None else str(default_port)\n\n    tmp = parts['params'] if parts['params'] is not None else default_params\n    params = ';'.join([('='.join([x[0], str(x[1])]) if not isinstance(x[1], bool) else x[0]) for x in tmp.items()])\n\n    return proto + ':' + \\\n           (encodeSipUser(user) + '@' if len(user) > 0 else '') + \\\n           host + ':' + \\\n           port + (';' + params if len(params) > 0 else '')\n\n\ndef getRoutingTableIPv4():\n    \"\"\"\n    Get IPv4 routing table entries\n\n    :return:    routing table entries\n    :rtype:     list\n    \"\"\"\n\n    rt_entries = []\n\n    try:\n        with open('/proc/net/route', 'r') as fp:\n            _ = next(fp)\n            for line in fp:\n                fields = line.strip().split()\n                rt_entries.append({\n                    'iface': fields[0],  # interface name\n                    'dst_addr': int(fields[1], 16),  # destination network/host address\n                    'gw_addr': int(fields[2], 16),  # gateway address\n                    'flags': int(fields[3], 16),  # routing flags\n                    'use': int(fields[5]),  # number of lookups for this route\n                    'metric': int(fields[6]),  # hops to target address\n                    'mask': int(fields[7], 16),  # network mask for destination\n                    'mtu': int(fields[8])  # max packet size for this route\n                })\n    except:\n        pass\n    return rt_entries\n\n\ndef getRoutingTableIPv6():\n    \"\"\"\n    Get IPv6 routing table entries\n\n    :return:    routing table entries\n    :rtype:     list\n    \"\"\"\n\n    rt_entries = []\n\n    try:\n        with open('/proc/net/ipv6_route', 'r') as fp:\n            _ = next(fp)\n            for line in fp:\n                fields = line.strip().split()\n                rt_entries.append({\n                    'iface': fields[9],  # interface name\n                    'dst_addr': int(fields[0], 16),  # destination network/host address\n                    'dst_plen': int(fields[1], 16),  # destination address prefix length\n                    'src_addr': int(fields[2], 16),  # source network/host address\n                    'src_plen': int(fields[3], 16),  # source address prefix length\n                    'next_hop': int(fields[4], 16),  # gateway / next hop address\n                    'metric': int(fields[5], 16),  # hops to target address\n                    'use': int(fields[7], 16),  # number of lookups for this route\n                    'flags': int(fields[8], 16),  # routing flags\n                })\n    except:\n        pass\n    return rt_entries\n\n\n# Inspired By: `Python CookBook <https://www.safaribooksonline.com/library/view/python-cookbook/0596001673/ch10s06.html>`_\ndef ipToInt(ip_str):\n    \"\"\"\n    Convert IP string to integer\n\n    :param ip_str:          ipv4 or ipv6 address\n    :type ip_str:           str\n    :return:                integer value for ip\n    :rtype:                 int\n    :raises ValueError:     on invalid IP conversion\n    \"\"\"\n\n    for sock_type in (socket.AF_INET, socket.AF_INET6):\n        try:\n            return int(binascii.hexlify(socket.inet_pton(sock_type, ip_str)), 16)\n        except:\n            pass\n    raise ValueError(\"invalid IP address\")\n\n\ndef ipToStr(ip_int):\n    \"\"\"\n    Convert integer IP to string\n\n    :param ip_int:          integer value for ip\n    :type ip_int:           int\n    :return:                ipv4 or ipv6 address\n    :rtype:                 str\n    :raises ValueError:     on invalid IP conversion\n    \"\"\"\n\n    for sock_type in (socket.AF_INET, socket.AF_INET6):\n        try:\n            return socket.inet_ntop(sock_type, binascii.unhexlify(\"{0:x}\".format(ip_int)))\n        except:\n            pass\n    raise ValueError(\"invalid IP address\")\n\n\ndef netMaskToPrefixLen(mask):\n    \"\"\"\n    Convert a network address mask to a CIDR prefix length\\n\n    For example: 255.255.255.0 -> 24\\n\n    Supports IPv4 and IPv6 (ipv6 generally doesn't use net masks though)\\n\n    Supplying network addresses other than a mask will give unintended results\n\n    :param mask:    network address mask\n    :type mask:     str|int\n    :return:        CIDR prefix length\n    :rtype:         str\n    \"\"\"\n\n    if isinstance(mask, str):\n        mask = ipToInt(mask)\n    return str(len(bin(mask)[2:]))\n\n\ndef prefixLenToNetMask(prefixlen):\n    \"\"\"\n    Convert a CIDR prefix length to a network address mask\\n\n    For example: 32 -> 255.255.255.255\\n\n    Supports IPv4 and IPv6 (prefix length range is from 0-128)\n\n    :param prefixlen:       CIDR prefix length\n    :type prefixlen:        str|int\n    :return:                network address mask\n    :rtype:                 str\n    :raises ValueError:     on invalid IP conversion\n    \"\"\"\n\n    if isinstance(prefixlen, str):\n        prefixlen = int(prefixlen)\n    mask = int('1' * 128, 2) & int('1' * prefixlen, 2)\n    return ipToStr(mask)\n\n\ndef getInternalCIDR(ip_ver=''):\n    \"\"\"\n    Get internal IP and network prefix length as CIDR address\n\n    :param ip_ver:      IP version to use\n    :type ip_ver:       str\n    :return:            CIDR address\n    :rtype:             str|None\n    \"\"\"\n\n    if ip_ver == '4' or len(ip_ver) == 0:\n        try:\n            # find default ipv4 route and mask, then create CIDR address\n            rt_entries = getRoutingTableIPv4()\n            if len(rt_entries) == 0:\n                raise Exception()\n            def_iface = next(x for x in rt_entries \\\n                if x['dst_addr'] == 0 and x['flags'] & (RTF_UP | RTF_GATEWAY))['iface']\n            net_mask = next(x for x in rt_entries if x['gw_addr'] == 0 and x['iface'] == def_iface)['mask']\n            prefix_len = netMaskToPrefixLen(net_mask)\n            ip = getInternalIP(ip_ver='4')\n            return '{}/{}'.format(ip, prefix_len)\n        except:\n            pass\n    if ip_ver == '6' or len(ip_ver) == 0:\n        try:\n            # find default ipv6 route and mask, then create CIDR address\n            rt_entries = getRoutingTableIPv6()\n            if len(rt_entries) == 0:\n                raise Exception()\n            def_iface = next(x for x in rt_entries \\\n                if x['dst_addr'] == 0 and x['flags'] & (RTF_UP | RTF_GATEWAY))['iface']\n            prefix_len = str(next(x for x in rt_entries \\\n                if x['dst_addr'] != 0 and x['next_hop'] != 0 \\\n                   and x['iface'] == def_iface)['dst_plen'])\n            ip = getInternalIP(ip_ver='6')\n            return '{}/{}'.format(ip, prefix_len)\n        except:\n            pass\n    return None\n"
  },
  {
    "path": "gui/util/notifications.py",
    "content": "# make sure the generated source files are imported instead of the template ones\nimport sys\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nimport os, smtplib\nfrom email import encoders\nfrom email.mime.base import MIMEBase\nfrom email.mime.text import MIMEText\nfrom email.mime.multipart import MIMEMultipart\nfrom util.pyasync import thread\nfrom util.security import AES_CTR\nfrom shared import debugException\nimport settings\n\n@thread\ndef sendEmail(recipients, text_body, html_body=None, subject=settings.MAIL_DEFAULT_SUBJECT,\n               sender=settings.MAIL_DEFAULT_SENDER, data=None, attachments=[]):\n    \"\"\"\n    Send an Email asynchronously to recipients\n\n    :param recipients:      email addresses we are sending to\n    :type recipients:       list|tuple\n    :param text_body:       email plain text message to send\n    :type text_body:        str\n    :param html_body:       email html message to send\n    :type html_body:        str\n    :param subject:         subject of the email\n    :type subject:          str\n    :param sender:          email address we are sending from\n    :type sender:           str\n    :param data:            key, value pairs to add to message\n    :type data:             dict\n    :param attachments:     list|tuple\n    :type attachments:      files to attach to email\n    :return:                no return value\n    :rtype:                 None\n    \"\"\"\n\n    try:\n        if data is not None:\n            text_body += \"\\r\\n\\n\"\n            for key, value in data.items():\n                text_body += \"{}: {}\\n\".format(str(key),str(value))\n            text_body += \"\\n\"\n\n        # print(\"Creating email\")\n        msg_root = MIMEMultipart('alternative')\n        msg_root['From'] = sender\n        msg_root['To'] = \", \".join(recipients)\n        msg_root['Subject'] = subject\n        msg_root.preamble = \"|-------------------MULTIPART_BOUNDARY-------------------|\\n\"\n\n        # print(\"Adding text body to email\")\n        msg_root.attach(MIMEText(text_body, 'plain'))\n\n        if html_body is not None and html_body != \"\":\n            # print(\"Adding html body to email\")\n            msg_root.attach(MIMEText(html_body, 'html'))\n\n        if len(attachments) > 0:\n            # print(\"Adding attachments to email\")\n            for file in attachments:\n                with open(file, 'rb') as fp:\n                    msg_attachments = MIMEBase('application', \"octet-stream\")\n                    msg_attachments.set_payload(fp.read())\n                encoders.encode_base64(msg_attachments)\n                msg_attachments.add_header('Content-Disposition', 'attachment', filename=os.path.basename(file))\n                msg_root.attach(msg_attachments)\n\n        # check environ vars if in debug mode\n        if settings.DEBUG:\n            settings.MAIL_USERNAME = os.getenv('MAIL_USERNAME', settings.MAIL_USERNAME)\n            settings.MAIL_PASSWORD = os.getenv('MAIL_PASSWORD', settings.MAIL_PASSWORD)\n\n        # need to decrypt password\n        if isinstance(settings.MAIL_PASSWORD, bytes):\n            mailpass = AES_CTR.decrypt(settings.MAIL_PASSWORD)\n        else:\n            mailpass = settings.MAIL_PASSWORD\n\n        # print(\"sending email\")\n        with smtplib.SMTP(settings.MAIL_SERVER, settings.MAIL_PORT) as server:\n            server.connect(settings.MAIL_SERVER, settings.MAIL_PORT)\n            server.ehlo()\n            if settings.MAIL_USE_TLS:\n                server.starttls()\n                server.ehlo()\n            server.login(settings.MAIL_USERNAME, mailpass)\n            msg_root_str = msg_root.as_string()\n            server.sendmail(sender, recipients, msg_root_str)\n            server.quit()\n\n    except Exception as ex:\n        debugException(ex)\n"
  },
  {
    "path": "gui/util/parse_json.py",
    "content": "import inspect, uuid, dataclasses\nfrom datetime import datetime, date, time\nfrom decimal import Decimal\nfrom types import MethodType\nfrom flask.json import JSONEncoder\nfrom sqlalchemy.ext.declarative import DeclarativeMeta\n\n\n# TODO: finish custom json decoder\n\ndef CreateEncoder(revisit_self=False, fields_to_expand=(), fields_to_remove=()):\n    \"\"\"\n    Wrapper method for creating Custom JSON Encoders                        \\n\n    Allows dynamic class definition for use in json.dumps(cls=<encoder>)    \\n\n\n    :param revisit_self:        True | False\n    :type revisit_self:         bool\n    :param fields_to_expand:    ORM Model Fields to expand\n    :type fields_to_expand:     list|tuple\n    :param fields_to_remove:    ORM Model Fields to remove\n    :type fields_to_remove:     list|tuple\n    :return:                    A Voodoo Alchemy Encoder class\n    :rtype:                     VoodooAlchemyEncoder\n    \"\"\"\n\n    visited_values = []\n\n    class VoodooAlchemyEncoder(JSONEncoder):\n        \"\"\"\n        JSON serializer for objects not serializable by default json code                   \\n\n        Also adds nested serialization and dynamic field customization of database Models   \\n\n\n        -----\n\n        The following data types are supported:\n\n        - :class:`types.FunctionType`\n        - :class:`types.MethodType`\n        - :class:`dataclasses`\n        - :class:`decimal.Decimal`\n        - :class:`datetime.datetime`\n        - :class:`datetime.date`\n        - :class:`datetime.time`\n        - :class:`uuid.UUID`\n        - :class:`flask.Markup`\n        - :class:`sqlalchemy.ext.declarative.DeclarativeMeta`\n        - :class:`collections.Iterable`\n        - :class:`types.GeneratorType`\n        - and all classes supported by :class:`flask.JSONEncoder`\n        \"\"\"\n\n        def default(self, value):\n            \"\"\" Override JSONEcoder default class \"\"\"\n\n            if self.is_valid_callable(value):\n                value = value()\n            elif dataclasses and dataclasses.is_dataclass(value):\n                value = dataclasses.asdict(value)\n\n            if isinstance(value, Decimal):\n                value.to_eng_string()\n\n            elif isinstance(value, datetime):\n                return value.strftime('%Y-%m-%d %H:%M:%S')\n\n            elif isinstance(value, date):\n                return value.strftime('%Y-%m-%d')\n\n            elif isinstance(value, time):\n                return value.strftime('%H:%M:%S')\n\n            elif isinstance(value, uuid.UUID):\n                return str(value)\n\n            elif hasattr(value, \"__html__\"):\n                return str(value.__html__())\n\n            elif isinstance(value.__class__, DeclarativeMeta):\n                return self.serialize_model(value)\n\n            elif isinstance(value, dict):\n                return self.serialize_dict(value)\n\n            elif isinstance(value, list):\n                return self.serialize_list(value)\n\n            elif hasattr(value, '__iter__') and hasattr(value, '__next__'):\n                return self.serialize_iter(value)\n\n            else:\n                return JSONEncoder.default(self, value)\n\n        @staticmethod\n        def is_valid_callable(func):\n            if callable(func):\n                i = inspect.getfullargspec(func)\n                if i.args == ['self'] and isinstance(func, MethodType) and not any([i.varargs, i.varkw]):\n                    return True\n                return not any([i.args, i.varargs, i.varkw])\n            return False\n\n        def serialize_model(self, value):\n            # don't re-visit self\n            if revisit_self:\n                if value in visited_values:\n                    return None\n                visited_values.append(value)\n\n            # go through each field in this SQLalchemy class\n            fields = {}\n            for field in [x for x in dir(value) if not x.startswith('_') and x != 'metadata' and not x in fields_to_remove]:\n                val = value.__getattribute__(field)\n\n                # is this field another SQLalchemy valueect, or a list of SQLalchemy valueects?\n                if isinstance(val.__class__, DeclarativeMeta) or (\n                        isinstance(val, list) and len(val) > 0 and isinstance(val[0].__class__, DeclarativeMeta)):\n                    # unless we're expanding this field, stop here\n                    if field not in fields_to_expand:\n                        # not expanding this field: set it to None and continue\n                        fields[field] = None\n                        continue\n\n                # serialize each field if necessary\n                val = self.default(val)\n                fields[field] = val\n\n            # a json-encodable dict\n            return fields\n\n        def serialize_iter(self, value):\n            return [self.default(v) for v in value]\n\n        def serialize_list(self, value):\n            return [self.default(v) for v in value]\n\n        def serialize_dict(self, value):\n            return {self.default(k):self.default(v) for k,v in value.items()}\n\n    return VoodooAlchemyEncoder\n\n# class CustomJSONDecoder(JSONDecoder):\n#     def __init__(self, *args, **kwargs):\n#         self.orig_obj_hook = kwargs.pop(\"object_hook\", None)\n#         super(CustomJSONDecoder, self).__init__(*args,\n#             object_hook=self.custom_obj_hook, **kwargs)\n#\n#     def custom_obj_hook(self, dct):\n#         # Calling custom decode function:\n#         dct = HelperFunctions.jsonDecodeHandler(dct)\n#         if (self.orig_obj_hook):  # Do we have another hook to call?\n#             return self.orig_obj_hook(dct)  # Yes: then do it\n#         return dct  # No: just return the decoded dict\n"
  },
  {
    "path": "gui/util/persistence.py",
    "content": "import json, os\n\n\n# TODO: not thread/process safe, move to shared memory manager\n\n\ndef getPersistentState():\n    if not os.path.exists('/run/dsiprouter/state.json'):\n        return {}\n\n    with open('/run/dsiprouter/state.json', 'r') as f:\n        return json.load(f)\n\ndef setPersistentState(state):\n    with open('/run/dsiprouter/state.json', 'w') as f:\n        json.dump(state, f)\n\ndef updatePersistentState(updates):\n    if not os.path.exists('/run/dsiprouter/state.json'):\n        with open('/run/dsiprouter/state.json', 'w') as f:\n            json.dump(updates, f)\n        return updates\n\n    with open('/run/dsiprouter/state.json', 'r+') as f:\n        state = json.load(f)\n        state.update(updates)\n        f.seek(0)\n        f.truncate(0)\n        json.dump(state, f)\n    return state"
  },
  {
    "path": "gui/util/pyasync.py",
    "content": "import subprocess\nfrom functools import wraps\nfrom threading import Thread, Lock\nfrom multiprocessing import Process\nfrom concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor\n\n\nclass ThreadingIter():\n    \"\"\"\n    Takes an iterator/generator and makes it thread-safe\\n\n    Implements locking for given iterator / generator\n    \"\"\"\n\n    def __init__(self, iter):\n        self.iter = iter\n        self.lock = Lock()\n\n    def __iter__(self):\n        return self\n\n    def next(self):\n        with self.lock:\n            return self.iter.next()\n\n\ndef thread(func):\n    \"\"\"\n    Wrap function to execute within a single thread\n    \"\"\"\n\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        thr = Thread(target=func, args=args, kwargs=kwargs, daemon=True)\n        thr.start()\n\n    return wrapper\n\n\ndef process(func):\n    \"\"\"\n    Wrap function to execute within a single process\n    \"\"\"\n\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        proc = Process(target=func, args=args, kwargs=kwargs, daemon=True)\n        proc.start()\n\n    return wrapper\n\n\n@process\ndef daemonize(cmd, timeout=300):\n    \"\"\"\n    Run command in detached daemon process\n\n    :param cmd:     commands to run\n    :type cmd:      list\n    :param timeout: timeout before daemonized process quits\n    :type timeout:  int\n    :return:        no return value\n    :rtype:         None\n    \"\"\"\n\n    # decouple from parent environment\n    #os.chdir('/')\n    #os.setsid()\n    #os.umask(0)\n\n    # run command with new sid\n    # TODO: systemd tracks processes by cgroup and will kill these daemons when parent dies\n    #       os.unshare should accomplish this but is too new and only supported in python 3.12\n    #       back-porting would require compiling CPython modules or supporting multiple python versions?\n    #       alternatively we could ship a small ansi-C program that handles this\n    #       for now we workaround this in dsiprouter.sh but we should be more platform independent\n    subprocess.run(\n        cmd,\n        stdin=subprocess.DEVNULL,\n        stdout=subprocess.DEVNULL,\n        stderr=subprocess.DEVNULL,\n        timeout=timeout,\n        #start_new_session=True,\n        restore_signals=False,\n        #preexec_fn=lambda: os.unshare(os.CLONE_NEWCGROUP)\n    )\n\n\ndef mpexec(func, args=None, kwargs=None, workers=None, callback=None):\n    \"\"\"\n    Execute task within pool of processes\n\n    :param func:        callable function to execute\n    :param args:        list of arg lists for each function execution\n    :param kwargs:      list of kwarg dicts for each function execution\n    :param workers:     number of worker processes to use (or rounds to execute when args not provided)\n    :param callback:    callable function to execute after each completion\n    :return:            list of results returned by executing tasks\n    \"\"\"\n\n    with ProcessPoolExecutor(max_workers=workers) as executor:\n        # no args or kwargs, number of tasks = num workers\n        if args is None and kwargs is None:\n            workers = workers if workers is not None else 1\n            if callback:\n                tasks = [executor.submit(func).add_done_callback(callback) for _ in range(workers)]\n            else:\n                tasks = [executor.submit(func) for _ in range(workers)]\n        # args without kwargs\n        elif kwargs is None:\n            if callback:\n                tasks = [executor.submit(func, *func_args).add_done_callback(callback) for func_args in args]\n            else:\n                tasks = [executor.submit(func, *func_args) for func_args in args]\n        # args and kwargs\n        else:\n            if callback:\n                tasks = [executor.submit(func, *func_args, **func_kwargs).add_done_callback(callback) for func_args, func_kwargs in zip(args, kwargs)]\n            else:\n                tasks = [executor.submit(func, *func_args, **func_kwargs) for func_args, func_kwargs in zip(args, kwargs)]\n\n    return [task.result() for task in tasks]\n\n\ndef mtexec(func, args=None, kwargs=None, workers=None, callback=None):\n    \"\"\"\n    Execute task within pool of threads\n\n    :param func:        callable function to execute\n    :param args:        list of arg lists for each function execution\n    :param kwargs:      list of kwarg dicts for each function execution\n    :param workers:     number of worker processes to use (or rounds to execute when args not provided)\n    :param callback:    callable function to execute after each completion\n    :return:            list of results returned by executing tasks\n    \"\"\"\n\n    with ThreadPoolExecutor(max_workers=workers) as executor:\n        # no args or kwargs, number of tasks = num workers\n        if args is None and kwargs is None:\n            workers = workers if workers is not None else 1\n            if callback:\n                tasks = [executor.submit(func).add_done_callback(callback) for _ in range(workers)]\n            else:\n                tasks = [executor.submit(func) for _ in range(workers)]\n        # args without kwargs\n        elif kwargs is None:\n            if callback:\n                tasks = [executor.submit(func, *func_args).add_done_callback(callback) for func_args in args]\n            else:\n                tasks = [executor.submit(func, *func_args) for func_args in args]\n        # args and kwargs\n        else:\n            if callback:\n                tasks = [executor.submit(func, *func_args, **func_kwargs).add_done_callback(callback) for func_args, func_kwargs in zip(args, kwargs)]\n            else:\n                tasks = [executor.submit(func, *func_args, **func_kwargs) for func_args, func_kwargs in zip(args, kwargs)]\n\n    return [task.result() for task in tasks]\n"
  },
  {
    "path": "gui/util/security.py",
    "content": "# make sure the generated source files are imported instead of the template ones\nimport sys\n\nif sys.path[0] != '/etc/dsiprouter/gui':\n    sys.path.insert(0, '/etc/dsiprouter/gui')\n\nimport os, hashlib, binascii, string, ssl, OpenSSL, secrets, re\nfrom Crypto.Cipher import AES\nfrom Crypto.Random import get_random_bytes\nfrom shared import updateConfig, StatusCodes\nimport settings\n\n\n#\n# Notes on storing credentials:\n#\n# best practice is to store hash and salt of the credentials\n# when plaintext access is mandatory we can use symmetric encryption\n# when sending encrypted data over the net asymmetric encryption is preferred\n# if credentials need to be stored/accessed in a python module, they need encoded/decoded\n#\n\ndef urandomChars(length=32):\n    \"\"\"\n    Return printable characters from urandom\n\n    :param length:  number of bytes to return\n    :type length:   int\n    :return:        random characters\n    :rtype:         str\n    \"\"\"\n\n    chars = string.ascii_lowercase + string.ascii_uppercase + string.digits\n    return ''.join([chars[ord(os.urandom(1)) % len(chars)] for _ in range(length)])\n\n\nclass Credentials():\n    \"\"\"\n    Wrapper class for credential management functions\n    \"\"\"\n\n    SALT_LEN = 16\n    CREDS_MAX_LEN = 64\n    DK_LEN_DEFAULT = 48\n    HASH_ITERATIONS = 10000\n    # literals to make parsing from bash easier\n    HASHED_CREDS_ENCODED_MAX_LEN = 128\n    assert HASHED_CREDS_ENCODED_MAX_LEN == CREDS_MAX_LEN * 2\n\n    @staticmethod\n    def hashCreds(creds, salt=None, dklen=None):\n        \"\"\"\n        Hash credentials using pbkdf2_hmac with sha512 algorithm\n\n        :param creds:   byte string to hash\n        :type creds:    bytes|str\n        :param salt:    salt to hash creds with (hex encoded byte string or string)\n        :type salt:     bytes|str\n        :param dklen:   the derived key length to return as hash\n        :type dklen:    int\n        :return:        hash+salt as hex encoded byte string\n        :rtype:         bytes\n        \"\"\"\n        if isinstance(creds, bytes):\n            pass\n        elif isinstance(creds, str):\n            creds = creds.encode('utf-8')\n        else:\n            raise ValueError('credentials must be a string or hex encoded byte string')\n        if len(creds) > Credentials.CREDS_MAX_LEN:\n            raise ValueError('credentials must be {} bytes or less'.format(str(Credentials.CREDS_MAX_LEN)))\n\n        if salt is None:\n            salt = get_random_bytes(Credentials.SALT_LEN)\n        elif isinstance(salt, str):\n            salt = salt.encode('utf-8')\n        elif isinstance(salt, bytes):\n            salt = binascii.unhexlify(salt)\n        else:\n            raise ValueError('salt must be a string or hex encoded byte string')\n        if len(salt) != Credentials.SALT_LEN:\n            raise ValueError('salt must be {} bytes long'.format(str(Credentials.SALT_LEN)))\n\n        if dklen is None:\n            dklen = Credentials.DK_LEN_DEFAULT\n        elif not isinstance(dklen, int):\n            raise ValueError('dklen must be an integer')\n\n        hash = hashlib.pbkdf2_hmac('sha512', creds, salt, iterations=Credentials.HASH_ITERATIONS, dklen=dklen)\n        return binascii.hexlify(hash + salt)\n\n    @staticmethod\n    def setCreds(dsip_creds=b'', api_creds=b'', kam_creds=b'', mail_creds=b'',\n                 ipc_creds=b'', rootdb_creds=b'', sesh_creds=b''):\n        \"\"\"\n        Set secure credentials, either by hashing or encrypting\\n\n        Values must be within size limit and empty values are ignored\\n\n\n        :param dsip_creds:      dsiprouter admin password as byte string\n        :type dsip_creds:       bytes|str\n        :param api_creds:       dsiprouter api token as byte string\n        :type api_creds:        bytes|str\n        :param kam_creds:       kamailio db password as byte string\n        :type kam_creds:        bytes|str\n        :param mail_creds:      dsiprouter mail password as byte string\n        :type mail_creds:       bytes|str\n        :param ipc_creds:       dsiprouter ipc connection password as byte string\n        :type ipc_creds:        bytes|str\n        :param rootdb_creds:    root db user's password as byte string\n        :type rootdb_creds:     bytes|str\n        :param sesh_creds:      flask session manager key as byte string\n        :type sesh_creds:       bytes|str\n        :return:                None\n        :rtype:                 None\n        \"\"\"\n        fields = {}\n        local_fields = {}\n\n        if len(dsip_creds) > 0:\n            if len(dsip_creds) > Credentials.CREDS_MAX_LEN:\n                raise ValueError('dsiprouter credentials must be {} bytes or less'.format(str(Credentials.CREDS_MAX_LEN)))\n            fields['DSIP_PASSWORD'] = Credentials.hashCreds(dsip_creds)\n\n        if len(api_creds) > 0:\n            if len(api_creds) > Credentials.CREDS_MAX_LEN:\n                raise ValueError('api credentials must be {} bytes or less'.format(str(Credentials.CREDS_MAX_LEN)))\n            fields['DSIP_API_TOKEN'] = AES_CTR.encrypt(api_creds)\n\n        if len(kam_creds) > 0:\n            if len(kam_creds) > Credentials.CREDS_MAX_LEN:\n                raise ValueError('kamailio credentials must be {} bytes or less'.format(str(Credentials.CREDS_MAX_LEN)))\n            fields['KAM_DB_PASS'] = AES_CTR.encrypt(kam_creds)\n\n        if len(mail_creds) > 0:\n            if len(mail_creds) > Credentials.CREDS_MAX_LEN:\n                raise ValueError('mail credentials must be {} bytes or less'.format(str(Credentials.CREDS_MAX_LEN)))\n            fields['MAIL_PASSWORD'] = AES_CTR.encrypt(mail_creds)\n\n        if len(ipc_creds) > 0:\n            if len(ipc_creds) > Credentials.CREDS_MAX_LEN:\n                raise ValueError('ipc credentials must be {} bytes or less'.format(str(Credentials.CREDS_MAX_LEN)))\n            fields['DSIP_IPC_PASS'] = AES_CTR.encrypt(ipc_creds)\n\n        # some fields are not synced with DB (also not constrained by max length limitations)\n        if len(rootdb_creds) > 0:\n            local_fields['ROOT_DB_PASS'] = AES_CTR.encrypt(rootdb_creds)\n\n        if len(sesh_creds) > 0:\n            local_fields['DSIP_SESSION_KEY'] = AES_CTR.encrypt(sesh_creds)\n\n        # update settings based on where they are loaded from\n        if len(fields) > 0 or len(local_fields) > 0:\n            # update db settings\n            from database import updateDsipSettingsTable\n            # WARNING: if called after updating settings.py the session loader may import\n            #          incorrect connection credentials and fail to connect to the DB\n            updateDsipSettingsTable(fields)\n            # update file settings including the local fields\n            updateConfig(settings, dict(fields, **local_fields))\n\n\nclass AES_CTR():\n    \"\"\"\n    Wrapper class for pycrypto's AES256 functions in AES_CTR mode\n    \"\"\"\n\n    BLOCK_SIZE = 16\n    KEY_SIZE = 32\n    # literal to make parsing from bash easier\n    NONCE_SIZE = 8\n    assert NONCE_SIZE == BLOCK_SIZE // 2\n    AESCTR_CREDS_ENCODED_MAX_LEN = 144\n    assert AESCTR_CREDS_ENCODED_MAX_LEN == (Credentials.CREDS_MAX_LEN * 2) + (NONCE_SIZE * 2)\n\n    @staticmethod\n    def genKey(keyfile=settings.DSIP_PRIV_KEY):\n        with open(keyfile, 'wb') as f:\n            key = get_random_bytes(AES_CTR.KEY_SIZE)\n            f.write(key)\n\n    @staticmethod\n    def encrypt(byte_string, key_file=settings.DSIP_PRIV_KEY):\n        if isinstance(byte_string, str):\n            byte_string = byte_string.encode('utf-8')\n\n        with open(key_file, 'rb') as f:\n            key = f.read(AES_CTR.KEY_SIZE)\n\n        nonce = get_random_bytes(AES_CTR.NONCE_SIZE)\n        aes = AES.new(key, AES.MODE_CTR, nonce=nonce)\n        ct_bytes = aes.encrypt(byte_string)\n\n        return binascii.hexlify(nonce + ct_bytes)\n\n    @staticmethod\n    def decrypt(byte_string, key_file=settings.DSIP_PRIV_KEY, decode=True):\n        if isinstance(byte_string, str):\n            byte_string = byte_string.encode('utf-8')\n        byte_string = binascii.unhexlify(byte_string)\n\n        with open(key_file, 'rb') as f:\n            key = f.read(AES_CTR.KEY_SIZE)\n\n        nonce = byte_string[:AES_CTR.NONCE_SIZE]\n        aes = AES.new(key, AES.MODE_CTR, nonce=nonce)\n        pt_bytes = aes.decrypt(byte_string[AES_CTR.NONCE_SIZE:])\n\n        if decode:\n            return pt_bytes.decode('utf-8')\n        return pt_bytes\n\nclass APIToken:\n    token = None\n\n    def __init__(self, request):\n        if 'Authorization' in request.headers:\n            auth_header = request.headers.get('Authorization', None)\n            if auth_header is not None:\n                header_values = auth_header.split(' ')\n                if len(header_values) == 2:\n                    self.token = header_values[1]\n\n    def isValid(self):\n        try:\n            if self.token:\n                # Get Environment Variables if in debug mode\n                # This is the only case we allow plain text token comparison\n                if settings.DEBUG:\n                    settings.DSIP_API_TOKEN = os.getenv('DSIP_API_TOKEN', settings.DSIP_API_TOKEN)\n\n                # need to decrypt token\n                if isinstance(settings.DSIP_API_TOKEN, bytes):\n                    tokencheck = AES_CTR.decrypt(settings.DSIP_API_TOKEN)\n                else:\n                    tokencheck = settings.DSIP_API_TOKEN\n\n                return secrets.compare_digest(tokencheck, self.token)\n\n            return False\n        except:\n            return False\n\n\nclass CryptoLibInfo():\n    \"\"\"\n    Wrapper class to standardize and simplify gathering info about the system crypto libraries\n    \"\"\"\n\n    @staticmethod\n    def getOpenSSLVer():\n        \"\"\"\n        Get major, minor, patch release numbers as one int\n\n        :return: openssl release version\n        :rtype: int\n        \"\"\"\n        return int(''.join([str(x) for x in ssl.OPENSSL_VERSION_INFO[0:3]]))\n\n    @staticmethod\n    def getSupportedSSLProtocols():\n        \"\"\"\n        Get the support SSL/TLS protocols we serve\n\n        :return: all support protocols\n        :rtype: list\n        \"\"\"\n\n        ssl_ver = CryptoLibInfo.getOpenSSLVer()\n        if ssl_ver < 101:\n            return [OpenSSL.SSL.TLSv1_METHOD]\n        elif ssl_ver < 111:\n            return [OpenSSL.SSL.TLSv1_1_METHOD, OpenSSL.SSL.TLSv1_2_METHOD]\n        else:\n            # TODO: pyOpenSSL does not expose TLSv1.3 support yet\n            # ref: https://github.com/pyca/pyopenssl/issues/860\n            return [OpenSSL.SSL.TLSv1_2_METHOD]\n\n\n# TODO: move to the standard library implementation \"cryptography\" module\nclass KeyCertPair():\n    \"\"\"\n    Represents a private key and cert(s) pair\n    \"\"\"\n\n    X509_DER_FILESIG = b'0\\x82'\n    X509_PEM_FILESIG = b'-----BEGIN CERTIFICATE-----'\n    X509_PEM_CERT_BEGIN = b'-----BEGIN CERTIFICATE-----'\n    X509_PEM_CERT_END = b'-----END CERTIFICATE-----'\n\n    def __init__(self, files):\n        \"\"\"\n        Files to extract key/cert pair from\n\n        :param files: A list of filepaths or objects implementing the read method\n        :type files: list[str|IO]|tuple[str|IO]\n        \"\"\"\n\n        if not isinstance(files, (list, tuple)):\n            raise ValueError('files must be provided via a list or tuple')\n\n        # we can accept key/cert pairs in one file or two separate files\n        num_files = len(files)\n        if num_files == 1:\n            buff = KeyCertPair.readFile(files[0])\n            self.pkey = KeyCertPair.convertKeyBuffToPkey(buff)\n            self.certs = KeyCertPair.convertCertBuffToX509List(buff)\n        elif num_files == 2:\n            for file in files:\n                buff = KeyCertPair.readFile(file)\n                # we don't know which file is which so try each type one at a time\n                try:\n                    self.pkey = KeyCertPair.convertKeyBuffToPkey(buff)\n                    continue\n                except ValueError:\n                    pass\n                try:\n                    self.certs = KeyCertPair.convertCertBuffToX509List(buff)\n                    continue\n                except ValueError:\n                    pass\n        else:\n            raise ValueError(\"key/cert pairs must be in a single file or two files\")\n\n    @staticmethod\n    def readFile(file):\n        \"\"\"\n        Read a file via filepath or using the object's read method\n        \"\"\"\n\n        if isinstance(file, str):\n            with open(file, 'rb') as fp:\n                return fp.read()\n        elif hasattr(file, 'read'):\n            return file.read()\n        else:\n            raise ValueError('files must contain filepaths or file pointers')\n\n    @staticmethod\n    def convertPKCS7CertToX509List(pkcs7_cert):\n        \"\"\"\n        Modified version of: https://github.com/pyca/pyopenssl/pull/367/files#r67300900 \\n\n        Returns all certificates for the PKCS7 structure, if present\n\n        :param pkcs7_cert: the PKCS cert object\n        :type pkcs7_cert: OpenSSL.crypto.PKCS7\n        :return: The certificates in PEM format\n        :rtype: list[OpenSSL.crypto.X509]\n        \"\"\"\n\n        if not isinstance(pkcs7_cert, OpenSSL.crypto.PKCS7):\n            raise ValueError('Invalid PKCS7 formatted certificate')\n\n        certs = OpenSSL.crypto._ffi.NULL\n\n        if pkcs7_cert.type_is_signed():\n            certs = pkcs7_cert._pkcs7.d.sign.cert\n        elif pkcs7_cert.type_is_signedAndEnveloped():\n            certs = pkcs7_cert._pkcs7.d.signed_and_enveloped.cert\n\n        pycerts = []\n        for i in range(OpenSSL.crypto._lib.sk_X509_num(certs)):\n            pycert = OpenSSL.crypto.X509.__new__(OpenSSL.crypto.X509)\n            pycert._x509 = OpenSSL.crypto._lib.X509_dup(OpenSSL.crypto._lib.sk_X509_value(certs, i))\n            pycerts.append(pycert)\n\n        if len(pycerts) == 0:\n            raise ValueError('Invalid PKCS7 formatted certificate')\n\n        return pycerts\n\n    @staticmethod\n    def convertKeyBuffToPkey(buff):\n        \"\"\"\n        Convert a single private key of any format to PKey\n\n        :param buff: private key buffer of unknown format\n        :type buff: bytes\n        :return: private key in OpenSSL usable format, type of\n        :rtype: OpenSSL.crypto.PKey\n        :raises: ValueError if the data can not be converted\n        \"\"\"\n\n        if not isinstance(buff, bytes):\n            raise ValueError('buffer must be bytes')\n\n        # try loading each type of file until we find the format that works\n        # PEM encoded private key\n        try:\n            return OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, buff)\n        except:\n            pass\n        # ASN1/DER encoded private key\n        try:\n            return OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_ASN1, buff)\n        except:\n            pass\n        # PKCS12 formatted file\n        try:\n            return OpenSSL.crypto.load_pkcs12(buff).get_privatekey()\n        except:\n            pass\n\n        raise ValueError('could not convert private key to PKey')\n\n    @staticmethod\n    def convertCertBuffToX509List(buff):\n        \"\"\"\n        Convert a one or more certificates of any format to X509\n\n        :param buff: certificate(s) buffer of unknown format\n        :type buff: bytes\n        :return: certificate(s) in OpenSSL usable format\n        :rtype: list[OpenSSL.crypto.X509]\n        :raises: ValueError if the data can not be converted\n        \"\"\"\n\n        if not isinstance(buff, bytes):\n            raise ValueError('buffer must be bytes')\n\n        # try loading each type of file until we find the format that works\n        try:\n            # PEM encoded certificate(s)\n            if buff[0:len(KeyCertPair.X509_PEM_FILESIG)] == KeyCertPair.X509_PEM_FILESIG:\n                cert_regex = KeyCertPair.X509_PEM_CERT_BEGIN + rb'.*?' + KeyCertPair.X509_PEM_CERT_END\n                certs = []\n                for cert_bytes in re.findall(cert_regex, buff, flags=re.DOTALL):\n                    certs.append(OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_bytes))\n                return certs\n            # ASN1/DER encoded certificate(s)\n            if buff[0:len(KeyCertPair.X509_DER_FILESIG)] == KeyCertPair.X509_DER_FILESIG:\n                # return [OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, buff)]\n                raise NotImplementedError('parsing DER encoded certificates is not supported, convert to PEM encoding and try again')\n        except OpenSSL.crypto.Error:\n            raise ValueError('could not convert certificate(s) to X509 list')\n        try:\n            pkcs12_obj = OpenSSL.crypto.load_pkcs12(buff)\n            certs = [pkcs12_obj.get_certificate()]\n            certs.extend(pkcs12_obj.get_ca_certificates())\n            return certs\n        except:\n            pass\n        try:\n            pkcs7_cert = OpenSSL.crypto.load_pkcs7_data(OpenSSL.crypto.FILETYPE_PEM, buff)\n            return KeyCertPair.convertPKCS7CertToX509List(pkcs7_cert)\n        except:\n            pass\n        try:\n            pkcs7_cert = OpenSSL.crypto.load_pkcs7_data(OpenSSL.crypto.FILETYPE_ASN1, buff)\n            return KeyCertPair.convertPKCS7CertToX509List(pkcs7_cert)\n        except:\n            pass\n\n        raise ValueError('could not convert certificate(s) to X509 list')\n\n    @staticmethod\n    def getCertSubjectPrintable(cert):\n        return str(cert.get_subject())[18:-2]\n\n    def validateKeyCertPair(self):\n        \"\"\"\n        Check if the key/cert pair is valid\n\n        :raises: ValueError when invalid\n        \"\"\"\n\n        for cert in self.certs:\n            if cert.has_expired():\n                raise ValueError('certificate with subject \"{}\" is expired'.format(KeyCertPair.getCertSubjectPrintable(cert)))\n\n        try:\n            ctx = OpenSSL.SSL.Context(CryptoLibInfo.getSupportedSSLProtocols()[0])\n            ctx.use_privatekey(self.pkey)\n            ctx.use_certificate(self.certs[0])\n            for cert in self.certs[1:]:\n                ctx.add_extra_chain_cert(cert)\n            ctx.check_privatekey()\n        except:\n            raise ValueError('Private key does not match x509 certificates supplied')\n\n    def dumpPkey(self, encoding=OpenSSL.crypto.FILETYPE_PEM):\n        return OpenSSL.crypto.dump_privatekey(encoding, self.pkey)\n\n    def dumpCerts(self, encoding=OpenSSL.crypto.FILETYPE_PEM):\n        return b'\\n'.join([OpenSSL.crypto.dump_certificate(encoding, cert) for cert in self.certs])\n"
  },
  {
    "path": "gui/util/time_funcs.py",
    "content": "'''\n@summary: Provides methods for working with datetimes, timestamps, etc..\n@author: devopsec\n'''\n\nimport pytz\nfrom datetime import datetime, timezone\n\ndef convert_ts(ts, millis=False, is_utc=False):\n    '''\n    convert timestamp to human readable format\n    optionally keep up to microsecond precision\n    if timestamp is utc format pass param is_utc=True\n    '''\n\n    # if ts is not in string format\n    if not type(ts).__name__ == 'str':\n        ts = str(ts)\n\n    # if contains ts contains millis and millis is false, strip them #\n    formatted_ts = None # create var above routine\n    # TODO: add try catch error handling\n    if len(ts) > 10:\n        if millis == False:\n            replace = len(ts) - 10\n            ts = ts[:-replace]\n            if is_utc == True:\n                formatted_ts = datetime.fromtimestamp(int(ts), tz=pytz.utc).strftime('%Y-%m-%d %H:%M:%S')\n            else:\n                formatted_ts = datetime.fromtimestamp(int(ts)).strftime('%Y-%m-%d %H:%M:%S')\n\n        else: # convert w/ millis and return\n            if not '.' in ts: # needs . to distinguish millis\n                ts = ts[0:10] + '.' + ts[10:]\n            if is_utc == True:\n                formatted_ts = datetime.fromtimestamp(float(ts), tz=pytz.utc).strftime('%Y-%m-%d %H:%M:%S.%f')\n            else:\n                formatted_ts = datetime.fromtimestamp(float(ts)).strftime('%Y-%m-%d %H:%M:%S.%f')\n    else: # convert w/o millis and return\n        if is_utc == True:\n            formatted_ts = datetime.fromtimestamp(int(ts), tz=pytz.utc).strftime('%Y-%m-%d %H:%M:%S')\n        else:\n            formatted_ts = datetime.fromtimestamp(int(ts)).strftime('%Y-%m-%d %H:%M:%S')\n\n    return formatted_ts\n\n# DEBUG\n# print(convert_ts(\"1486782196652\"))\n# print(convert_ts(\"1486782196\"))\n# print(convert_ts(1486782196.652655, millis=True))\n# print(convert_ts(\"1486782196652655\", millis=True))\n# print(convert_ts(\"1486782196652655\", is_utc=True))\n\n# TODO: add try catch error handling\ndef utcnow(format=\"ts\"):\n    ''' returns time-aware utc datetime object or timestamp of current time/date\n        @Param: format  -  defaults to timestamp, provide \"dt\" for datetime obj '''\n\n    dt = datetime.now(tz=pytz.utc)\n    if format == \"dt\":\n        return dt\n    else: # timestamp includes millis down to 6th decimal place\n        ts = str(dt.timestamp())\n        return ts.replace('.', '')\n\n# DEBUG\n# print(utcnow())\n# print(utcnow(\"dt\"))\n\n# TODO add methods for converting to client local timezone dynamically\n"
  },
  {
    "path": "kamailio/almalinux/8.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local KAM_MINOR_VERSION=$(perl -pe 's%^([0-9])\\.([0-9]).*$%\\1.\\2%' <<<\"$KAM_VERSION\")\n    local RHEL_BASE_VER=$(rpm -E %{rhel})\n    local NPROC=$(nproc)\n\n    # Install Dependencies\n    dnf config-manager --enable powertools &&\n    dnf install -y epel-release &&\n    dnf groupinstall --setopt=group_package_types=mandatory,default,optional -y 'core' &&\n    dnf groupinstall --setopt=group_package_types=mandatory,default,optional -y 'base' &&\n    dnf groupinstall --setopt=group_package_types=mandatory,default,optional -y 'Development Tools' &&\n    dnf install -y psmisc curl wget sed gawk vim perl firewalld logrotate rsyslog \\\n        uuid openssl-devel libatomic libuuid-devel libjwt-devel bzip2-devel libffi-devel libcurl-devel \\\n        python3.11 python3.11-pip policycoreutils-python-utils\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        exit 1\n    fi\n\n    # we need a newer version of certbot than the distro repos offer\n    dnf remove -y *certbot*\n    python3 -m venv --upgrade-deps /opt/certbot/\n    /opt/certbot/bin/pip install certbot\n    ln -sf /opt/certbot/bin/certbot /usr/bin/certbot\n\n    dnf install -y kernel-modules-extra-$(uname -r) || {\n        printwarn 'could not install kernel modules for current kernel'\n        echo 'upgrading kernel and installing new modules'\n        printwarn 'you will need to reboot the machine for changes to take effect'\n        dnf install -y kernel-modules-extra\n    }\n\n    if (( $? == 0 )); then\n        echo 'sctp' >/etc/modules-load.d/sctp.conf\n        sed -i -re 's%^blacklist sctp%#blacklist sctp%g' /etc/modprobe.d/*\n        modprobe sctp\n    else\n        printwarn 'Could not install kernel modules for SCTP support. Continuing installation...'\n    fi\n\n    # create kamailio user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    # Add the Kamailio repos to yum\n    (cat << EOF\n[kamailio]\nname=Kamailio\nbaseurl=https://rpm.kamailio.org/centos/${RHEL_BASE_VER}/${KAM_MINOR_VERSION}/${KAM_VERSION}/\\$basearch/\nenabled=1\nmetadata_expire=30d\ngpgcheck=1\nrepo_gpgcheck=0\ngpgkey=https://rpm.kamailio.org/rpm-pub.key\ntype=rpm\nEOF\n    ) > /etc/yum.repos.d/kamailio.repo\n\n    dnf clean -y metadata\n    dnf makecache -y\n    dnf install -y kamailio kamailio-ldap kamailio-mysql kamailio-sipdump kamailio-websocket \\\n        kamailio-postgresql kamailio-debuginfo kamailio-xmpp kamailio-unixodbc kamailio-utils kamailio-tls \\\n        kamailio-presence kamailio-outbound kamailio-gzcompress kamailio-http_async_client kamailio-dmq_userloc \\\n        kamailio-sctp\n\n    # workaround for kamailio rpm transaction failures\n    if (( $? != 0 )); then\n        rpm --import $(grep 'gpgkey' /etc/yum.repos.d/kamailio.repo | cut -d '=' -f 2)\n        REPOS='kamailio kamailio-ldap kamailio-mysql kamailio-postgresql kamailio-debuginfo kamailio-xmpp kamailio-unixodbc kamailio-utils kamailio-tls kamailio-presence kamailio-outbound kamailio-gzcompress'\n        for REPO in $REPOS; do\n            yum install -y $(grep 'baseurl' /etc/yum.repos.d/kamailio.repo | cut -d '=' -f 2)$(uname -m)/$(repoquery -i ${REPO} | head -4 | tail -n 3 | tr -d '[:blank:]' | cut -d ':' -f 2 | perl -pe 'chomp if eof' | tr '\\n' '-').$(uname -m).rpm\n        done\n    fi\n\n    # get info about the kamailio install for later use in script\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    touch /etc/tmpfiles.d/kamailio.conf\n    echo \"d /run/kamailio 0750 kamailio users\" > /etc/tmpfiles.d/kamailio.conf\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR}\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    (cat << EOF\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n# STORE_PLAINTEXT_PW=0\nEOF\n    ) > ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create\n\n    # give kamailio permissions in SELINUX\n    semanage port -a -t sip_port_t -p udp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIPS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIPS_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_WSS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_WSS_PORT}\n    semanage port -a -t sip_port_t -p udp ${KAM_DMQ_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_DMQ_PORT}\n\n    # Start firewalld\n    systemctl start firewalld\n    systemctl enable firewalld\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Make sure MariaDB and Local DNS start before Kamailio\n    if ! grep -q v 'mariadb.service dnsmasq.service' /lib/systemd/system/kamailio.service 2>/dev/null; then\n        sed -i -r -e 's/(After=.*)/\\1 mariadb.service dnsmasq.service/' /lib/systemd/system/kamailio.service\n    fi\n    if ! grep -q v \"${DSIP_PROJECT_DIR}/dsiprouter.sh updatednsconfig\" /lib/systemd/system/kamailio.service 2>/dev/null; then\n        sed -i -r -e \"0,\\|^ExecStart.*|{s||ExecStartPre=-${DSIP_PROJECT_DIR}/dsiprouter.sh updatednsconfig\\n&|}\" /lib/systemd/system/kamailio.service\n    fi\n    systemctl daemon-reload\n\n    # Enable Kamailio for system startup\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-bundle.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libks\n    if [[ ! -d ${SRC_DIR}/libks ]]; then\n        git clone --single-branch -c advice.detachedHead=false https://github.com/signalwire/libks -b v1.8.3 ${SRC_DIR}/libks\n    fi\n    (\n        cd ${SRC_DIR}/libks &&\n        cmake -DCMAKE_BUILD_TYPE=Release . &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libks'\n        return 1\n    }\n\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    (\n        cd ${SRC_DIR}/libstirshaken &&\n        ./bootstrap.sh &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 &&\n        make -j $NPROC &&\n        make -j $NPROC install &&\n        ldconfig\n    ) || {\n        printerr 'Failed to compile and install libstirshaken'\n        return 1\n    }\n\n    ## compile and install STIR/SHAKEN module\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/stirshaken &&\n        make -j $NPROC\n    ) &&\n    cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/ || {\n        printerr 'Failed to compile and install STIR/SHAKEN module'\n        return 1\n    }\n\n    # patch uac module to support reload_delta\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/uac &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/uac.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/uac/uac.so ${KAM_MODULES_DIR}/\n    ) || {\n        printerr 'Failed to patch uac module'\n        return 1\n    }\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop servers\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR} ${SYSTEM_KAMAILIO_CONFIG_DIR}.bak.$(date +%Y%m%d_%H%M%S)\n\n    # Uninstall Kamailio modules\n    yum remove -y kamailio\\*\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "kamailio/almalinux/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local KAM_MINOR_VERSION=$(perl -pe 's%^([0-9])\\.([0-9]).*$%\\1.\\2%' <<<\"$KAM_VERSION\")\n    local RHEL_BASE_VER=$(rpm -E %{rhel})\n    local NPROC=$(nproc)\n\n    # Install Dependencies\n    dnf install -y epel-release &&\n    {\n        # TODO: fix upstream kamailio.repo file\n        #dnf config-manager -y --add-repo https://rpm.kamailio.org/centos/kamailio.repo &&\n        #dnf config-manager --disable 'kamailio*' &&\n        #dnf config-manager --enable \"kamailio-$KAM_VERSION_DOTTED\" &&\n\n        # Add the Kamailio repos to yum\n        (cat <<EOF\n[kamailio]\nname=Kamailio\nbaseurl=https://rpm.kamailio.org/centos/${RHEL_BASE_VER}/${KAM_MINOR_VERSION}/${KAM_VERSION}/\\$basearch/\nenabled=1\nmetadata_expire=30d\ngpgcheck=1\nrepo_gpgcheck=0\ngpgkey=https://rpm.kamailio.org/rpm-pub.key\ntype=rpm\nEOF\n        ) >/etc/yum.repos.d/kamailio.repo &&\n        dnf makecache -y\n    } &&\n    dnf groupinstall -y 'core' &&\n    dnf groupinstall -y 'base' &&\n    dnf groupinstall -y 'Development Tools' &&\n    dnf install -y git curl perl firewalld logrotate rsyslog certbot cmake libuuid-devel \\\n        libcurl-devel libjwt-devel libatomic openssl-devel policycoreutils-python-utils \\\n        libks-devel\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    dnf install -y kernel-modules-extra-$(uname -r) || {\n        printwarn 'could not install kernel modules for current kernel'\n        echo 'upgrading kernel and installing new modules'\n        printwarn 'you will need to reboot the machine for changes to take effect'\n        dnf install -y kernel-modules-extra\n    }\n\n    if (( $? == 0 )); then\n        echo 'sctp' >/etc/modules-load.d/sctp.conf\n        sed -i -re 's%^blacklist sctp%#blacklist sctp%g' /etc/modprobe.d/*\n        modprobe sctp\n    else\n        printwarn 'Could not install kernel modules for SCTP support. Continuing installation...'\n    fi\n\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel kamailio &>/dev/null; groupdel kamailio &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n\n    dnf install -y kamailio kamailio-ldap kamailio-mysql kamailio-sipdump kamailio-websocket kamailio-postgresql kamailio-debuginfo \\\n        kamailio-xmpp kamailio-unixodbc kamailio-utils kamailio-tls kamailio-presence kamailio-outbound kamailio-gzcompress \\\n        kamailio-http_async_client kamailio-dmq_userloc kamailio-jansson kamailio-json kamailio-uuid kamailio-sctp\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing kamailio packages'\n        return 1\n    fi\n\n    # get info about the kamailio install for later use in script\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    # make sure run dir exists\n    mkdir -p /var/run/kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n\n    touch /etc/tmpfiles.d/kamailio.conf\n    echo \"d /run/kamailio 0750 kamailio users\" > /etc/tmpfiles.d/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR} ${BACKUPS_DIR}/kamailio\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${BACKUPS_DIR}/kamailio/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    cat <<EOF >${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n#STORE_PLAINTEXT_PW=0\nEOF\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create\n\n    # give kamailio permissions in SELINUX\n    semanage port -a -t sip_port_t -p udp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIPS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIPS_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_WSS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_WSS_PORT}\n    semanage port -a -t sip_port_t -p udp ${KAM_DMQ_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_DMQ_PORT}\n\n    # Start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Configure Kamailio systemd service\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio-v2.service /lib/systemd/system/kamailio.service\n    chmod 644 /lib/systemd/system/kamailio.service\n    systemctl daemon-reload\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-bundle.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    (\n        cd ${SRC_DIR}/libstirshaken &&\n        ./bootstrap.sh &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 &&\n        make -j $NPROC CFLAGS='-Wno-deprecated-declarations' &&\n        make -j $NPROC install &&\n        ldconfig\n    ) || {\n        printerr 'Failed to compile and install libstirshaken'\n        return 1\n    }\n\n    ## compile and install STIR/SHAKEN module\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/stirshaken &&\n        make -j $NPROC\n    ) &&\n    cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/ || {\n        printerr 'Failed to compile and install STIR/SHAKEN module'\n        return 1\n    }\n\n    # patch uac module to support reload_delta\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/uac &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/uac.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/uac/uac.so ${KAM_MODULES_DIR}/\n    ) || {\n        printerr 'Failed to patch uac module'\n        return 1\n    }\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop servers\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR} ${SYSTEM_KAMAILIO_CONFIG_DIR}.bak.$(date +%Y%m%d_%H%M%S)\n\n    # Uninstall Kamailio modules\n    dnf remove -y kamailio\\*\n\n    # remove our selinux changes\n    semanage port -D -t sip_port_t -p udp\n    semanage port -D -t sip_port_t -p tcp\n    semanage port -D -t rabbitmq_port_t -p udp\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "kamailio/amzn/2.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local KAM_VERSION_DOTTED RHEL_BASE_VER NPROC\n\n    # Install Dependencies\n    amazon-linux-extras install -y epel >/dev/null &&\n    yum install -y yum-utils &&\n    yum groupinstall --setopt=group_package_types=mandatory,default -y 'Development Tools' &&\n    yum install -y psmisc curl wget sed gawk vim perl firewalld logrotate rsyslog cmake3 gcc10 \\\n        uuid-devel libtool jansson-devel libuuid-devel libcurl-devel libjwt-devel libatomic \\\n        bzip2-devel libffi-devel policycoreutils-python\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        exit 1\n    fi\n\n    # hardcoded to the latest release available for centos (patch updates broken)\n    KAM_VERSION_DOTTED='5.7.4'\n    RHEL_BASE_VER=$(rpm -E %{rhel})\n    NPROC=$(nproc)\n\n    # link latest version of cmake\n    ln -sf $(which cmake3) /usr/local/bin/cmake\n\n    ## compile and install openssl v1.1.1 (workaround for amazon linux repo conflicts)\n    ## we must overwrite system packages (openssl/openssl-devel) otherwise python's openssl package is not supported\n    if [[ \"$(openssl version 2>/dev/null | awk '{print $2}')\" != \"1.1.1w\" ]]; then\n        if [[ ! -d ${SRC_DIR}/openssl ]]; then\n            ( cd ${SRC_DIR} &&\n            curl -sL https://www.openssl.org/source/openssl-1.1.1w.tar.gz 2>/dev/null |\n            tar -xzf - --transform 's%openssl-1.1.1w%openssl%'; )\n        fi\n        (\n            cd ${SRC_DIR}/openssl &&\n            ./Configure --prefix=/usr linux-$(uname -m) &&\n            make -j $NPROC &&\n            make -j $NPROC install\n        ) || {\n            printerr 'Failed to compile openssl'\n            return 1\n        }\n    fi\n\n    # python 3.8 or higher is required\n    # if not installed already, install it now\n    if [[ \"$(python3 -V 2>/dev/null | cut -d ' ' -f 2)\" != \"3.9.18\" ]]; then\n        # installation / compilation never completed, start it now\n        if [[ ! -d \"${SRC_DIR}/Python-3.9.18\" ]]; then\n            (\n                cd ${SRC_DIR} &&\n                curl -s -o Python-3.9.18.tgz https://www.python.org/ftp/python/3.9.18/Python-3.9.18.tgz &&\n                tar -xf Python-3.9.18.tgz &&\n                rm -f Python-3.9.18.tgz\n            )\n        fi\n        (\n            cd ${SRC_DIR} &&\n            cd Python-3.9.18/ &&\n            ./configure --enable-optimizations CFLAGS=-I${SRC_DIR}/openssl/include LDFLAGS=-L${SRC_DIR}/openssl &&\n            make -j $NPROC &&\n            make -j $NPROC install\n        ) || {\n            printerr 'Failed to compile and install required python version'\n            return 1\n        }\n        python3 -m pip install -U pip setuptools || {\n            printerr 'Failed to update pip and setuptools'\n            return 1\n        }\n    fi\n\n    # we need a newer version of certbot than the distro repos offer\n    yum remove -y *certbot*\n    python3 -m venv /opt/certbot/\n    /opt/certbot/bin/pip install --upgrade pip\n    /opt/certbot/bin/pip install certbot\n    ln -sf /opt/certbot/bin/certbot /usr/bin/certbot\n\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel kamailio &>/dev/null; groupdel kamailio &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n\n    # TODO: amzn2 should be based off rhel not centos but the kamailio rhel repos are out of date\n    yum-config-manager -y --add-repo https://rpm.kamailio.org/centos/kamailio.repo &&\n    echo $RHEL_BASE_VER >/etc/yum/vars/rhelver &&\n    perl -i -pe 's%\\$releasever%\\$rhelver%g' /etc/yum.repos.d/kamailio.repo\n    yum-config-manager --disable 'kamailio*' >/dev/null &&\n    yum-config-manager --enable \"kamailio-$KAM_VERSION_DOTTED\" >/dev/null &&\n    yum install -y kamailio kamailio-ldap kamailio-mysql kamailio-sipdump kamailio-websocket kamailio-postgresql kamailio-debuginfo \\\n        kamailio-xmpp kamailio-unixodbc kamailio-utils kamailio-tls kamailio-presence kamailio-outbound kamailio-gzcompress \\\n        kamailio-http_async_client kamailio-dmq_userloc kamailio-jansson kamailio-json kamailio-uuid kamailio-sctp\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing kamailio packages'\n        exit 1\n    fi\n\n    # enable sctp\n    echo 'sctp' >/etc/modules-load.d/sctp.conf\n    sed -i -re 's%^blacklist sctp%#blacklist sctp%g' /etc/modprobe.d/*\n    modprobe sctp\n\n    # get info about the kamailio install for later use in script\n    KAM_VERSION_FULL=$(kamailio -v 2>/dev/null | awk '/^version:/ {print $3}')\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    # make sure run dir exists\n    mkdir -p /var/run/kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n    # create kamailio tmp files\n    echo \"d /run/kamailio 0750 kamailio kamailio\" > /etc/tmpfiles.d/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR} ${BACKUPS_DIR}/kamailio\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${BACKUPS_DIR}/kamailio/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    cat <<EOF >${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n#STORE_PLAINTEXT_PW=0\nEOF\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create\n\n    # give kamailio permissions in SELINUX\n    semanage port -a -t sip_port_t -p udp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIPS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIPS_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_WSS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_WSS_PORT}\n    semanage port -a -t sip_port_t -p udp ${KAM_DMQ_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_DMQ_PORT}\n\n    # Start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    if (( $? != 0 )); then\n        # fix for bug: https://bugzilla.redhat.com/show_bug.cgi?id=1575845\n        systemctl restart dbus\n        systemctl restart firewalld\n        # fix for ensuing bug: https://bugzilla.redhat.com/show_bug.cgi?id=1372925\n        systemctl restart systemd-logind\n    fi\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Configure Kamailio systemd service\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio-v1.service /lib/systemd/system/kamailio.service\n    chmod 644 /lib/systemd/system/kamailio.service\n    systemctl daemon-reload\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-bundle.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libjwt\n    #if [[ ! -d ${SRC_DIR}/libjwt ]]; then\n    #    git clone --depth 1 -c advice.detachedHead=false https://github.com/benmcollins/libjwt.git ${SRC_DIR}/libjwt\n    #fi\n    #( cd ${SRC_DIR}/libjwt && autoreconf -i && ./configure --prefix=/usr --libdir=/usr/lib64 && make && make install; exit $?; ) ||\n    #{ printerr 'Failed to compile and install libjwt'; return 1; }\n\n    ## compile and install libks\n    if [[ ! -d ${SRC_DIR}/libks ]]; then\n        git clone --single-branch -c advice.detachedHead=false https://github.com/signalwire/libks -b v1.8.3 ${SRC_DIR}/libks\n    fi\n    (\n        cd ${SRC_DIR}/libks &&\n        cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release . &&\n        make -j $NPROC &&\n        make -j $NPROC install &&\n        ln -sft /usr/lib64/ /usr/lib/libks.so* &&\n        ln -sft /usr/lib64/pkgconfig/ /usr/lib/pkgconfig/libks.pc\n    ) || {\n        printerr 'Failed to compile and install libks'\n        return 1\n    }\n\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    (\n        cd ${SRC_DIR}/libstirshaken &&\n        ./bootstrap.sh &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 CC=/bin/gcc10-gcc CXX=/usr/bin/gcc10-g++ \\\n            CFLAGS=-I${SRC_DIR}/openssl/include LDFLAGS=-L${SRC_DIR}/openssl \\\n            PKG_CONFIG_PATH=${SRC_DIR}/openssl &&\n        make -j $NPROC CFLAGS='-Wno-deprecated-declarations' &&\n        make -j $NPROC install &&\n        ldconfig\n    ) || {\n        printerr 'Failed to compile and install libstirshaken'\n        return\n    }\n\n    ## compile and install STIR/SHAKEN module\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION_FULL}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION_FULL} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION_FULL} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/stirshaken &&\n        make -j $NPROC\n    ) &&\n    cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/ || {\n        printerr 'Failed to compile and install STIR/SHAKEN module'\n        return 1\n    }\n\n    # patch htable module to support coldelim/colnull on kamailio v5.7.x\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/htable &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/htable-kam57.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/htable/htable.so ${KAM_MODULES_DIR}/\n    )\n    if (( $? != 0 )); then\n        printerr 'Failed to patch htable module'\n        return 1\n    fi\n\n    # patch uac module to support reload_delta\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/uac &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/uac.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/uac/uac.so ${KAM_MODULES_DIR}/\n    )\n    if (( $? != 0 )); then\n        printerr 'Failed to patch uac module'\n        return 1\n    fi\n\n    return 0\n}\n\nfunction uninstall() {\n    # Stop servers\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    cp -rf ${SYSTEM_KAMAILIO_CONFIG_DIR}/. ${BACKUPS_DIR}/kamailio/\n    rm -rf ${SYSTEM_KAMAILIO_CONFIG_DIR}\n\n    # Uninstall Stirshaken Required Packages\n    ( cd ${SRC_DIR}/libjwt; make uninstall; exit $?; ) && rm -rf ${SRC_DIR}/libjwt\n    ( cd ${SRC_DIR}/libks; make uninstall; exit $?; ) && { rm -rf ${SRC_DIR}/libks; rm -f /usr/lib64/{,pkgconfig/}libks*; }\n    ( cd ${SRC_DIR}/libstirshaken; make uninstall; exit $?; ) && rm -rf ${SRC_DIR}/libstirshaken\n    rm -rf ${SRC_DIR}/openssl\n    rm -rf ${SRC_DIR}/kamailio\n\n    # Uninstall Kamailio modules\n    yum remove -y kamailio\\*\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "kamailio/centos/7.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local KAM_MINOR_VERSION=$(perl -pe 's%^([0-9])\\.([0-9]).*$%\\1.\\2%' <<<\"$KAM_VERSION\")\n    local RHEL_BASE_VER=$(rpm -E %{rhel})\n    local NPROC=$(nproc)\n\n    # Install Dependencies\n    yum groupinstall -y 'Development Tools' &&\n    yum install -y epel-release &&\n    yum install -y centos-release-scl &&\n    yum install -y git curl perl gawk sed vim firewalld logrotate rsyslog cmake3 \\\n        policycoreutils-python devtoolset-11 libcurl-devel libjwt-devel libatomic \\\n        uuid-devel jansson-devel libuuid-devel bzip2-devel libffi-devel libtool\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # enable the newer development toolchain\n    source scl_source enable devtoolset-11\n    # symlink cmake to cmake3\n    ln -sf $(which cmake3) /usr/local/bin/cmake\n\n    # sctp support\n    echo 'sctp' >/etc/modules-load.d/sctp.conf\n    sed -i -re 's%^blacklist sctp%#blacklist sctp%g' /etc/modprobe.d/*\n    modprobe sctp\n\n    ## compile and install openssl v1.1.1 (repo versions too old)\n    ## we must overwrite system packages (openssl/openssl-devel) otherwise python's openssl package is not supported\n    if [[ \"$(openssl version 2>/dev/null | awk '{print $2}')\" != \"1.1.1w\" ]]; then\n        if [[ ! -d ${SRC_DIR}/openssl ]]; then\n            ( cd ${SRC_DIR} &&\n            curl -sL https://www.openssl.org/source/openssl-1.1.1w.tar.gz 2>/dev/null |\n            tar -xzf - --transform 's%openssl-1.1.1w%openssl%'; )\n        fi\n        (\n            cd ${SRC_DIR}/openssl &&\n            ./Configure --prefix=/usr linux-$(uname -m) &&\n            make -j $NPROC &&\n            make -j $NPROC install\n        )\n        if (( $? != 0 )); then\n            printerr 'Failed to compile openssl'\n            return 1\n        fi\n    fi\n\n    # python 3.8 or higher is required\n    # if not installed already, install it now\n    if [[ \"$(python3 -V 2>/dev/null | cut -d ' ' -f 2)\" != \"3.9.18\" ]]; then\n        # installation / compilation never completed, start it now\n        if [[ ! -d \"${SRC_DIR}/Python-3.9.18\" ]]; then\n            (\n                cd ${SRC_DIR} &&\n                curl -s -o Python-3.9.18.tgz https://www.python.org/ftp/python/3.9.18/Python-3.9.18.tgz &&\n                tar -xf Python-3.9.18.tgz &&\n                rm -f Python-3.9.18.tgz\n            )\n        fi\n        (\n            cd ${SRC_DIR} &&\n            cd Python-3.9.18/ &&\n            ./configure --enable-optimizations CFLAGS=-I${SRC_DIR}/openssl/include LDFLAGS=-L${SRC_DIR}/openssl &&\n            make -j $NPROC &&\n            make -j $NPROC install\n        )\n        if (( $? != 0 )); then\n            printerr 'Failed to compile and install required python version'\n            return 1\n        fi\n        python3 -m pip install -U pip setuptools || {\n            printerr 'Failed to update pip and setuptools'\n            return 1\n        }\n    fi\n\n    # we need a newer version of certbot than the distro repos offer\n    yum remove -y *certbot*\n    python3 -m venv /opt/certbot/\n    /opt/certbot/bin/pip install --upgrade pip\n    /opt/certbot/bin/pip install certbot\n    ln -sf /opt/certbot/bin/certbot /usr/bin/certbot\n\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel kamailio &>/dev/null; groupdel kamailio &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n\n    # TODO: fix upstream kamailio.repo file\n    #yum install -y yum-utils &&\n    #yum-config-manager --add-repo https://rpm.kamailio.org/centos/kamailio.repo &&\n    #yum-config-manager --disable 'kamailio*' >/dev/null &&\n    #yum-config-manager --enable \"kamailio-$KAM_VERSION_DOTTED\" >/dev/null &&\n\n    # Add the Kamailio repos to yum\n    (cat << EOF\n[kamailio]\nname=Kamailio\nbaseurl=https://rpm.kamailio.org/centos/${RHEL_BASE_VER}/${KAM_MINOR_VERSION}/${KAM_VERSION}/\\$basearch/\nenabled=1\nmetadata_expire=30d\ngpgcheck=1\nrepo_gpgcheck=0\ngpgkey=https://rpm.kamailio.org/rpm-pub.key\ntype=rpm\nEOF\n    ) > /etc/yum.repos.d/kamailio.repo\n    yum makecache -y\n\n    yum install -y kamailio kamailio-ldap kamailio-mysql kamailio-sipdump kamailio-websocket kamailio-postgresql kamailio-debuginfo \\\n        kamailio-xmpp kamailio-unixodbc kamailio-utils kamailio-tls kamailio-presence kamailio-outbound kamailio-gzcompress \\\n        kamailio-http_async_client kamailio-dmq_userloc kamailio-jansson kamailio-json kamailio-uuid kamailio-sctp\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing kamailio packages'\n        return 1\n    fi\n\n    # get info about the kamailio install for later use in script\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    # make sure run dir exists\n    mkdir -p /var/run/kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    touch /etc/tmpfiles.d/kamailio.conf\n    echo \"d /run/kamailio 0750 kamailio users\" > /etc/tmpfiles.d/kamailio.conf\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR} ${BACKUPS_DIR}/kamailio\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${BACKUPS_DIR}/kamailio/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    cat <<EOF >${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n#STORE_PLAINTEXT_PW=0\nEOF\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create\n\n    # give kamailio permissions in SELINUX\n    semanage port -a -t sip_port_t -p udp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIPS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIPS_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_WSS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_WSS_PORT}\n    semanage port -a -t sip_port_t -p udp ${KAM_DMQ_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_DMQ_PORT}\n\n    # Start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Configure Kamailio systemd service\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio-v1.service /lib/systemd/system/kamailio.service\n    chmod 644 /lib/systemd/system/kamailio.service\n    systemctl daemon-reload\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-bundle.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libks\n    if [[ ! -d ${SRC_DIR}/libks ]]; then\n        git clone --single-branch -c advice.detachedHead=false https://github.com/signalwire/libks -b v1.8.3 ${SRC_DIR}/libks\n    fi\n    (\n        cd ${SRC_DIR}/libks &&\n        cmake -DCMAKE_BUILD_TYPE=Release . &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    )\n    if (( $? != 0 )); then\n        printerr 'Failed to compile and install libks'\n        return 1\n    fi\n\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    (\n        cd ${SRC_DIR}/libstirshaken &&\n        ./bootstrap.sh &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 &&\n        make -j $NPROC &&\n        make -j $NPROC install &&\n        ldconfig\n    )\n    if (( $? != 0 )); then\n        printerr 'Failed to compile and install libstirshaken'\n        return 1\n    fi\n\n    ## compile and install STIR/SHAKEN module\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/stirshaken &&\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/\n    )\n    if (( $? != 0 )); then\n        printerr 'Failed to compile and install STIR/SHAKEN module'\n        return 1\n    fi\n\n    # patch htable module to support coldelim/colnull on kamailio v5.7.x\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/htable &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/htable-kam57.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/htable/htable.so ${KAM_MODULES_DIR}/\n    )\n    if (( $? != 0 )); then\n        printerr 'Failed to patch htable module'\n        return 1\n    fi\n\n    # patch uac module to support reload_delta\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/uac &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/uac.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/uac/uac.so ${KAM_MODULES_DIR}/\n    )\n    if (( $? != 0 )); then\n        printerr 'Failed to patch uac module'\n        return 1\n    fi\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop servers\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR} ${SYSTEM_KAMAILIO_CONFIG_DIR}.bak.$(date +%Y%m%d_%H%M%S)\n\n    # Uninstall Kamailio modules\n    yum remove -y kamailio\\*\n\n    # remove our selinux changes\n    semanage port -D -t sip_port_t -p udp\n    semanage port -D -t sip_port_t -p tcp\n    semanage port -D -t rabbitmq_port_t -p udp\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "kamailio/centos/8.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local KAM_MINOR_VERSION=$(perl -pe 's%^([0-9])\\.([0-9]).*$%\\1.\\2%' <<<\"$KAM_VERSION\")\n    local RHEL_BASE_VER=$(rpm -E %{rhel})\n    local NPROC=$(nproc)\n\n    # Install Dependencies\n    dnf groupinstall -y 'Development Tools' &&\n    dnf install -y epel-release dnf-plugins-core &&\n    dnf install -y git curl perl firewalld logrotate rsyslog certbot cmake libuuid-devel \\\n        libcurl-devel libjwt-devel libatomic openssl-devel policycoreutils-python-utils\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    dnf install -y kernel-modules-extra-$(uname -r) || {\n        printwarn 'could not install kernel modules for current kernel'\n        echo 'upgrading kernel and installing new modules'\n        printwarn 'you will need to reboot the machine for changes to take effect'\n        dnf install -y kernel-modules-extra\n    }\n\n    if (( $? == 0 )); then\n        echo 'sctp' >/etc/modules-load.d/sctp.conf\n        sed -i -re 's%^blacklist sctp%#blacklist sctp%g' /etc/modprobe.d/*\n        modprobe sctp\n    else\n        printwarn 'Could not install kernel modules for SCTP support. Continuing installation...'\n    fi\n\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel kamailio &>/dev/null; groupdel kamailio &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n\n    # TODO: fix upstream kamailio.repo file\n    #dnf config-manager -y --add-repo https://rpm.kamailio.org/centos/kamailio.repo &&\n    #dnf config-manager --disable 'kamailio*' &&\n    #dnf config-manager --enable \"kamailio-$KAM_VERSION_DOTTED\" &&\n\n    # Add the Kamailio repos to yum\n    (cat << EOF\n[kamailio]\nname=Kamailio\nbaseurl=https://rpm.kamailio.org/centos/${RHEL_BASE_VER}/${KAM_MINOR_VERSION}/${KAM_VERSION}/\\$basearch/\nenabled=1\nmetadata_expire=30d\ngpgcheck=1\nrepo_gpgcheck=0\ngpgkey=https://rpm.kamailio.org/rpm-pub.key\ntype=rpm\nEOF\n    ) > /etc/yum.repos.d/kamailio.repo\n    yum makecache -y\n\n    dnf install -y kamailio kamailio-ldap kamailio-mysql kamailio-sipdump kamailio-websocket kamailio-postgresql kamailio-debuginfo \\\n        kamailio-xmpp kamailio-unixodbc kamailio-utils kamailio-tls kamailio-presence kamailio-outbound kamailio-gzcompress \\\n        kamailio-http_async_client kamailio-dmq_userloc kamailio-jansson kamailio-json kamailio-uuid kamailio-sctp\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing kamailio packages'\n        return 1\n    fi\n\n    # get info about the kamailio install for later use in script\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    # make sure run dir exists\n    mkdir -p /var/run/kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    touch /etc/tmpfiles.d/kamailio.conf\n    echo \"d /run/kamailio 0750 kamailio users\" > /etc/tmpfiles.d/kamailio.conf\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR} ${BACKUPS_DIR}/kamailio\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${BACKUPS_DIR}/kamailio/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    cat <<EOF >${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n#STORE_PLAINTEXT_PW=0\nEOF\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create\n\n    # give kamailio permissions in SELINUX\n    semanage port -a -t sip_port_t -p udp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIPS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIPS_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_WSS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_WSS_PORT}\n    semanage port -a -t sip_port_t -p udp ${KAM_DMQ_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_DMQ_PORT}\n\n    # Start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Configure Kamailio systemd service\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio-v2.service /lib/systemd/system/kamailio.service\n    chmod 644 /lib/systemd/system/kamailio.service\n    systemctl daemon-reload\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-bundle.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libks\n    if [[ ! -d ${SRC_DIR}/libks ]]; then\n        git clone --single-branch -c advice.detachedHead=false https://github.com/signalwire/libks -b v1.8.3 ${SRC_DIR}/libks\n    fi\n    (\n        cd ${SRC_DIR}/libks &&\n        cmake -DCMAKE_BUILD_TYPE=Release . &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libks'\n        return 1\n    }\n\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    (\n        cd ${SRC_DIR}/libstirshaken &&\n        ./bootstrap.sh &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 &&\n        make -j $NPROC &&\n        make -j $NPROC install &&\n        ldconfig\n    ) || {\n        printerr 'Failed to compile and install libstirshaken'\n        return 1\n    }\n\n    ## compile and install STIR/SHAKEN module\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/stirshaken &&\n        make -j $NPROC\n    ) &&\n    cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/ || {\n        printerr 'Failed to compile and install STIR/SHAKEN module'\n        return 1\n    }\n\n    # patch uac module to support reload_delta\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/uac &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/uac.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/uac/uac.so ${KAM_MODULES_DIR}/\n    ) || {\n        printerr 'Failed to patch uac module'\n        return 1\n    }\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop servers\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR} ${SYSTEM_KAMAILIO_CONFIG_DIR}.bak.$(date +%Y%m%d_%H%M%S)\n\n    # Uninstall Kamailio modules\n    dnf remove -y kamailio\\*\n\n    # remove our selinux changes\n    semanage port -D -t sip_port_t -p udp\n    semanage port -D -t sip_port_t -p tcp\n    semanage port -D -t rabbitmq_port_t -p udp\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "kamailio/centos/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local KAM_MINOR_VERSION=$(perl -pe 's%^([0-9])\\.([0-9]).*$%\\1.\\2%' <<<\"$KAM_VERSION\")\n    local RHEL_BASE_VER=$(rpm -E %{rhel})\n    local NPROC=$(nproc)\n\n    # Install Dependencies\n    dnf groupinstall -y 'core' &&\n    dnf groupinstall -y 'base' &&\n    dnf groupinstall -y 'Development Tools' &&\n    dnf install -y epel-release dnf-plugins-core &&\n    dnf install -y git curl perl firewalld logrotate rsyslog certbot cmake libuuid-devel \\\n        libcurl-devel libjwt-devel libatomic openssl-devel policycoreutils-python-utils\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    dnf install -y kernel-modules-extra-$(uname -r) || {\n        printwarn 'could not install kernel modules for current kernel'\n        echo 'upgrading kernel and installing new modules'\n        printwarn 'you will need to reboot the machine for changes to take effect'\n        dnf install -y kernel-modules-extra\n    }\n\n    if (( $? == 0 )); then\n        echo 'sctp' >/etc/modules-load.d/sctp.conf\n        sed -i -re 's%^blacklist sctp%#blacklist sctp%g' /etc/modprobe.d/*\n        modprobe sctp\n    else\n        printwarn 'Could not install kernel modules for SCTP support. Continuing installation...'\n    fi\n\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel kamailio &>/dev/null; groupdel kamailio &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n\n    # TODO: fix upstream kamailio.repo file\n    #dnf config-manager -y --add-repo https://rpm.kamailio.org/centos/kamailio.repo &&\n    #dnf config-manager --disable 'kamailio*' &&\n    #dnf config-manager --enable \"kamailio-$KAM_VERSION_DOTTED\" &&\n\n    # Add the Kamailio repos to yum\n    (cat << EOF\n[kamailio]\nname=Kamailio\nbaseurl=https://rpm.kamailio.org/centos/${RHEL_BASE_VER}/${KAM_MINOR_VERSION}/${KAM_VERSION}/\\$basearch/\nenabled=1\nmetadata_expire=30d\ngpgcheck=1\nrepo_gpgcheck=0\ngpgkey=https://rpm.kamailio.org/rpm-pub.key\ntype=rpm\nEOF\n    ) > /etc/yum.repos.d/kamailio.repo\n\n    dnf clean -y metadata\n    dnf makecache -y\n    dnf install -y kamailio kamailio-ldap kamailio-mysql kamailio-sipdump kamailio-websocket kamailio-postgresql kamailio-debuginfo \\\n        kamailio-xmpp kamailio-unixodbc kamailio-utils kamailio-tls kamailio-presence kamailio-outbound kamailio-gzcompress \\\n        kamailio-http_async_client kamailio-dmq_userloc kamailio-jansson kamailio-json kamailio-uuid kamailio-sctp\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing kamailio packages'\n        return 1\n    fi\n\n    # get info about the kamailio install for later use in script\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    # make sure run dir exists\n    mkdir -p /var/run/kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n\n    touch /etc/tmpfiles.d/kamailio.conf\n    echo \"d /run/kamailio 0750 kamailio users\" > /etc/tmpfiles.d/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR} ${BACKUPS_DIR}/kamailio\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${BACKUPS_DIR}/kamailio/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    cat <<EOF >${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n#STORE_PLAINTEXT_PW=0\nEOF\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create\n\n    # give kamailio permissions in SELINUX\n    semanage port -a -t sip_port_t -p udp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIPS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIPS_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_WSS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_WSS_PORT}\n    semanage port -a -t sip_port_t -p udp ${KAM_DMQ_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_DMQ_PORT}\n\n    # Start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Configure Kamailio systemd service\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio-v2.service /lib/systemd/system/kamailio.service\n    chmod 644 /lib/systemd/system/kamailio.service\n    systemctl daemon-reload\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-bundle.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libks\n    if [[ ! -d ${SRC_DIR}/libks ]]; then\n        git clone --single-branch -c advice.detachedHead=false https://github.com/signalwire/libks -b v1.8.3 ${SRC_DIR}/libks\n    fi\n    (\n        cd ${SRC_DIR}/libks &&\n        cmake -DCMAKE_BUILD_TYPE=Release . &&\n        make -j $NPROC CFLAGS='-Wno-deprecated-declarations' &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libks'\n        return 1\n    }\n\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    (\n        cd ${SRC_DIR}/libstirshaken &&\n        ./bootstrap.sh &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 &&\n        make -j $NPROC CFLAGS='-Wno-deprecated-declarations' &&\n        make -j $NPROC install &&\n        ldconfig\n    ) || {\n        printerr 'Failed to compile and install libstirshaken'\n        return 1\n    }\n\n    ## compile and install STIR/SHAKEN module\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/stirshaken &&\n        make -j $NPROC\n    ) &&\n    cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/ || {\n        printerr 'Failed to compile and install STIR/SHAKEN module'\n        return 1\n    }\n\n    # patch uac module to support reload_delta\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/uac &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/uac.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/uac/uac.so ${KAM_MODULES_DIR}/\n    ) || {\n        printerr 'Failed to patch uac module'\n        return 1\n    }\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop servers\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR} ${SYSTEM_KAMAILIO_CONFIG_DIR}.bak.$(date +%Y%m%d_%H%M%S)\n\n    # Uninstall Kamailio modules\n    dnf remove -y kamailio\\*\n\n    # remove our selinux changes\n    semanage port -D -t sip_port_t -p udp\n    semanage port -D -t sip_port_t -p tcp\n    semanage port -D -t rabbitmq_port_t -p udp\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "kamailio/configs/kamailio.cfg",
    "content": "#!KAMAILIO\n\n#======================================================================\n# Defined Features Enabled\n#======================================================================\n\n#!define WITH_MYSQL\n#!define WITH_AUTH\n#!define WITH_IPAUTH\n#!define WITH_UAC\n#!define WITH_USRLOCDB\n#!define WITH_ACCDB\n#!define WITH_CDRS\n#!define WITH_DROUTE\n##!define WITH_DEBUG\n#!define WITH_NAT\n#!define WITH_DISPATCHER\n#!define WITH_CALL_SETTINGS\n##!define WITH_SIGNAL_SERVERNAT\n##!define WITH_SIGNAL_SERVERNAT6\n##!define WITH_MEDIA_SERVERNAT\n#!define WITH_MULTIDOMAIN\n#!define WITH_TELEBLOCK\n#!define WITH_ANTIFLOOD\n##!define WITH_DBCLUSTER\n#!define WITH_LCR\n#!define WITH_TLS\n#!define WITH_SCTP\n#!define WITH_WEBSOCKETS\n##!define WITH_DMQ\n#!define WITH_MSTEAMS\n##!define WITH_DNID_LNP_ENRICHMENT\n#!define WITH_RTPENGINE\n#!define WITH_TRANSNEXUS\n#!define WITH_STIRSHAKEN\n##!define WITH_IPV6\n##!define WITH_HOMER\n##!define WITH_DMZ\n##!define WITH_PUSH\n\n#======================================================================\n# Define String Replacements Within Config\n#======================================================================\n\n#!subst \"!DMQ_REPLICATE_ENABLED!0!g\"\n\n#======================================================================\n# Defined Constants with String Replacement\n#======================================================================\n\n#!substdef \"!DSIP_ID!!g\"\n#!substdef \"!DSIP_CLUSTER_ID!!g\"\n#!substdef \"!DSIP_VERSION!!g\"\n#!substdef \"!HOMER_ID!!g\"\n#!substdef \"!INTERNAL_IP_ADDR!!g\"\n#!substdef \"!INTERNAL_IP_NET!!g\"\n#!substdef \"!INTERNAL_IP6_ADDR!!g\"\n#!substdef \"!INTERNAL_IP6_NET!!g\"\n#!substdef \"!INTERNAL_FQDN!!g\"\n#!substdef \"!EXTERNAL_IP_ADDR!!g\"\n#!substdef \"!EXTERNAL_IP6_ADDR!!g\"\n#!substdef \"!EXTERNAL_FQDN!!g\"\n#!substdef \"!UAC_REG_ADDR!!g\"\n#!substdef \"!INBOUND_NLB_FQDN!!g\"\n#!substdef \"!OUTBOUND_NLB_FQDN!!g\"\n#!substdef \"!HOMER_HOST!!g\"\n#!substdef \"!SIP_PORT!5060!g\"\n#!substdef \"!SIPS_PORT!5061!g\"\n#!substdef \"!DMQ_PORT!5090!g\"\n#!substdef \"!WSS_PORT!4443!g\"\n#!substdef \"!HEP_PORT!9060!g\"\n#!substdef \"!RTPENGINE_URI!udp:127.0.0.1:7722!g\"\n\n#======================================================================\n# Defined Constants\n#======================================================================\n\n# - database URL - used to connect to database server by modules\n#!ifdef WITH_MYSQL\n#!ifdef WITH_DBCLUSTER\n#!define DBURL \"cluster://dbcluster\"\n#!define SQLCONN_KAM \"kam=>mysql://kamailio:kamailiorw@localhost:3306/kamailio\"\n#!define SQLCONN_AST \"asterisk=>mysql://kamailio:kamailiorw@localhost:3306/kamailio\"\n#!else\n#!define DBURL \"mysql://kamailio:kamailiorw@localhost:3306/kamailio\"\n#!define SQLCONN_KAM \"kam=>mysql://kamailio:kamailiorw@localhost:3306/kamailio\"\n#!define SQLCONN_AST \"asterisk=>mysql://kamailio:kamailiorw@localhost:3306/kamailio\"\n#!endif\n#!endif\n\n### constants used by dispatcher routes\n#!define DSTALG_ROUND_ROBIN 4\n#!define DSTALG_PRIORITY_BASED 8\n#!define DSTALG_WEIGHT_BASED 9\n#!define DSTALG_LOAD_DISTRIBUTION 10\n#!define DSTALG_RELATIVE_WEIGHT 11\n#!define DSTALG_PARALLEL_FORKING 12\n\n#!ifdef WITH_MULTIDOMAIN\n# - the value for 'use_domain' parameters\n#!define MULTIDOMAIN 1\n#!else\n#!define MULTIDOMAIN 0\n#!endif\n\n\n### flag definitions and usage:\n# NOTE: flags are stored in each bit of an integer, there range is from 0-31 (32 bits)\n# NOTE: it seems each set/type of flags has their own variable / namespace\n# FLT_ \t\tper transaction flags\n#   usage:\tsetflag(), resetflag(), isflagset()\n# FLB_ \t\tper branch flags\n#   usage:\tsetbflag(), resetbflag(), isbflagset()\n# FLS_ \t\tper script flags\n#   usage:\tsetsflag(), resetsflag(), issflagset()\n# FLD_ \t\tper dialog flags\n#\tusage:\tdlg_setflag(), dlg_resetflag(), dlg_isflagset()\n#!define FLT_ACC 0\n#!define FLT_ACCMISSED 1\n#!define FLT_ACCFAILED 2\n#!define FLT_FAILOVER 3\n#!define FLT_DLG_ALLOWED 4\n#!define FLT_DOMAINROUTING 6\n#!define FLT_PBX_AUTH 7\n#!define FLT_CARRIER 8\n#!define FLT_PBX 9\n#!define FLT_CARRIER_AUTH 10\n#!define FLT_EXTERNAL_AUTH 11\n#!define FLT_PASSTHRU_AUTH 12\n#!define FLT_SRC_SIP 13\n#!define FLT_SRC_WS 14\n#!define FLT_SRC_ALLOWED 15\n#!define FLT_DST_INTERNAL_IP 16\n#!define FLT_MSTEAMS 17\n#!define FLT_SRC_INTERNAL_IP 18\n#!define FLT_DST_IPV4 19\n#!define FLT_DST_IPV6 20\n#!define FLT_SRC_IPV4 21\n#!define FLT_SRC_IPV6 22\n#!define FLT_HAS_TOTAG 23\n#!define FLB_NATB 0\n#!define FLB_NATSIPPING 1\n#!define FLB_WS_DEVICE 2\n#!define FLB_SRC_PBX 3\n#!define FLB_DST_PBX 4\n#!define FLB_SRC_CARRIER 5\n#!define FLB_DST_CARRIER 6\n#!define FLB_SRC_MSTEAMS 7\n#!define FLB_DST_MSTEAMS 8\n#!define FLB_SRC_MSTEAMS_ONHOLD 9\n#!define FLB_SRC_SELF 10\n#!define FLD_USE_RTPE 1\n\n# NOTE: not actual flags\n#!define FLT_OUTBOUND 8000\n#!define FLT_INBOUND 9000\n\n#!define NOTIFICATION_OVERLIMIT \"0\"\n#!define NOTIFICATION_GWFAILURE \"1\"\n\n#======================================================================\n# Global Parameters\n#======================================================================\n\n### LOG Levels:\n# L_ALERT     -5\n# L_BUG       -4\n# L_CRIT2     -3\n# L_CRIT      -2\n# L_ERR       -1\n# L_WARN       0\n# L_NOTICE     1\n# L_INFO       2\n# L_DBG        3\n#!ifdef WITH_DEBUG\ndebug = 3\nlog_stderror = false\n#!else\ndebug = 2\nlog_stderror = false\n#!endif\n\n# specifies on which log level the memory debugger/statistics will be logged\nmemdbg = 5\nmemlog = 5\n\n# syslog log facility messages will be logged to\nlog_facility = LOG_LOCAL0\n\n# configure the prefix for all log messages\nlog_prefix_mode = 1\nlog_prefix = \"[$cfg(name):$cfg(line):$cfg(route)] [$ci:$rm:$rs] \"\n\n# multiprocess settings on startup\nfork = true\nchildren = 1\n\n# increase loop limit to allow 10000 DID checks for inbound calls\nmax_while_loops = 10000\n\n# uncomment the next line to disable TCP (default on)\n#disable_tcp = true\n\n# uncomment the next line to disable the auto discovery of local aliases\n# based on reverse DNS on IPs (default on)\n#auto_aliases = false\n\n# add local domain aliases\n# NOTE:\tport is required in alias for loose routing to function properly\n# REF:\thttps://www.kamailio.org/wiki/cookbooks/5.5.x/core#alias\nalias = \"EXTERNAL_FQDN:SIP_PORT\"\nalias = \"EXTERNAL_FQDN:SIPS_PORT\"\nalias = \"EXTERNAL_FQDN:DMQ_PORT\"\nalias = \"EXTERNAL_FQDN:WSS_PORT\"\nalias = \"INBOUND_NLB_FQDN:SIP_PORT\"\nalias = \"INBOUND_NLB_FQDN:SIPS_PORT\"\nalias = \"INBOUND_NLB_FQDN:DMQ_PORT\"\nalias = \"INBOUND_NLB_FQDN:WSS_PORT\"\nalias = \"OUTBOUND_NLB_FQDN:SIP_PORT\"\nalias = \"OUTBOUND_NLB_FQDN:SIPS_PORT\"\nalias = \"OUTBOUND_NLB_FQDN:DMQ_PORT\"\nalias = \"OUTBOUND_NLB_FQDN:WSS_PORT\"\n\n# configure interface/port/proto kamailio will bind on\nlisten = udp:127.0.0.1:SIP_PORT\nlisten = tcp:127.0.0.1:SIP_PORT\n#!ifdef WITH_TLS\nlisten = tls:127.0.0.1:SIPS_PORT\n#!ifdef WITH_WEBSOCKETS\nlisten = tls:127.0.0.1:WSS_PORT\n#!endif\n#!endif\n#!ifdef WITH_SCTP\nlisten = sctp:127.0.0.1:SIP_PORT\n#!endif\n\n#!ifdef WITH_DMZ\nlisten = udp:EXTERNAL_IP_ADDR:SIP_PORT\nlisten = tcp:EXTERNAL_IP_ADDR:SIP_PORT\n#!endif\n\n#!ifdef WITH_SIGNAL_SERVERNAT\nlisten = udp:INTERNAL_IP_ADDR:SIP_PORT advertise EXTERNAL_IP_ADDR:SIP_PORT\nlisten = tcp:INTERNAL_IP_ADDR:SIP_PORT advertise EXTERNAL_IP_ADDR:SIP_PORT\n#!ifdef WITH_TLS\n#!ifdef WITH_WEBSOCKETS\nlisten = tls:INTERNAL_IP_ADDR:WSS_PORT advertise \"EXTERNAL_FQDN\":WSS_PORT\n#!endif\n#!endif\n#!ifdef WITH_SCTP\nlisten = sctp:INTERNAL_IP_ADDR:SIP_PORT advertise EXTERNAL_IP_ADDR:SIP_PORT\n#!endif\n#!else\nlisten = udp:INTERNAL_IP_ADDR:SIP_PORT\nlisten = tcp:INTERNAL_IP_ADDR:SIP_PORT\n#!ifdef WITH_TLS\n#!ifdef WITH_WEBSOCKETS\nlisten = tls:INTERNAL_IP_ADDR:WSS_PORT\n#!endif\n#!endif\n#!ifdef WITH_SCTP\nlisten = sctp:INTERNAL_IP_ADDR:SIP_PORT\n#!endif\n#!endif\n\n#!ifdef WITH_IPV6\nlisten = udp:[::1]:SIP_PORT\nlisten = tcp:[::1]:SIP_PORT\n#!ifdef WITH_TLS\nlisten = tls:[::1]:SIPS_PORT\n#!ifdef WITH_WEBSOCKETS\nlisten = tls:[::1]:WSS_PORT\n#!endif\n#!endif\n#!ifdef WITH_SCTP\nlisten = sctp:[::1]:SIP_PORT\n#!endif\n#!ifdef WITH_DMZ\nlisten = udp:[EXTERNAL_IP6_ADDR]:SIP_PORT\nlisten = tcp:[EXTERNAL_IP6_ADDR]:SIP_PORT\n#!endif\n#!ifdef WITH_SIGNAL_SERVERNAT6\nlisten = udp:[INTERNAL_IP6_ADDR]:SIP_PORT advertise EXTERNAL_IP6_ADDR:SIP_PORT\nlisten = tcp:[INTERNAL_IP6_ADDR]:SIP_PORT advertise EXTERNAL_IP6_ADDR:SIP_PORT\n#!ifdef WITH_TLS\n#!ifdef WITH_WEBSOCKETS\nlisten = tls:[INTERNAL_IP6_ADDR]:WSS_PORT advertise \"EXTERNAL_FQDN\":WSS_PORT\n#!endif\n#!endif\n#!ifdef WITH_SCTP\nlisten = sctp:[INTERNAL_IP6_ADDR]:SIP_PORT advertise EXTERNAL_IP6_ADDR:SIP_PORT\n#!endif\n#!else\nlisten = udp:[INTERNAL_IP6_ADDR]:SIP_PORT\nlisten = tcp:[INTERNAL_IP6_ADDR]:SIP_PORT\n#!endif\n#!ifdef WITH_TLS\n#!ifdef WITH_WEBSOCKETS\nlisten = tls:[INTERNAL_IP6_ADDR]:WSS_PORT\n#!endif\n#!endif\n#!ifdef WITH_SCTP\nlisten = sctp:[INTERNAL_IP6_ADDR]:SIP_PORT\n#!endif\n#!endif\n\n#!ifdef WITH_DMQ\nlisten = udp:INTERNAL_IP_ADDR:DMQ_PORT\n#!endif\n\n#!ifdef WITH_TLS\nenable_tls = true\ntcp_accept_no_cl = true\ntcp_rd_buf_size = 16384\nlisten = tls:INTERNAL_IP_ADDR:SIPS_PORT advertise \"EXTERNAL_FQDN\":SIPS_PORT\n#!ifdef WITH_IPV6\nlisten = tls:[INTERNAL_IP6_ADDR]:SIPS_PORT advertise \"EXTERNAL_FQDN\":SIPS_PORT\n#!endif\n#!endif\n\n# life time of TCP connection when there is no traffic\n# - a bit higher than registration expires to cope with UA behind NAT\ntcp_connection_lifetime = 3605\n\n# Whether the “Server” header for locally generated messages is set\nserver_signature = true\n# What the generated \"Server\" header will be set to\nserver_header = \"Server: dSIPRouter/DSIP_VERSION\"\n\n# disable Stream Control Tranmission Protocol (SCTP)\n#!ifdef WITH_SCTP\nenable_sctp = 1\n#!else\nenable_sctp = 0\n#!endif\n\n# enable/disable DMZ support\n#!ifdef WITH_DMZ\nmhomed=1\n#!else\nmhomed=0\n#!endif\n\n####### Custom Parameters #########\n\n# These parameters can be modified runtime via RPC interface\n# - see the documentation of 'cfg_rpc' module.\n#\n# Format: group.id = value 'desc' description\n# Access: $sel(cfg_get.group.id) or @cfg_get.group.id\n#\n\n#!ifdef WITH_VOICEMAIL\n# VoiceMail Routing on offline, busy or no answer\n#\n# - by default Voicemail server IP is empty to avoid misrouting\nvoicemail.srv_ip = \"\" desc \"VoiceMail IP Address\"\nvoicemail.srv_port = \"5060\" desc \"VoiceMail Port\"\n#!endif\n\n#!ifdef WITH_TELEBLOCK\nteleblock.gw_enabled = 0 desc \"Enable Teleblock support\"\nteleblock.gw_ip = \"62.34.24.22\" desc \"Teleblock IP\"\nteleblock.gw_port = \"5066\" desc \"Teleblock Port\"\nteleblock.media_ip = \"\" desc \"Teleblock media ip\"\nteleblock.media_port = \"\" desc \"Teleblock media port\"\n#!endif\n\n# Define the role of the server\n#   \"\"     \t\t- default behavior\n#   outbound    - outbound only (no domain routing)\n#   inout       - inbound and outbound only (no domain routing)\nserver.role = \"\" desc \"Role of the server in the topology\"\n\n# Local calling maximum digits for the initiating PBX - PBX sending the INVITE\nserver.pbx_max_local_digits = 5 desc \"Maximum digits for local pbx extensions\"\n\n# PBX INVITE Timeout (msecs) to support having a Primary and Secondary PBX\nserver.pbx_invite_timeout = 5000 desc \"The default PBX INVITE timeout\"\n\n# PBX INVITE Timeout (msecs) if a SIP 100/180/181/183 message is received\nserver.pbx_invite_timeout_aftertry = 16000 desc \"PBX INVITE timeout value if a SIP 100 message is received\"\n\n# DSIPRouter API Server Settings\nserver.api_server = \"https://127.0.0.1:5000\" desc \"URL to the DSIPRouter API Server\"\nserver.api_token = \"admin\" desc \"API Token for DSIPRouter API Server\"\n\n# Emergency Numbers\nserver.emergency_numbers = \"^([2-9]11|112|999|000|988|933)$\" desc \"Emergency Numbers\"\n\n# Transnexus External STIR/SHAKEN Support\ntransnexus.authservice_lrn_enabled = 0 desc \"Enable LRN for authservice\"\ntransnexus.authservice_enabled = 0 desc \"Enable auth service for all Trunks\"\ntransnexus.authservice_host = \"outbound.sip.clearip.com:5060\" desc \"Host to connect to for auth service\"\ntransnexus.verifyservice_enabled = 0 desc \"Enable verify service for all Trunks\"\ntransnexus.verifyservice_host = \"inbound.sip.clearip.com:5060\" desc \"Host to connect to for verify service\"\n\n# Native Kamailio STIR/SHAKEN Support\nstir_shaken.stir_shaken_enabled = 0 desc \"Whether native STIR/SHAKEN support is enabled\"\nstir_shaken.stir_shaken_prefix_a = \"\" desc \"Default Empty\"\nstir_shaken.stir_shaken_prefix_b = \"\" desc \"Default Empty\"\nstir_shaken.stir_shaken_prefix_c = \"\" desc \"Default Empty\"\nstir_shaken.stir_shaken_prefix_invalid = \"\" desc \"Default Empty\"\nstir_shaken.stir_shaken_block_invalid = 0 desc \"Default Disabled\"\nstir_shaken.stir_shaken_key_path = \"/etc/dsiprouter/certs/stirshaken/stirshaken-key.pem\" desc \"Local path to RSA key\"\nstir_shaken.stir_shaken_cert_url = \" https://cr.example.com/xK/order/xk\" desc \"URL to download X509 certificate from\"\n\n# MSTeams Settings\nserver.msteams_disable_refer = 1\n\n# set location to load modules from (source or installation folders)\n#!ifdef WITH_SRCPATH\nmpath = \"/usr/lib/x86_64-linux-gnu/kamailio/modules/\"\n#!else\nmpath = \"/usr/lib/x86_64-linux-gnu/kamailio/modules/\"\n#!endif\n\n#======================================================================\n# Module Loading\n#======================================================================\n\n#!ifdef WITH_TLS\nloadmodule \"tls.so\"\n#!endif\n\n#!ifdef WITH_SCTP\nloadmodule \"sctp.so\"\n#!endif\n\n#!ifdef WITH_MYSQL\nloadmodule \"db_mysql.so\"\n#!endif\n\nloadmodule \"kex.so\"\nloadmodule \"corex.so\"\nloadmodule \"tm.so\"\nloadmodule \"tmx.so\"\nloadmodule \"sl.so\"\nloadmodule \"rr.so\"\nloadmodule \"path.so\"\nloadmodule \"pv.so\"\nloadmodule \"maxfwd.so\"\nloadmodule \"usrloc.so\"\nloadmodule \"registrar.so\"\nloadmodule \"textops.so\"\nloadmodule \"textopsx.so\"\nloadmodule \"siputils.so\"\nloadmodule \"xlog.so\"\nloadmodule \"sanity.so\"\nloadmodule \"ctl.so\"\nloadmodule \"cfg_rpc.so\"\nloadmodule \"acc.so\"\nloadmodule \"xhttp.so\"\nloadmodule \"json.so\"\nloadmodule \"jansson.so\"\nloadmodule \"jsonrpcs.so\"\nloadmodule \"http_async_client.so\"\nloadmodule \"uuid.so\"\nloadmodule \"ipops.so\"\nloadmodule \"sdpops.so\"\nloadmodule \"rtimer.so\"\nloadmodule \"sqlops.so\"\n\n#!ifdef WITH_DEBUG\nloadmodule \"sipdump.so\"\n#!endif\n\n#!ifdef WITH_DMQ\nloadmodule \"dmq.so\"\nloadmodule \"dmq_usrloc.so\"\n#!endif\n# must be loaded after dmq\nloadmodule \"htable.so\"\nloadmodule \"dialog.so\"\n\n#!ifdef WITH_AUTH\nloadmodule \"auth.so\"\nloadmodule \"auth_db.so\"\n#!ifdef WITH_IPAUTH\nloadmodule \"permissions.so\"\n#!endif\n#!ifdef WITH_UAC\nloadmodule \"uac.so\"\nloadmodule \"uac_redirect.so\"\n#!endif\n#!endif\n\n#!ifdef WITH_ALIASDB\nloadmodule \"alias_db.so\"\n#!endif\n\n#!ifdef WITH_SPEEDDIAL\nloadmodule \"speeddial.so\"\n#!endif\n\n#!ifdef WITH_MULTIDOMAIN\nloadmodule \"domain.so\"\n#!endif\n\n#!ifdef WITH_PRESENCE\nloadmodule \"presence.so\"\nloadmodule \"presence_xml.so\"\n#!endif\n\n#!ifdef WITH_NAT\nloadmodule \"nathelper.so\"\n#!endif\n\n#!ifdef WITH_RTPENGINE\nloadmodule \"rtpengine.so\"\n#!endif\n\n#!ifdef WITH_ANTIFLOOD\nloadmodule \"pike.so\"\n#!endif\n\n#!ifdef WITH_XMLRPC\nloadmodule \"xmlrpc.so\"\n#!endif\n\n#!ifdef WITH_DEBUG\nloadmodule \"debugger.so\"\n#!endif\n\n#!ifdef WITH_DROUTE\nloadmodule \"drouting.so\"\n#!endif\n\n#!ifdef WITH_DBCLUSTER\nloadmodule \"db_cluster\"\n#!endif\n\n#!ifdef WITH_DISPATCHER\nloadmodule \"keepalive.so\"\nloadmodule \"dispatcher.so\"\n#!endif\n\n#!ifdef WITH_WEBSOCKETS\nloadmodule \"websocket.so\"\n#!endif\n\n#!ifdef WITH_STIRSHAKEN\nloadmodule \"stirshaken.so\"\n#!endif\n\n#!ifdef WITH_HOMER\nloadmodule \"siptrace.so\"\n#!endif\n\n#!ifdef WITH_PUSH\nloadmodule \"tsilo.so\"\n#!endif\n\n#======================================================================\n# Module-Specific Parameters\n#======================================================================\n\n# ---- xlog global params ----\nmodparam(\"xlog\", \"buf_size\", 8192)\nmodparam(\"xlog\", \"prefix\", \"\")\n\n# ---- htable global params ----\nmodparam(\"htable\", \"db_url\", DBURL)\n\n# ---- dispatcher params ----\n#!ifdef WITH_DISPATCHER\nmodparam(\"dispatcher\", \"flags\", 2)\nmodparam(\"dispatcher\", \"db_url\", DBURL)\nmodparam(\"dispatcher\", \"table_name\", \"dispatcher\")\n# The flags column controls the mode of a destination and keepalives. It is a bitwise value that can be built using the following flags:\n# 1\t (1 <<0):\tinactive destination\n# 2  (1 <<1):\ttemporary trying destination (in the way to become inactive if it does not reply to keepalives\n# 4  (1 <<2):\tadmin disabled destination\n# 8  (1 <<3):\tprobing destination (sending keep alives)\n# 16 (1 <<4):\tskip DNS A/AAAA resolve at startup, useful when the hostname of the destination address is a NAPTR or SRV record only\nmodparam(\"dispatcher\", \"flags_col\", \"flags\")\n# Controls what gateways are tested to see if they are reachable:\n# 0:\tOnly the gateways with state PROBING (flags & 0x08 == 1) are tested. After a gateway is probed, the PROBING state is cleared in this mode (it will probe only one time at startup or after dispatcher reload).\n# 1: \tAll gateways are tested. If there is a failure of keepalive to an active gateway, then it is set to TRYING state.\n# 2: \tOnly gateways in INACTIVE state with PROBING mode set are tested.\n# 3: \tAny gateway with state PROBING is continually probed without modifying/removing the PROBING state. This allows selected gateways to be probed continually, regardless of state changes.\nmodparam(\"dispatcher\", \"ds_probing_mode\", 3)\nmodparam(\"dispatcher\", \"ds_ping_latency_stats\", 1)\t\t# 1 means to provide latency stats\nmodparam(\"dispatcher\", \"ds_ping_method\", \"OPTIONS\")\t\t# The SIP method to use when pinging destinations\nmodparam(\"dispatcher\", \"ds_ping_interval\", 60)\t\t\t# How often (seconds) to ping destinations to check status\nmodparam(\"dispatcher\", \"ds_probing_threshold\", 1)\t\t# How many failed ping requests before marking the destination as inactive\nmodparam(\"dispatcher\", \"ds_inactive_threshold\", 1)\t\t# How many successful ping requests before marking the destination as active\nmodparam(\"dispatcher\", \"xavp_dst\", \"dispatcher_dst\")\t# Will contain selected destination info\nmodparam(\"dispatcher\", \"xavp_dst_mode\", 0)\t\t\t\t# What attributes to set in the xavp\nmodparam(\"dispatcher\", \"xavp_ctx\", \"dispatcher_ctx\")\t# Will contain current dispatcher context info\nmodparam(\"dispatcher\", \"xavp_ctx_mode\", 0)\t\t\t\t# What attributes to set in the xavp\nmodparam(\"dispatcher\", \"reload_delta\", 1)\t\t\t\t# how quickly (in seconds) a RPC reload is allowed\n#!endif\n\n# ----- db_cluster params ----\n# connection:   set for each db connection uri\n# cluster:      s == serial, r == roundrobin, p == parallel (write/only)\n#!ifdef WITH_DBCLUSTER\nmodparam(\"db_cluster\", \"connection\", \"c1=>mysql://kamailio:kamailiorw@192.168.1.2/kamailio\")\nmodparam(\"db_cluster\", \"connection\", \"c2=>mysql://kamailio:kamailiorw@192.168.1.3/kamailio\")\nmodparam(\"db_cluster\", \"connection\", \"c3=>mysql://kamailio:kamailiorw@192.168.1.4/kamailio\")\nmodparam(\"db_cluster\", \"connection\", \"c4=>mysql://kamailio:kamailiorw@192.168.1.5/kamailio\")\nmodparam(\"db_cluster\", \"cluster\", \"dbcluster=>c1=9r9r;c2=9r9r;c3=9r9r;c4=9r9r\")\nmodparam(\"db_cluster\", \"inactive_interval\", 180)\n#!endif\n\n# ----- jsonrpcs params -----\nmodparam(\"jsonrpcs\", \"pretty_format\", 1)\nmodparam(\"jsonrpcs\", \"fifo_name\", \"/var/run/kamailio/kamailio_rpc.fifo\")\nmodparam(\"jsonrpcs\", \"transport\", 3)\n#modparam#(\"jsonrpcs\", \"dgram_socket\", \"/var/run/kamailio/kamailio_rpc.sock\")\n\n# ----- ctl params -----\nmodparam(\"ctl\", \"binrpc\", \"unix:/var/run/kamailio/kamailio_ctl\")\n\n# ----- tm params -----\n# auto-discard branches from previous serial forking leg\nmodparam(\"tm\", \"failure_reply_mode\", 3)\n# default retransmission timeout: 30sec\nmodparam(\"tm\", \"fr_timer\", 30000)\n# default invite retransmission timeout after 1xx: 60sec\nmodparam(\"tm\", \"fr_inv_timer\", 60000)\n# XAVP used to store contacts used by t_load_contacts()/t_next_contacts()\nmodparam(\"tm\", \"contacts_avp\", \"tm_contacts\")\n# Used to store contacts (if any) that it skipped, because they contained same +sip.instance\nmodparam(\"tm\", \"contact_flows_avp\", \"tm_contact_flows\")\n# consider branch failures for to_on_failure() routing as well\nmodparam(\"tm\", \"failure_exec_mode\", 1)\n\n# ----- rr params -----\n# set next param to 1 to add value to ;lr param (helps with some UAs)\nmodparam(\"rr\", \"enable_full_lr\", 0)\n# append from tag to the RR (required for is_direction() to work)\nmodparam(\"rr\", \"append_fromtag\", 1)\n\n# ----- registrar params -----\nmodparam(\"registrar\", \"method_filtering\", 1)\n/* uncomment the next line to disable parallel forking via location */\n# modparam(\"registrar\", \"append_branches\", 0)\n/* uncomment the next line not to allow more than 10 contacts per AOR */\n#modparam(\"registrar\", \"max_contacts\", 10)\n# max value for expires of registrations\nmodparam(\"registrar\", \"max_expires\", 0)\n# set it to 1 to enable GRUU\nmodparam(\"registrar\", \"gruu_enabled\", 0)\n\n# ----- acc params -----\n# what special events should be accounted?\nmodparam(\"acc\", \"early_media\", 0)\nmodparam(\"acc\", \"report_ack\", 0)\nmodparam(\"acc\", \"report_cancels\", 0)\n# by default ww do not adjust the direction of the sequential requests\n# if you enable this parameter, be sure the enable \"append_fromtag\" in \"rr\" module\nmodparam(\"acc\", \"detect_direction\", 0)\nmodparam(\"acc\", \"log_flag\", FLT_ACC)\nmodparam(\"acc\", \"log_facility\", \"LOG_LOCAL0\")\nmodparam(\"acc\", \"log_missed_flag\", FLT_ACCMISSED)\nmodparam(\"acc\", \"log_extra\",\n\t\"src_user=$fU;src_domain=$fd;src_ip=$si;dst_ouser=$tU;dst_user=$rU;dst_domain=$rd;\"\n\t\"calltype=$avp(calltype);src_gwgroupid=$dlg_var(src_gwgroupid);dst_gwgroupid=$dlg_var(dst_gwgroupid)\")\nmodparam(\"acc\", \"failed_transaction_flag\", FLT_ACCFAILED)\n# enhanced DB accounting\n#!ifdef WITH_ACCDB\nmodparam(\"acc\", \"db_flag\", FLT_ACC)\nmodparam(\"acc\", \"db_missed_flag\", FLT_ACCMISSED)\nmodparam(\"acc\", \"db_url\", DBURL)\nmodparam(\"acc\", \"db_extra\",\n\t\"src_user=$fU;src_domain=$fd;src_ip=$si;dst_ouser=$tU;dst_user=$rU;dst_domain=$rd;\"\n\t\"calltype=$avp(calltype);src_gwgroupid=$dlg_var(src_gwgroupid);dst_gwgroupid=$dlg_var(dst_gwgroupid)\")\n#!endif\n\n# ----- usrloc params -----\n/* enable DB persistency for location entries */\n#!ifdef WITH_USRLOCDB\nmodparam(\"usrloc\", \"db_url\", DBURL)\nmodparam(\"usrloc\", \"db_mode\", 3)\nmodparam(\"usrloc\", \"use_domain\", MULTIDOMAIN)\nmodparam(\"usrloc\", \"handle_lost_tcp\", 1)\n#!endif\n\n# ----- auth_db params -----\n#!ifdef WITH_AUTH\nmodparam(\"auth_db\", \"db_url\", DBURL)\nmodparam(\"auth_db\", \"calculate_ha1\", 1)\nmodparam(\"auth_db\", \"password_column\", \"password\")\n# We use the rpid field of the subscriber table to track the assigned gwgroup (type of endpoint or carrier)\nmodparam(\"auth_db\", \"load_credentials\", \"$avp(s:src_gwgroupid)=rpid;\")\nmodparam(\"auth_db\", \"use_domain\", MULTIDOMAIN)\n\n# ----- permissions params -----\n#!ifdef WITH_IPAUTH\nmodparam(\"permissions\", \"db_url\", DBURL)\nmodparam(\"permissions\", \"db_mode\", 1)\n# how quickly (in seconds) a RPC reload is allowed\nmodparam(\"permissions\", \"reload_delta\", 1)\n#!endif\n#!endif\n\n# ----- alias_db params -----\n#!ifdef WITH_ALIASDB\nmodparam(\"alias_db\", \"db_url\", DBURL)\nmodparam(\"alias_db\", \"use_domain\", MULTIDOMAIN)\n#!endif\n\n# ----- speeddial params -----\n#!ifdef WITH_SPEEDDIAL\nmodparam(\"speeddial\", \"db_url\", DBURL)\nmodparam(\"speeddial\", \"use_domain\", MULTIDOMAIN)\n#!endif\n\n# ----- domain params -----\n#!ifdef WITH_MULTIDOMAIN\nmodparam(\"domain\", \"db_url\", DBURL)\n# register callback to match myself condition with domains list\nmodparam(\"domain\", \"register_myself\", 1)\n#!endif\n\n#!ifdef WITH_PRESENCE\n# ----- presence params -----\nmodparam(\"presence\", \"db_url\", DBURL)\n\n# ----- presence_xml params -----\nmodparam(\"presence_xml\", \"db_url\", DBURL)\nmodparam(\"presence_xml\", \"force_active\", 1)\n#!endif\n\n#!ifdef WITH_NAT\n# ----- nathelper params -----\nmodparam(\"nathelper\", \"natping_interval\", 60)\nmodparam(\"nathelper\", \"ping_nated_only\", 1)\nmodparam(\"nathelper\", \"sipping_bflag\", FLB_NATSIPPING)\nmodparam(\"nathelper\", \"sipping_from\", \"sip:pinger@UAC_REG_ADDR\")\n\n# params needed for NAT traversal in other modules\nmodparam(\"nathelper|registrar\", \"received_avp\", \"$avp(RECEIVED)\")\nmodparam(\"usrloc\", \"nat_bflag\", FLB_NATB)\n#!endif\n\n#!ifdef WITH_RTPENGINE\n# ----- rtpengine params -----\nmodparam(\"rtpengine\", \"rtpengine_sock\", \"RTPENGINE_URI\")\n# Specify if the RTPEngine instances have to be pinged at startup to detect if they are active. Set it to 0 to disable pinging and to 1 to activate pinging\nmodparam(\"rtpengine\", \"ping_mode\", 0)\n# Once an RTP proxy was found unreachable and marked as disabled, the rtpengine module will not attempt to establish communication to that RTP proxy for rtpengine_disable_tout seconds\nmodparam(\"rtpengine\", \"rtpengine_disable_tout\", 0)\n# How many times the module should retry to send and receive after timeout was generated\nmodparam(\"rtpengine\", \"rtpengine_retr\", 5)\n# Number of seconds after an rtpengine hash table entry is marked for deletion. The entries correspond to active calls using an rtpengine instance\n# This should match the default rtpengine timeout set in rtpengine.conf\nmodparam(\"rtpengine\", \"hash_table_tout\", 60)\n#!endif\n\n#!ifdef WITH_TLS\n# ----- tls params -----\nmodparam(\"tls\", \"config\", \"//etc/kamailio/tls.cfg\")\n#!endif\n\n#!ifdef WITH_SCTP\nmodparam(\"sctp\", \"sctp_assoc_tracking\", 0)\nmodparam(\"sctp\", \"sctp_assoc_reuse\", 0)\n#!endif\n\n\n#!ifdef WITH_ANTIFLOOD\n# ----- pike params -----\nmodparam(\"pike\", \"sampling_time_unit\", 2)\nmodparam(\"pike\", \"reqs_density_per_unit\", 50)\nmodparam(\"pike\", \"remove_latency\", 30)\n\n# ----- htable params -----\n# ip ban htable with autoexpire after 5 minutes\nmodparam(\"htable\", \"htable\", \"ipban=>size=8;autoexpire=300;dmqreplicate=DMQ_REPLICATE_ENABLED;\")\n#!endif\n\n#!ifdef WITH_XMLRPC\n# ----- xmlrpc params -----\nmodparam(\"xmlrpc\", \"route\", \"XMLRPC\");\nmodparam(\"xmlrpc\", \"url_match\", \"^/RPC\")\n#!endif\n\n#!ifdef WITH_DEBUG\n# ----- debugger params -----\nmodparam(\"debugger\", \"cfgtrace\", 0)\nmodparam(\"debugger\", \"log_level_name\", \"debugger\")\n# ----- sipdump params -----\nmodparam(\"sipdump\", \"enable\", 1)\nmodparam(\"sipdump\", \"wait\", 100)\nmodparam(\"sipdump\", \"rotate\", 3600)\nmodparam(\"sipdump\", \"folder\", \"/tmp\")\nmodparam(\"sipdump\", \"fprefix\", \"dsipdump-\")\n#!endif\n\n#!ifdef WITH_UAC\n# ----- uac params -----\nmodparam(\"uac\", \"restore_mode\", \"none\")\nmodparam(\"uac\", \"reg_db_url\", DBURL)\nmodparam(\"uac\", \"reg_db_table\", \"uacreg\")\nmodparam(\"uac\", \"reg_timer_interval\", 60)\nmodparam(\"uac\", \"reg_retry_interval\", 120)\nmodparam(\"uac\", \"reg_keep_callid\", 1)\nmodparam(\"uac\", \"reg_gc_interval\", 30)\nmodparam(\"uac\", \"credential\", \"username:domain:password\")\nmodparam(\"uac\", \"auth_realm_avp\", \"$avp(auth_realm)\")\nmodparam(\"uac\", \"auth_username_avp\", \"$avp(auth_user)\")\nmodparam(\"uac\", \"auth_password_avp\", \"$avp(auth_pass)\")\nmodparam(\"uac\", \"reg_contact_addr\", \"UAC_REG_ADDR:SIP_PORT\")\n# how quickly (in seconds) a RPC reload is allowed\nmodparam(\"uac\", \"reload_delta\", 1)\n#!endif\n\n#!ifdef WITH_DROUTE\n# ----- drouting params -----\nmodparam(\"drouting\", \"db_url\", DBURL)\nmodparam(\"drouting\", \"ruri_avp\", \"$avp(dr_ruri)\")\n# our convention for passing some data about the selected route:\n# attrs[0]: gwid,\n# attrs[1]: gwtype\n# attrs[2]: msteams domain\n# attrs[3]: signalling transport\n# attrs[4]: media transport\nmodparam(\"drouting\", \"attrs_avp\", \"$avp(dr_attrs)\")\n# don't match on domain (per group) only use routing group\nmodparam(\"drouting\", \"use_domain\", 0)\n# do not resolve DNS names during load (will blindly try them)\nmodparam(\"drouting\", \"force_dns\", 0)\n# how gateways are selected from each rule\n# 0: destination groups are ignored and all the destinations are tried in the given order\n# 1: the destinations from each group are randomly arranged (only the two first elements are randomly selected); groups do maintain their order (as given)\n# 2: from each destination group, only a single destination is randomly selected; groups do maintain their order (as given)\nmodparam(\"drouting\", \"sort_order\", 0)\n# htable for maintenance mode\nmodparam(\"htable\", \"htable\", \"maintmode=>size=8;autoexpire=0;dmqreplicate=DMQ_REPLICATE_ENABLED;dbtable=dsip_maintmode;cols='ipaddr,gwid';\")\n#modparam(\"drouting\", \"enable_keepalive\", 1)\n#!endif\n\n#!ifdef WITH_LCR\n# ----- htable params for from/to prefix lookup -----\nmodparam(\"htable\", \"htable\", \"tofromprefix=>size=8;autoexpire=0;dmqreplicate=DMQ_REPLICATE_ENABLED;dbtable=dsip_lcr;cols='pattern,dr_groupid';\")\n#!endif\n\n# ----- rtimer params -----\n#!ifdef WITH_CDRS\nmodparam(\"rtimer\", \"timer\", \"name=cdr;interval=300;mode=1;\")\nmodparam(\"rtimer\", \"exec\", \"timer=cdr;route=CDRS\")\n#!endif\n\n# ----- sqlops params -----\n# Kamailio Connection\nmodparam(\"sqlops\", \"sqlcon\", SQLCONN_KAM)\n# Asterisk Realtime Connection\nmodparam(\"sqlops\", \"sqlcon\", SQLCONN_AST)\n\n# ----- dialog params -----\nmodparam(\"dialog\", \"db_url\", DBURL)\nmodparam(\"dialog\", \"db_mode\", 0)\nmodparam(\"dialog\", \"enable_stats\", 1)\nmodparam(\"dialog\", \"hash_size\", 4096)\nmodparam(\"dialog\", \"detect_spirals\", 1)\nmodparam(\"dialog\", \"track_cseq_updates\", 1)\nmodparam(\"dialog\", \"default_timeout\", 21600)\nmodparam(\"dialog\", \"early_timeout\", 300)\nmodparam(\"dialog\", \"noack_timeout\", 180)\nmodparam(\"dialog\", \"end_timeout\", 60)\nmodparam(\"dialog\", \"dlg_match_mode\", 1)\nmodparam(\"dialog\", \"send_bye\", 0)\nmodparam(\"dialog\", \"timeout_noreset\", 1)\nmodparam(\"dialog\", \"timeout_avp\", \"$dlg_ctx(timeout)\")\n\n#!ifdef WITH_DMQ\n# ---- dmq params ----\n# TODO: dmq module only supports one server_address so we do not listen on IPV6, possibly change to INTERNAL_FQDN?\n#\t\tthis would only be an issue for IPv4 disabled machines, which is very uncommon\nmodparam(\"dmq\", \"server_address\", \"sip:INTERNAL_IP_ADDR:DMQ_PORT\")\nmodparam(\"dmq\", \"notification_address\", \"sip:local.cluster:DMQ_PORT\")\nmodparam(\"dmq\", \"multi_notify\", 1)\nmodparam(\"dmq\", \"num_workers\", 4)\nmodparam(\"dmq\", \"ping_interval\", 15)\nmodparam(\"dmq_usrloc\", \"enable\", 1)\n# ---- dmq-related params ----\nmodparam(\"dialog\", \"enable_dmq\", 1)\nmodparam(\"htable\", \"enable_dmq\", 1)\n# only valid for kam ver >= 5.2\nmodparam(\"htable\", \"dmq_init_sync\", 1)\n#!endif\n\n#!ifdef WITH_CALL_SETTINGS\nmodparam(\"dialog\", \"profiles_with_value\", \"gwgroup\")\nmodparam(\"htable\", \"htable\", \"call_settings=>size=8;autoexpire=0;dmqreplicate=DMQ_REPLICATE_ENABLED;dbtable=dsip_call_settings_h;cols='gwgroupid,limit,timeout';colnull='';\")\nmodparam(\"htable\", \"htable\", \"concurrent_calls=>size=8;autoexpire=0;dmqreplicate=DMQ_REPLICATE_ENABLED;initval=0;\")\n#!endif\n\n# gw2gwroup is used to lookup gwgroupid\nmodparam(\"htable\", \"htable\", \"gw2gwgroup=>size=8;autoexpire=0;dmqreplicate=DMQ_REPLICATE_ENABLED;dbtable=dsip_gw2gwgroup;cols='gwid,gwgroupid';\")\n# gwgroup2lb is used to lookup the dispatcher setid associated with a gwgroupid\nmodparam(\"htable\", \"htable\", \"gwgroup2lb=>size=8;autoexpire=0;dmqreplicate=DMQ_REPLICATE_ENABLED;dbtable=dsip_gwgroup2lb;cols='gwgroupid,setid,enabled';\")\n# inbound_hardfwd is used to lookup did and dr_groupid for forwarding calls unconditionally\nmodparam(\"htable\", \"htable\", \"inbound_hardfwd=>size=8;autoexpire=0;dmqreplicate=DMQ_REPLICATE_ENABLED;dbtable=dsip_hardfwd;cols='dr_ruleid,did,dr_groupid';\")\n# inbound_failfwd is used to lookup did and dr_groupid for forwarding calls on failover\nmodparam(\"htable\", \"htable\", \"inbound_failfwd=>size=8;autoexpire=0;dmqreplicate=DMQ_REPLICATE_ENABLED;dbtable=dsip_failfwd;cols='dr_ruleid,did,dr_groupid';\")\n# prefix_to_route is used to lookup dr_ruleid for a prefix\nmodparam(\"htable\", \"htable\", \"prefix_to_route=>size=8;autoexpire=0;dmqreplicate=DMQ_REPLICATE_ENABLED;dbtable=dsip_prefix_mapping;cols='prefix,ruleid,priority';\")\n# to manage Pass Thru Auth for Registration. Used to send Authorization requests back to the same backend media server\nmodparam(\"htable\", \"htable\", \"pass_thru_auth=>size=8;autoexpire=3600;dmqreplicate=DMQ_REPLICATE_ENABLED;\")\n# enrichdnid_lnpmap is used to lookup dnid prefixes to match against\n#!ifdef WITH_DNID_LNP_ENRICHMENT\nmodparam(\"htable\", \"htable\", \"enrichdnid_lnpmap=>size=8;autoexpire=0;dmqreplicate=DMQ_REPLICATE_ENABLED;dbtable=dsip_dnid_lnp_mapping;cols='dnid,prefix';\")\n#!endif\n# Pass-Thru Auth IP to Domain mapping lookup. Allows PJSIP Pass-Thru to work correctly\n#modparam(\"htable\", \"htable\", \"pbxip2domain=>size=8;autoexpire=0;dmqreplicate=DMQ_REPLICATE_ENABLED\")\n\n# ----- http_async_client params -----\nmodparam(\"http_async_client\", \"workers\", 1)\nmodparam(\"http_async_client\", \"connection_timeout\", 500)\nmodparam(\"http_async_client\", \"hash_size\", 2048)\n#!ifdef WITH_DEBUG\nmodparam(\"http_async_client\", \"curl_verbose\", 1)\n#!endif\n#!ifdef WITH_TLS\nmodparam(\"http_async_client\", \"tls_client_cert\", \"/etc/dsiprouter/certs/dsiprouter-cert.pem\")\nmodparam(\"http_async_client\", \"tls_client_key\", \"/etc/dsiprouter/certs/dsiprouter-key.pem\")\nmodparam(\"http_async_client\", \"tls_ca_path\", \"/etc/dsiprouter/certs/\")\n#!endif\n\n# ---- sip trace params ----\n#!ifdef WITH_HOMER\nmodparam(\"siptrace\", \"duplicate_uri\", \"sip:HOMER_HOST:HEP_PORT\")\nmodparam(\"siptrace\", \"hep_version\", 3)\nmodparam(\"siptrace\", \"hep_mode_on\", 1)\nmodparam(\"siptrace\", \"hep_capture_id\", HOMER_ID)\nmodparam(\"siptrace\", \"trace_to_database\", 0)\nmodparam(\"siptrace\", \"trace_on\", 1)\nmodparam(\"siptrace\", \"trace_mode\", 1)\nmodparam(\"siptrace\", \"trace_sl_acks\", 1)\n#!endif\n\n\n#!ifdef WITH_PUSH\nmodparam(\"htable\", \"htable\", \"push=>size=10;autoexpire=120;\")\n#!endif\n\n#======================================================================\n# Routing Logic\n#======================================================================\n\n# Main SIP request routing logic\n# - executed for each SIP request received from the network\n# - specific request processing logic should be abstracted into sub-routes\n# - sub-routes are defined using the syntax: route[...] { ... }\n# - sub-routes can be executed within any routing block\nrequest_route {\n#!ifdef WITH_DEBUG\n\txlog(\"L_DBG\", \"processing request from $sas on $Rut\\n\");\n#!endif\n\n\t# handle DMQ messages\n\troute(DMQ);\n\n\t# per request initial checks\n\troute(REQINIT);\n\n#!ifdef WITH_STIRSHAKEN\n\tif ((int)$sel(cfg_get.stir_shaken.stir_shaken_enabled)) {\n\t\troute(STIRSHAKEN_INBOUND);\n\t}\n#!endif\n\n\t# NAT detection\n\troute(NATDETECT);\n\n\t# CANCEL processing\n\troute(HANDLE_CANCEL);\n\n\t# is this request allowed to manage dialogs?\n\troute(CHECK_DLG_ALLOWED);\n\n\t# handle requests within SIP dialogs\n\troute(WITHINDLG);\n\n\t# handle retransmissions\n\troute(HANDLE_RETRANS);\n\n\t# authentication\n\troute(AUTH);\n\n\t# store the call source info for later usage\n\troute(SET_CALLSRC_INFO);\n\n\t# apply NAT changes after dialog is setup\n\troute(NATMANAGE);\n\n\t# handle registrations\n\troute(REGISTRAR);\n\n\t# handle presence related requests\n\troute(PRESENCE);\n\n\t# dispatch to local endpoints that registered thru the proxy\n\troute(LOCATION);\n\n\t# enrich dialed number before routing\n\troute(ENRICH_DNID);\n\n#!ifdef WITH_TRANSNEXUS\n\t# Process call if Transnexus validation service is enabled\n\tif ((int)$sel(cfg_get.transnexus.verifyservice_enabled) && !isflagset(FLT_HAS_TOTAG)) {\n\t\troute(TRANSNEXUS_INBOUND);\n\t}\n#!endif\n\n\t# route the call to the next hop\n\troute(NEXTHOP);\n}\n\n# Main SIP response handling logic\n# - executed for each SIP response received from the network\n# - specific reply processing logic should be abstracted into sub-routes\n# - onreply routes should not be used here and are only executed when set w/ t_on_reply()\n# - sub-routes called here are executed by the core (onreply routes are executed by tm module)\nreply_route {\n#!ifdef WITH_DEBUG\n\txlog(\"L_DBG\", \"processing reply from $sas on $Rut\\n\");\n#!endif\n\n\treturn;\n}\n\n# Pre-Send SIP request handling logic\n# - executed prior to forwarding specific SIP requests from the network\n# - not executed for replies, retransmissions, or locally generated messages\n# - a very limited set of core functions are available here, no sub-routes either\nonsend_route {\n#!ifdef WITH_DEBUG\n\txlog(\"L_DBG\", \"sending message\\n\");\n#!endif\n\n\treturn;\n}\n\nroute[HANDLE_CANCEL] {\n\tif (is_method(\"CANCEL\")) {\n\t\tif (t_check_trans()) {\n\t\t\troute(RELAY);\n\t\t}\n\t \troute(RTPENGINEDELETE);\n\t\texit;\n\t}\n}\n\nroute[HANDLE_RETRANS] {\n\tif (t_precheck_trans()) {\n\t\tt_check_trans();\n\t\texit;\n\t}\n\tt_check_trans();\n}\n\nroute[DMQ] {\n#!ifdef WITH_DMQ\n\tif ($rm == \"KDMQ\" && $rp == DMQ_PORT) {\n\t\tdmq_handle_message();\n\t\texit;\n\t}\n#!endif\n\n\treturn;\n}\n\nroute[REFORMATRURI] {\n\txlog(\"L_DBG\", \"original rU <$rU> and original tU <$tU>\\n\");\n\n\t# This is to deal with those who are used to dialing 7 digits\n\t# assuming that the 7 digit number being dialed is in the same area code as the FROM number\n\tif ($(rU{s.len}) == 7) {\n\t\tif ($(fU{s.len}) == 10) {\n\t\t\t$rU = $(fU{s.substr,0,3}) + $rU;\n\t\t}\n\t\telse {\n\t\t\t$rU = $(fU{s.substr,0,4}) + $rU;\n\t\t}\n\t\t$tU = $rU;\n\t}\n\telse if ($(rU{s.len}) > 10) {\n\t\t# Check for +1 and remove it from the RURI and the To header\n\t\tif ($(rU{s.substr,0,2}) == \"+1\") {\n\t\t\t$rU = $(rU{s.substr,2,0});\n\t\t\t$tU = $rU;\n\t\t}\n\t\t# Check for 1 and remove it from the RURI and the To header\n\t\telse if ($(rU{s.substr,0,1}) == \"1\") {\n\t\t\t$rU = $(rU{s.substr,1,0});\n\t\t\t$tU = $rU;\n\t\t}\n\t}\n\n\txlog(\"L_DBG\", \"modified rU <$rU> and modified tU <$tU>\\n\");\n}\n\nroute[ENRICH_SIPHEADER] {\n\tif (!strempty($xavp(ra=>sipdomain))) {\n\t\tappend_hf(\"X-SIPDOMAIN: $xavp(ra=>sipdomain)\\r\\n\");\n\t}\n}\n\nroute[ENRICH_DNID] {\n\troute(ENRICH_DNID_LNP);\n\t#route(REFORMATRURI);\n}\n\nroute[ENRICH_DNID_LNP] {\n#!ifdef WITH_DNID_LNP_ENRICHMENT\n\t# enrich dialed number with country code and area code\n\t$var(dnid) = $(rU{s.unescape.user});\n\t$avp(dnid_prefix) = $sht(enrichdnid_lnpmap=>$var(dnid));\n\tif ($avp(dnid_prefix) != $null && !strempty($avp(dnid_prefix))) {\n\t\t$rU = $(avp(dnid_prefix){s.escape.user}) + $rU;\n\t\t$tU = $(avp(dnid_prefix){s.escape.user}) + $tU;\n\t}\n#!endif\n\n\treturn;\n}\n\n# Route the call to the next hop, which can be a PBX or Carrier\nroute[NEXTHOP] {\n\t######################################\n\t# Endpoint to PBX via Domain Routing #\n\t######################################\n\n\t# TODO: pbx_type in domain_attrs is not standardized, standardize this in v0.80\n\tif (isflagset(FLT_DOMAINROUTING) && !isflagset(FLT_EXTERNAL_AUTH)) {\n\t\t#Grab the value of the avp that contains the domain_pbx_ip.\n\t\t#This is where requests for that domain should be routed\n\n\t\t# Route to the endpoints defined in the Endpoint Gateway list (dr_gw_list)\n\t\tif ($avp(domain_pbx_type) == \"2\") {\n\t\t\txlog(\"L_INFO\", \"<$ci> Routing to Endpoint Gateway List\\n\");\n\n\t\t\tif (!strempty($sht(pass_thru_auth=>$ci))) {\n\t\t\t\t$du = $sht(pass_thru_auth=>$ci);\n\t\t\t\t\txlog(\"L_INFO\", \"DOMAINROUTING last du was $du\\n\");\n\t\t\t}\n\t\t\t# Forward the registration onto one of the servers in the cluster\n\t\t\telse if (!strempty($avp(domain_dispatcher_set_id))) {\n\t\t\t\t$avp(dispatcher_setid) = $avp(domain_dispatcher_set_id);\n\t\t\t\tif (!strempty($avp(domain_dispatcher_reg_alg))) {\n\t\t\t\t\t# Set the registration algoritm\n\t\t\t\t\t$avp(dispatcher_alg) = $avp(domain_dispatcher_reg_alg);\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t# Set the dispatcher algorthim to round robin by default\n\t\t\t\t\t$avp(dispatcher_alg) = DSTALG_ROUND_ROBIN;\n\t\t\t\t}\n\n\t\t\t\troute(DISPATCHER_SELECT);\n\t\t\t}\n\t\t}\n\t\t# Otherwise, send to the single PBX defined by PBX_IP\n\t\telse {\n\t\t\txlog(\"L_INFO\", \"DOMAINROUTING Routing to Single Endpoint Gateway\\n\");\n\t\t\t$du = \"sip:\" + $avp(domain_pbx_ip);\n\t\t\t# TODO: are these lookups valid here??\n\t\t\troute(SET_CALLDST_INFO);\n\t\t\troute(CHECK_CALL_LIMIT);\n\t\t\troute(SETUP_CALLAUTH_INFO);\n\t\t \troute(SETUP_DIALOG);\n\t\t}\n\t\txlog(\"L_INFO\", \"DOMAINROUTING should be routed to $rd:$rp\\n\");\n\n\t\troute(RELAY);\n\t\texit;\n\t}\n\n\t#Route to one PBX using an algorithm with External Authentication\n\t#(aka We are acting as a Registration and Location Server)\n\telse if (isflagset(FLT_DOMAINROUTING) && isflagset(FLT_EXTERNAL_AUTH) && !is_method(\"REGISTER\")) {\n\t\tif (strempty($avp(domain_dispatcher_set_id))) {\n\t\t\tsend_reply(\"404\", \"Destination Not Found\");\n\t\t\texit;\n\t\t}\n\t\telse {\n\t\t\t$avp(dispatcher_setid) = $avp(domain_dispatcher_set_id);\n\t\t\txlog(\"L_INFO\", \"DOMAINROUTING Routing for $fd via dispatcher set $avp(dispatcher_setid)\\n\");\n\t\t}\n\n\t\t# set the algorithm to load balancing if not set\n\t\tif (strempty($avp(domain_dispatcher_alg))) {\n\t\t\t$avp(dispatcher_alg) = DSTALG_ROUND_ROBIN;\n\t\t}\n\t\telse {\n\t\t\t$avp(dispatcher_alg) = $avp(domain_dispatcher_alg);\n\t\t}\n\n#!ifdef WITH_MSTEAMS\n\t\t# check if destination is msteams\n\t\tif ($avp(domain_pbx_type) == \"3\") {\n\t\t\tsetbflag(FLB_DST_MSTEAMS);\n\t\t\t$dlg_var(dst_msteams_domain) = $fd;\n\n\t\t\t# contact must match the domain of the TLS cert (i.e. the CN)\n\t\t\tremove_hf(\"Contact\");\n\t\t\tappend_hf(\"Contact: <sip:$fd:SIPS_PORT;transport=tls>\\r\\n\");\n\t\t\txlog(\"L_DBG\", \"Changed contact to $ct\\n\");\n\n\t\t\t# prevent MSTeams from sending REFER requests\n\t\t\tif ((int)$sel(cfg_get.server.msteams_disable_refer)) {\n\t\t\t\troute(REMOVE_REFER);\n\t\t\t}\n\t\t\troute(DISPATCHER_SELECT);\n\t\t\troute(RELAY);\n\t\t\texit;\n\t\t}\n#!endif\n\n\t\t# routing to PBX destination set\n\t\troute(ENRICH_SIPHEADER);\n\t\troute(DISPATCHER_SELECT);\n\t\troute(RELAY);\n\t\texit;\n\t}\n\n#!ifdef WITH_DROUTE\n\t######################################\n\t# From Carrier                       #\n\t######################################\n\n\t# Check if this is coming from carrier\n\tif (isbflagset(FLB_SRC_CARRIER)) {\n\t\txlog(\"L_INFO\", \"The call coming from $si is from a carrier\\n\");\n\n\t\t# Enrich inbound calls from carriers\n\t\troute(ENRICH_CARRIER_INBOUND);\n\n\t\t# Route to PBX\n\t\txlog(\"L_DBG\", \"DROUTING Logic for routing to PBX\\n\");\n\t\tappend_hf(\"P-hint: inbound\\r\\n\");\n\t\t$avp(calltype) = \"inbound\";\n\n\t\t# don't overwrite fwding info if we already set it\n\t\tif (!isflagset(FLT_FAILOVER)) {\n\t\t\troute(SET_CALLFWD_INFO);\n\t\t}\n\n\t\t# don't overwrite dr_groupid if this is n'th time executing this route\n\t\tif ($avp(dr_groupid) == $null) {\n\t\t\t# first time executing, save the dialed DID for forwarding features\n\t\t\t$avp(dr_saved_rU) = $rU;\n\n\t\t\t# check for hard fwd, then set did and dr_groupid accordingly\n\t\t\tif ($avp(hardfwdinfo) != $null) {\n\t\t\t\t# allow DID to be unchanged\n\t\t\t\tif (!strempty($(avp(hardfwdinfo){s.select,0,,}))) {\n\t\t\t\t\t$rU = $(avp(hardfwdinfo){s.select,0,,});\n\t\t\t\t}\n\t\t\t\t$avp(dr_groupid) = $(avp(hardfwdinfo){s.select,1,,}{s.int});\n\t\t\t}\n\t\t\t# otherwise we are using inbound mapping rules\n\t\t\telse {\n\t\t\t\t$avp(dr_groupid) = FLT_INBOUND;\n\t\t\t}\n\t\t}\n\n\t\t# try routing based on rules in dr_rules table\n\t\tif (!do_routing($avp(dr_groupid))) {\n\t\t\txlog(\"L_WARN\", \"RURI routing failed, trying to route on the To header\\n\");\n\t\t\t$rU = $tU;\n\t\t\t# otherwise as a last ditch effort try to route based on To header\n\t\t\tif (!do_routing($avp(dr_groupid))) {\n\t\t\t\t# No rules defined for the phone number\n\t\t\t\txlog(\"L_WARN\", \"No rules defined for $rU coming from this carrier endpoint: $si\\n\");\n\t\t\t\tsl_reply(\"500\", \"No rules defined for number\");\n\t\t\t\texit;\n\t\t\t}\n\t\t}\n\t\t# found a match, set du to match what drouting selected\n\t\t$du = $(ru{uri.duri});\n\n\t\troute(MAINTMODE_CHECK);\n\t\troute(SET_CALLDST_INFO);\n\t\troute(CHECK_CALL_LIMIT);\n\t   \troute(SETUP_CALLAUTH_INFO);\n\t\troute(SETUP_DIALOG);\n\n\t\t# handle clientside NAT for the rest of the dialog\n\t\t# TODO: the other NAT checks are all over the place, we should simplify and aggregate them\n\t\tif (nat_uac_test(\"64\")) {\n\t\t\tadd_contact_alias();\n\t\t}\n\n\t\t# check if routing via dispatcher\n\t\tif ($avp(dispatcher_setid) != $null && $avp(lb_enabled)) {\n\t\t\t# dispatcher algo is always weighted here\n\t\t\t$avp(dispatcher_alg) = DSTALG_RELATIVE_WEIGHT;\n\t\t\troute(DISPATCHER_SELECT_LB);\n\t\t}\n\n#!ifdef WITH_MSTEAMS\n\t\t# Check if routing to MSTeams\n\t\tif ($avp(dr_attrs) != $null) {\n\t\t\tif ($(dlg_var(dst_msteams_domain){s.len}) > 0) {\n\t\t\t\tsetbflag(FLB_DST_MSTEAMS);\n\n\t\t\t\t# TODO: review in v0.80, do we need to change any of these other headers?\n\t\t\t\t#if (!subst_hf(\"Remote-Party-ID\", \"/^(.*<sips?:[0-9]+@)(.*?)(:?[0-9]{1,5}?>.*)$/\\1$dlg_var(dst_msteams_domain)\\3/\", \"f\")) {\n\t\t\t\t#\txlog(\"L_ERR\", \"failed updating Remote-Party-ID\\n\");\n\t\t\t\t#}\n\t\t\t\t#\n\t\t\t\t#$fd = $dlg_var(dst_msteams_domain);\n\t\t\t\t#$td = $dlg_var(dst_msteams_domain);\n\n\t\t\t\t# drouting does not have an easy way to update the transport on a matched address\n\t\t\t\t# not wasting the time to go through and integrate it into the dr_attrs column\n\t\t\t\t$du = \"sip:\" + $rd + \":\" + $rp + \";transport=tls\";\n\n\t\t\t\t# contact must match the domain of the TLS cert (i.e. the CN)\n\t\t\t\tremove_hf(\"Contact\");\n\t\t\t\tappend_hf(\"Contact: <sip:$dlg_var(dst_msteams_domain):SIPS_PORT;transport=tls>\\r\\n\");\n\t\t\t\txlog(\"L_DBG\", \"Changed contact to $ct\\n\");\n\n\t\t\t\tif (is_present_hf(\"ALLOW\")) {\n\n\t\t\t\t\t#Prevent MSTeams from sending REFER requests\n\t\t\t\t\tif ((int)$sel(cfg_get.server.msteams_disable_refer)) {\n\t\t\t\t\t\troute(REMOVE_REFER);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tappend_hf(\"ALLOW: INVITE,ACK,OPTIONS,CANCEL,BYE,NOTIFY\\r\\n\");\n\t\t\t\t}\n\n\t\t\t\t\n \n\n\t\t\t\t# set RTP to RTP/SAVP because all MSTeams audio needs to use RTP/SAVP\n\t\t\t\t$dlg_var(dst_media) = \"rtp_savp\";\n\t\t\t}\n\t\t}\n#!endif\n\n\t\t# Set INVITE  max lifetime to ensure Primary and Secondary PBX server feature works.\n\t\tt_set_fr((int)$sel(cfg_get.server.pbx_invite_timeout_aftertry), (int)$sel(cfg_get.server.pbx_invite_timeout));\n\n\t\t#route(SET_CALLID_INBOUND_ENDPOINT_MAP);\n\t\troute(RELAY);\n\t\texit;\n\t}\n\n\t######################################\n \t# To Carrier                         #\n \t######################################\n\n\telse if (isbflagset(FLB_SRC_PBX) || isflagset(FLT_PBX_AUTH) || isbflagset(FLB_SRC_MSTEAMS)) {\n\t\txlog(\"L_INFO\", \"The call coming from $si will be routed to carrier groups via drouting\\n\");\n\n\t\tappend_hf(\"P-hint: outbound\\r\\n\");\n\t\t$avp(calltype) = \"outbound\";\n\n#!ifdef WITH_LCR\n\t\t# LCR Routing\n\t\t#   - route based on from prefix and to prefix\n\t\t#   - match selection is similar to dRouting module from longest to shortest match\n\t\t# Logic Summary:\n\t\t#   1. iterate through htable matching entries starting with prefixes\n\t\t#   2. find diff between match and lookup (must be absolute value)\n\t\t#   3. if diff is less than previous overwrite match\n\t\t#   4. if a match is present attempt to set carrier group and relay\n\t\t# TODO:\n\t\t#   we could store and iterate through all matches if we use dispatcher instead\n\t\t#   this would allow failover in LCR Routing to shorter prefixes (if we wanted that)\n\t\t$var(lookup) = $(fU{s.unescape.user}) + \"-\" + $(tU{s.unescape.user});\n\t\t$avp(lcr_match_group) = $null;\n\t\t$var(lcr_match_diff) = 1000;\n\t\t$var(diff) = 1000;\n\n\t\tsht_iterator_start(\"iter\", \"tofromprefix\");\n\t\twhile(sht_iterator_next(\"iter\")) {\n\t\t\t$var(regex) = $(shtitkey(iter){s.select,0,-}{re.subst,/(\\+|\\*|\\#)/\\\\\\1/g}) + \"([0-9])*-\" +\n\t\t\t\t$(shtitkey(iter){s.select,-1,-}{re.subst,/(\\+|\\*|\\#)/\\\\\\1/g}) + \"([0-9])*\";\n\t\t\tif ($var(lookup) =~ $var(regex)) {\n\t\t\t\txlog(\"L_DBG\", \"LCR match on $shtitkey(iter)\\n\");\n\t\t\t\t$var(diff) = $(var(lookup){s.len}) - $(shtitkey(iter){s.len});\n\t\t\t\t# bitwise absolute value: ( mask = n>>31; (mask^n) - mask )\n\t\t\t\t# we are assuming 32 bit integers\n\t\t\t\t$var(mask) = $var(diff) >> 31;\n\t\t\t\t$var(diff) = ($var(mask) ^ $var(diff)) - $var(mask);\n\t\t\t\tif ($var(diff) < $var(lcr_match_diff)) {\n\t\t\t\t\txlog(\"L_DBG\", \"LCR prefix closer match diff=$var(diff)\\n\");\n\t\t\t\t\t$avp(lcr_match_group) = $shtitval(iter);\n\t\t\t\t\t$var(lcr_match_diff) = $var(diff);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tsht_iterator_end(\"iter\");\n\n\t\tif ($avp(lcr_match_group) > 0) {\n\t\t\t$avp(carrier_groupid) = $avp(lcr_match_group);\n\t\t}\n\t\telse {\n\t\t\t$avp(carrier_groupid) = FLT_OUTBOUND;\n\t\t}\n#!else\n\t\t$avp(carrier_groupid) = FLT_OUTBOUND;\n#!endif\n\n\t\tif (do_routing($avp(carrier_groupid))) {\n\t\t\t# set du to match what drouting selected\n\t\t\t$du = $(ru{uri.duri});\n\n\t\t\troute(SET_CALLDST_INFO);\n\t\t\troute(CHECK_CALL_LIMIT);\n\t\t\troute(SETUP_CALLAUTH_INFO);\n\t\t \troute(SETUP_DIALOG);\n\n\t\t\t# Checking if the carrier is using username/password auth\n\t\t\tif (!strempty($dlg_var(dst_auth_domain))) {\n\t\t\t\t$rd = $dlg_var(dst_auth_domain);\n\t\t\t\tsubst_hf(\"Contact\", \"/(.*)?<(sips?):([0-9]+)@([^:]+)(:[0-9]{1,5})?(;.*)?>(.*)?/<\\2:\\3@UAC_REG_ADDR:$Rp\\6>\\7/\", \"f\");\n\t\t\t\tremove_hf(\"P-Asserted-Identity\");\n\t\t\t\tappend_hf(\"P-Asserted-Identity: <sip:$avp(auth_user)@$rd>\\r\\n\");\n\t\t\t}\n\t\t\telse {\n\t\t\t\tsubst_hf(\"Contact\", \"/(.*)?<(sips?):([0-9]+)@([^:]+)(:[0-9]{1,5})?(;.*)?>(.*)?/<\\2:\\3@EXTERNAL_IP_ADDR:$Rp\\6>\\7/\", \"f\");\n\t\t\t\tremove_hf(\"P-Asserted-Identity\");\n\t\t\t\tappend_hf(\"P-Asserted-Identity: <sip:$fU@$rd>\\r\\n\");\n\t\t\t}\n\n\t\t\t# drouting only removes prefix from ru, typically we want it removed from the To as well\n\t\t\tuac_replace_from(\"\\\"$fU\\\"\", \"$rz:$fU@$rd:$rp\");\n\t\t\tuac_replace_to(\"\\\"$tU\\\"\", \"$rz:$tU@$rd:$rp\");\n\n\t\t\tmsg_apply_changes();\n\t\t\tif (!isbflagset(FLB_SRC_MSTEAMS)) {\t\n\t\t\t\tadd_contact_alias();\n\t\t\t}\n\t\t\troute(ENRICH_CARRIER_OUTBOUND);\n\n\t\t\t# check if routing via dispatcher\n\t\t\tif ($avp(dispatcher_setid) != $null && $avp(lb_enabled)) {\n\t\t\t\t# dispatcher algo is always weighted here\n\t\t\t\t$avp(dispatcher_alg) = DSTALG_RELATIVE_WEIGHT;\n\t\t\t\troute(DISPATCHER_SELECT_LB);\n\t\t\t}\n\t\t\troute(RELAY);\n\t\t\texit;\n\t\t}\n\t\t# No rules defined for the phone number\n\t\telse {\n\t\t\txlog(\"L_WARN\", \"No rules defined for $fu going to $tu\\n\");\n\t\t\tsl_reply(\"500\", \"No rules defined for number\");\n\t\t}\n\t}\n\telse {\n\t\tsl_send_reply(\"407\", \"Proxy Authentication Required. Add the PBX or Carrier IP using GUI\");\n\t}\n#!endif\n}\n\nroute[NEXTHOP_FAILOVER] {\n\t # check for failover fwd, then set did and dr_groupid accordingly\n\tif (($avp(failfwdinfo) != $null) && !isflagset(FLT_FAILOVER)) {\n\t\t# flag to make sure we don't loop endlessly\n\t\tsetflag(FLT_FAILOVER);\n\n\t\t# reset DID if user did not explicitly ask to overwrite it\n\t\tif (strempty($(avp(failfwdinfo){s.select,0,,}))) {\n\t\t\t$rU = $avp(dr_saved_rU);\n\t\t}\n\t\telse {\n\t\t\t$rU = $(avp(failfwdinfo){s.select,0,,});\n\t\t}\n\t\t$avp(dr_groupid) = $(avp(failfwdinfo){s.select,1,,}{s.int});\n\n\t\t# go back through NEXTHOP to allow our standard routing checks to run for this dr_group\n\t\troute(NEXTHOP);\n\t\t# we successfully routed via NEXTHOP, return true if we did not exit\n\t\treturn 1;\n\t}\n\n\t# return false if we did not route the call\n\treturn -1;\n}\n\n# MaintMode Check - recursive function for checking if a number is in maintmode\nroute[MAINTMODE_CHECK] {\n\txlog(\"L_DBG\", \"The request domain $rd before maintmode check\\n\");\n\n\tif ($sht(maintmode=>$rd) != $null) {\n\t\txlog(\"L_DBG\", \"request $rd is in maintenance mode\\n\");\n\t\t# The selected endpoint is in maintenance mode, try next endpoint\n\t\t# If there is only one endpoint then immediately return Service not Available\n\t\t# Otherwise, select the next gateway and see if it is in maintenance mode\n\t\tif (!use_next_gw()) {\n\t\t\txlog(\"L_DBG\", \"request $rd has no other gateways available\\n\");\n\t\t\tsl_send_reply(\"503\", \"Service not available\");\n\t\t\texit;\n\t\t}\n\t\t# set du to match what drouting selected\n\t\t$du = $(ru{uri.duri});\n\n\t\troute(MAINTMODE_CHECK);\n\t}\n}\n\n#!ifdef WITH_TRANSNEXUS\n\t\timport_file \"transnexus.cfg\"\n#!endif\n\n#!ifdef WITH_STIRSHAKEN\n\t\timport_file \"stir-shaken.cfg\"\n#!endif\n\n\n# TeleBlock routing\nroute[TELEBLOCK] {\n\tif (!isbflagset(FLB_SRC_PBX)) {\n\t\txlog(\"L_DBG\", \"source is not a pbx, skipping teleblock blacklist check\\n\");\n\t\treturn;\n\t}\n\n# TODO: This should be dynamic\n\t# Only route to teleblock if User-to-User header is present\n#    if (!is_present_hf(\"User-to-User\")) {\n#            xlog(\"L_DBG\", \"User-to-User header not found\\n\");\n#            return;\n#    }\n\n\t# save the requested route selections before overwriting for teleblock\n\t$avp(tb_saved_fu) = $fu;\n\t$avp(tb_saved_ru) = $ru;\n\t$avp(tb_saved_du) = $du;\n\n\t# Change source address to this proxy server\n\t$fu = \"sip:\" + $fU + \"@\" + $Ri + \":\" + $Rp;\n\n\t# Send Invite to TeleBlock with header fields:\n\t# Number  ==  To Username\n\t# CPN     ==  From Username\n\t# BTN     ==  Billing Number (optional)\n\t# Zipcode ==  US Postal Code (optional)\n\t# refkey  ==  Record ID      (optional)\n\n\t$ru = \"sip:\" + $rU + \"@\" + $sel(cfg_get.teleblock.gw_ip) + \":\" + $sel(cfg_get.teleblock.gw_port);\n\t$du = $(ru{uri.duri});\n\n\txlog(\"L_DBG\", \"Forwarding to teleblock: fu=$fu ru=$ru du=$du\\n\");\n\n\t# set failure route\n\tif (is_method(\"INVITE\")) {\n\t\tt_on_failure(\"TELEBLOCK_FAILURE\");\n\t}\n}\n\n\nfailure_route[TELEBLOCK_FAILURE] {\n\tif (t_is_canceled()) {\n\t\texit;\n\t}\n\n\txlog(\"L_DBG\", \"Processing reply for: $rU\\n\");\n\n\t# Check if a media server is setup for teleblock\n\tif (strempty($sel(cfg_get.teleblock.media_ip)) || strempty($sel(cfg_get.teleblock.media_port))) {\n\t\t$avp(s:teleblock_media_enabled) = \"0\";\n\t}\n\telse {\n\t\t$avp(s:teleblock_media_enabled) = \"1\";\n\t}\n\n\t# interpret teleblock response\n\tif (t_check_status(\"499\")) {\n\t\t$fu = $avp(tb_saved_fu);\n\t\t$ru = $avp(tb_saved_ru);\n\t\t$du = $avp(tb_saved_du);\n\t\tt_relay();\n\t\texit;\n\t}\n\n\txlog(\"L_DBG\", \"Relaying to: $sel(cfg_get.teleblock.media_ip):$sel(cfg_get.teleblock.media_port)\\n\");\n\n\tif (t_check_status(\"403|433\")) {\n\t\tif ($avp(s:teleblock_media_enabled) == \"1\") {\n\t\t\t# make sure media server can route back to kamailio\n\t\t\troute(SET_RECORD_ROUTE);\n\t\t\t$ru = \"sip:\" + $rU + \"@\" + $sel(cfg_get.teleblock.media_ip) + \":\" + $sel(cfg_get.teleblock.media_port);\n\t\t\tif (!t_relay()) {\n\t\t\t\tt_reply(\"403\", \"Do-Not-Contact\");\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tif (!t_relay()) {\n\t\t\t\tt_reply(\"403\", \"Do-Not-Contact\");\n\t\t\t}\n\t\t}\n\t}\n\telse {\n\t\tif ($avp(s:teleblock_media_enabled) == \"1\") {\n\t\t\t# make sure media server can route back to kamailio\n\t\t\troute(SET_RECORD_ROUTE);\n\t\t\t$ru = \"sip:\" + $rU + \"@\" + $sel(cfg_get.teleblock.media_ip) + \":\" + $sel(cfg_get.teleblock.media_port);\n\t\t\tif (!t_relay()) {\n\t\t\t\tt_reply(\"500\", \"Connection Failure\");\n\t\t\t}\n\t\t}\n\t}\n\texit;\n}\n\n# Wrapper for relaying requests\nroute[RELAY] {\n\tif (!has_totag()) {\n\t\t# update the signalling / media settings per the destination endpoint\n\t\troute(SET_DST_SIGNALLING);\n\t\troute(SET_DST_MEDIA);\n\t\troute(SET_CALLROUTE_INFO);\n\t}\n\t # From WebSocket\n        else if (has_totag() && isflagset(FLT_SRC_WS)) {\n                # update the signalling / media settings per the destination endpoint\n                $dlg_var(dst_media) = \"rtp_avp\";\n                # If $rU is empty\n                if ($rU == $null) {\n                        $rU = $tU;\n                }\n                route(SET_DST_SIGNALLING);\n                route(SET_DST_MEDIA);\n\n        }\n \t# Carrier to MSTeams REINVITE\n\t#else if (has_totag() && allow_address(FLT_MSTEAMS, \"$(du{uri.host})\", \"$(du{uri.port})\")) {\n\telse if (has_totag() && isbflagset(FLB_DST_MSTEAMS)) {\n\t\t\n\t\txlog(\"L_INFO\", \"+++ Carrier to MSTeams ru=$ru si=$si, du=$du\");\n        \n\t        # update the signalling / media settings per the destination endpoint\n                $dlg_var(dst_media) = \"rtp_savp\";\n                route(SET_DST_SIGNALLING);\n                route(SET_DST_MEDIA);\n\t\troute(SET_CALLROUTE_INFO);\n\t\t\n\t\t# Set the $ru to the contact from the original INVITE\n\t\t#if (is_method(\"INVITE|ACK\") && !allow_address(FLT_MSTEAMS, \"$(du{uri.host})\", \"$(du{uri.port})\")) {\n\t\t#\t\t\t$ru = $dlg_var(dst_cturi);\n\t\t#}\n\t        \t\n\t\t#append_hf(\"Contact: <sip:$dlg_var(dst_msteams_domain):SIPS_PORT;transport=tls>\\r\\n\");\n        }\n \t# MSTeams to Carrier REINVITE or REFER\n        else if (has_totag() && isbflagset(FLB_SRC_MSTEAMS)) {\n\n            # update the signalling / media settings per the destination endpoint\n\t\t\t$dlg_var(dst_media) = \"rtp_avp\";\n\t\t\troute(SET_DST_SIGNALLING);\n\t\t\troute(SET_DST_MEDIA);\n\n\t\t\txlog(\"L_INFO\", \"+++ MSTeams to Carrier ru=$ru si=$si du=$du\");\n\t\t\t\n\t\t\tif (is_method(\"INVITE|ACK\") && is_myself($rd)) {\n\t\t\t\t$ru = $dlg_var(dst_cturi);\n\t\t\t}\n\n\t\t\tif (is_method(\"REFER\")) {\n\t\t\t\troute(SET_RECORD_ROUTE);\n\t\t\t}\n        }\n\t# check for serverside NAT\n\troute(SERVERNATDETECT);\n\t# only set Record-Route and destination info for dialog creating/updating requests\n\tif (is_method(\"INVITE|SUBSCRIBE|REFER\") && !has_totag()) {\n\t\troute(SET_RECORD_ROUTE);\n\t}\n\n\tif (is_method(\"NOTIFY|BYE\") && is_rfc1918(\"$rd\")) {\n                if (!lookup(\"location\",\"sip:$tU@$td\")) {\n                        xlog(\"L_INFO\", \"Notify Failed - NAT'd address for $tU@$td not found\");\n                }\n        }\n\n\t# handle STIR/SHAKEN\n#!ifdef WITH_TRANSNEXUS\n\tif ((int)$sel(cfg_get.transnexus.authservice_enabled)) {\n\t\troute(TRANSNEXUS_OUTBOUND);\n\t}\n#!endif\n\n#!ifdef WITH_STIRSHAKEN\n\tif ((int)$sel(cfg_get.stir_shaken.stir_shaken_enabled)) {\n\t\troute(STIRSHAKEN_OUTBOUND);\n\t}\n#!endif\n\n\t# Check TeleBlock Blacklist\n#!ifdef WITH_TELEBLOCK\n\tif ((int)$sel(cfg_get.teleblock.gw_enabled)) {\n\t\troute(TELEBLOCK);\n\t}\n#!endif\n\n\n\t# enable additional event routes for forwarded requests\n\t# - serial forking, RTP relaying handling, a.s.o.\n\tif (is_method(\"INVITE|BYE|SUBSCRIBE|UPDATE\")) {\n\t\tif (!t_is_set(\"branch_route\")) t_on_branch(\"MANAGE_BRANCH\");\n\t}\n\tif (is_method(\"INVITE|SUBSCRIBE|UPDATE\")) {\n\t\tif (!t_is_set(\"onreply_route\")) t_on_reply(\"MANAGE_REPLY\");\n\t}\n\tif (is_method(\"INVITE\")) {\n\t\tif (!t_is_set(\"failure_route\")) t_on_failure(\"MANAGE_FAILURE\");\n\t}\n\n\t# do accounting only for INVITE's\n\tif (is_method(\"INVITE\")) {\n\t\tsetflag(FLT_ACC);\n\t}\n\n\txlog(\"L_INFO\", \"Attempting to route call. ru=$ru du=$du fu=$fu tu=$tu\\n\");\n\tif (!t_relay()) {\n\t\tsl_reply_error();\n\t\troute(RTPENGINEDELETE);\n\t}\n\texit;\n}\n\nroute[CHECK_CALL_LIMIT] {\n#!ifdef WITH_CALL_SETTINGS\n\t# Manage call limits for gwgroups\n\txlog(\"L_DBG\", \"dst_gwtype: $dlg_var(dst_gwtype), dst_gwid: $dlg_var(dst_gwid), dst_gwgroupid: $dlg_var(dst_gwgroupid) src_gwtype: $dlg_var(src_gwtype), src_gwid: $avp(src_gwid), src_gwgroupid: $dlg_var(src_gwgroupid)\\n\");\n\n\t# if src and dst gwgroup is the same there is no need to check both\n\tif ($dlg_var(src_gwgroupid) != $dlg_var(dst_gwgroupid)) {\n\t\tif (!strempty($xavu(src_call_settings=>limit))) {\n\t\t\t$var(num_calls) = $sht(concurrent_calls=>$dlg_var(src_gwgroupid));\n\t\t\tif ($var(num_calls) >= $xavu(src_call_settings=>limit)) {\n\t\t\t\tsl_reply(\"480\", \"Call Limit Exceeded\");\n\t\t\t\t$avp(notification_type) = NOTIFICATION_OVERLIMIT;\n\t\t\t\t$avp(notification_gwid) = $dlg_var(src_gwid);\n\t\t\t\t$avp(notification_gwgroupid) = $dlg_var(src_gwgroupid);\n\t\t\t\troute(SEND_NOTIFICATION);\n\t\t\t\texit;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!strempty($xavu(dst_call_settings=>limit))) {\n\t\t$var(num_calls) = $sht(concurrent_calls=>$dlg_var(dst_gwgroupid));\n\t\tif ($var(num_calls) >= $xavu(dst_call_settings=>limit)) {\n\t\t\tsl_reply(\"480\", \"Call Limit Exceeded\");\n\t\t\t$avp(notification_type) = NOTIFICATION_OVERLIMIT;\n\t\t\t$avp(notification_gwid) = $dlg_var(dst_gwid);\n\t\t\t$avp(notification_gwgroupid) = $dlg_var(dst_gwgroupid);\n\t\t\troute(SEND_NOTIFICATION);\n\t\t\texit;\n\t\t}\n\t}\n#!endif\n\n\treturn;\n}\n\n# Per SIP request initial checks\nroute[REQINIT] {\n\t# reusable flag denoting the source is an allowed address\n\t# checking if the flag is already set is a simple optimization for follow-on messages in the transaction\n\tif (!isflagset(FLT_SRC_ALLOWED)) {\n\t\tif (is_myself(\"$si\")) {\n\t\t\tsetbflag(FLB_SRC_SELF);\n\t\t   \tsetflag(FLT_SRC_ALLOWED);\n\t   \t}\n\t\telse if (allow_source_address_group()) {\n\t\t\tsetflag(FLT_SRC_ALLOWED);\n\t   \t}\n\t}\n\n#!ifdef WITH_ANTIFLOOD\n\t# if not from self then do flood detection on the source IP\n\tif (!isbflagset(FLB_SRC_SELF)) {\n\t\tif ($sht(ipban=>$si) != $null) {\n\t\t\t# refreshing ip ban\n\t\t\tpike_check_req();\n\t\t\txlog(\"L_INFO\", \"pike blocking request with source address $si:$sp\\n\");\n\t\t\texit;\n\t\t}\n\t\tif (!pike_check_req()) {\n\t\t\t# new ip ban\n\t\t\txlog(\"L_ALERT\", \"pike banning requests from source address $si:$sp\\n\");\n\t\t\t$sht(ipban=>$si) = 1;\n\t\t\texit;\n\t\t}\n\t}\n#!endif\n\n\tif (!mf_process_maxfwd_header(\"10\")) {\n\t\tsl_send_reply(\"483\", \"Too Many Hops\");\n\t\texit;\n\t}\n\n\t# Only reply to option messages if the endpoint or the carrier is defined\n\tif (is_method(\"OPTIONS\") && isflagset(FLT_SRC_ALLOWED)) {\n\t\tsl_send_reply(\"200\", \"Keepalive\");\n\t\texit;\n\t}\n\n\tif (!sanity_check(\"1511\", \"7\")) {\n\t\txlog(\"L_WARN\", \"Malformed SIP message from source address $si:$sp\\n\");\n\t\texit;\n\t}\n\n\t# request with no Username in RURI, default to the To username\n\tif ($rU == $null && is_method(\"INVITE\")) {\n\t\t$rU = $tU;\n\t}\n\n\t# set a flag denoting the source address type\n\t# checking if one of the flags is already set is a simple optimization for follow-on messages in the branch\n\tif (!isbflagset(FLB_SRC_SELF) && !isbflagset(FLB_SRC_PBX) && !isbflagset(FLB_SRC_CARRIER) && !isbflagset(FLB_SRC_MSTEAMS)) {\n\t\tif (allow_source_address(FLT_PBX)) {\n\t\t\tsetbflag(FLB_SRC_PBX);\n\t\t}\n\t\telse if (allow_source_address(FLT_CARRIER)) {\n\t\t\tsetbflag(FLB_SRC_CARRIER);\n\t\t}\n\t\telse if (allow_source_address(FLT_MSTEAMS)) {\n\t\t\tsetbflag(FLB_SRC_MSTEAMS);\n\t\t}\n\t\telse if (is_myself(\"$si\")) {\n\t\t\tsetbflag(FLB_SRC_SELF);\n\t   \t}\n\t}\n\n\t# set a flag denoting the type of UAC\n\tif ($pr == \"ws\" || $pr == \"wss\") {\n\t\tsetflag(FLT_SRC_WS);\n\t}\n\telse {\n\t\tsetflag(FLT_SRC_SIP);\n\t}\n}\n\n# Handle requests within SIP dialogs\nroute[WITHINDLG] {\n\t# whether we were in a dialog when starting to process this transaction\n\tif (!has_totag()) {\n\t\tresetflag(FLT_HAS_TOTAG);\n\t\treturn;\n\t}\n\telse {\n\t\tsetflag(FLT_HAS_TOTAG);\n\t}\n\n\t# Logic to handle BLF when using domain routing\n\tif ( is_method(\"SUBSCRIBE\") ) {\n\t\t# Get destination signaling from dialog and replace the transport with that signalling \n\t\t# However, accessing dialog variables seems not to be available when the SIP message \n\t\t# is a subscribe\n\t\t#\n\t\t#Tested with Polycom VVX phones\n\t\tsubst(\"/^Contact:(.*);transport=tcp(.*)/Contact:\\1\\2/i\");\n\t}\n\n\troute(NATMANAGE);\n\n\troute(MANAGE_ONHOLD);\n\n\t# Handling REINVITES from Carriers to MSTeams\n        if (!isbflagset(FLB_SRC_MSTEAMS) && $rd =~ \"pstnhub.microsoft.com\") {\n                setbflag(FLB_DST_MSTEAMS);\n        }\n\n\t# Handling onhold, but could be used for more\n\tif (is_method(\"INVITE\") && $hdr(User-Agent) =~ \"Microsoft.PSTNHub\" && $avp(sdp_media_direction) == \"inactive\") {\n\t\tsetbflag(FLB_SRC_MSTEAMS_ONHOLD);\n\t}\n\n\n\t# sequential request withing a dialog should\n\t# take the path determined by record-routing\n\tif (loose_route_mode(\"1\")) {\n\t\t# SBC rewrite takes priority\n\t\t#route(SBC_TRANSLATE_LR);\n\t\troute(DLGURI);\n\n\t\n\t\tif (is_method(\"BYE\")  && $rd =~ \".invalid\")\n\t\t{\n\t\t\tif (lookup(\"location\",\"sip:$tU@$td\")) {\n\t\t\t\txlog(\"L_DBG\", \"Looking up the domain and getting domain: $fd\\n\");\n\t\t\t}\n\t\t}\n\n\t\tif ($(ru{uri.param,transport}) == \"ws\" || $(ru{uri.param,transport}) == \"wss\") {\n\n\t\t\tif (is_method(\"ACK|BYE|UPDATE|CANCEL\")) {\n\n\t\t\t\t\t\t\t\n\t\t\t\tif (!strempty($(ru{uri.param,domain}))) {\n\t\t\t\t\n\t\t\t\t\t$var(domain) = $(ru{uri.param,domain}); \n\t\t\t\t\t$var(exten) = $(ru{uri.param,exten}); \n\t\t\t\t}\n\t\t\t\telse if (!strempty($(tu{uri.param,domain}))) {\n\t\t\t\t\n\t\t\t\t\t$var(domain) = $(tu{uri.param,domain}); \n\t\t\t\t\t$var(exten) = $(tu{uri.param,exten}); \n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t# Use the To domain if domain request param is empty\n\t\t\t\t\t$var(domain) = $td;\n\t\t\t\t\t$var(exten) = $tU;\n\t\t\t\t}\n\t\t\t\txlog(\"L_INFO\", \"Looking up the exten and domain and getting: $var(exten)@$var(domain)\\n\");\n\t\t\t\tif (lookup(\"location\",\"sip:$var(exten)@$var(domain)\")) {\n\t\t\t\t\txlog(\"L_DBG\", \"Looking up the domain and getting domain: $fd\\n\");\n\t\t\t\t}\n\t\t\t\n\t\t\t}\n\t\t}\n\n \t\t# Fix BYE Messages coming from MSTeams to carriers\n \t\tif (isbflagset(FLB_SRC_MSTEAMS) && allow_address(FLT_CARRIER, \"$(dlg_var(src_uri){uri.host})\", \"$(dlg_var(src_uri){uri.port})\") && is_method(\"BYE\")) {\n                \n\t\t\t$ru = $dlg_var(dst_cturi);\n       \t\t }\n\t\n\t\t# Fix BYE Messages coming from carriers and going to MSTEAMS Domain\n         \tif (is_method(\"BYE\") && isbflagset(FLB_SRC_CARRIER) && !strempty($dlg_var(src_msteams_domain))) {\n\n \t\t\t\n\t\t        $ru = $dlg_var(dst_cturi);\t\n\t\t\t$td = \"sip.pstnhub.microsoft.com\";\n \t\t\t$fd = $dlg_var(src_msteams_domain);\n \t\t\tremove_hf(\"Contact\");\n \t\t\tappend_hf(\"Contact: <sip:$dlg_var(src_msteams_domain):SIPS_PORT;transport=tls>\\r\\n\");\n         \t}\n\n\t\tif (is_method(\"BYE\")) {\n\t\t\t# do accounting even if the transaction fails\n\t\t\tsetflag(FLT_ACC);\n\t\t\tsetflag(FLT_ACCFAILED);\n\n\t\t\troute(RTPENGINEDELETE);\n\t\t}\n\n\t\troute(RELAY);\n\t\texit;\n\t}\n\n#!ifdef WITH_MSTEAMS\n\t# when handling double record route from msteams->dsip->pbx rewrite the destination based on the initial INVITE\n\tif (isbflagset(FLB_SRC_SELF) && allow_address(FLT_MSTEAMS, \"$(dlg_var(dst_uri){uri.host})\", \"$(dlg_var(dst_uri){uri.port})\")) {\n\t\txlog(\"L_INFO\", \"handling msteams double record route, rewriting ru from $ru to $dlg_var(src_uri)\\n\");\n\t\t$ru = $dlg_var(src_uri);\n\t}\n#!endif\n\n\tif (is_method(\"SUBSCRIBE\") && uri == myself) {\n\t\t# in-dialog subscribe requests\n\t\troute(PRESENCE);\n\t\texit;\n\t}\n\n\tif (is_method(\"ACK|UPDATE|INVITE|BYE|PRACK\")) {\n\t\t# SBC rewrite takes priority\n\t\t#route(SBC_TRANSLATE_LR);\n\t\troute(DLGURI);\n\n\t\t# Set Accounting flags for strict-routing transactions\n\t\tif (is_method(\"BYE\")) {\n\t\t\tsetflag(FLT_ACC);\n\t\t\tsetflag(FLT_ACCFAILED);\n\n\t\t\troute(RTPENGINEDELETE);\n\t\t}\n\n\t\t# if the message has a transaction try strict routing\n\t\tif (t_check_trans()) {\n\t\t\troute(RELAY);\n\t\t\texit;\n\t\t}\n\n\t\tsl_send_reply(\"481\", \"Call/Transaction Does Not Exist\");\n\t\texit;\n\t}\n\n\tsl_send_reply(\"604\", \"Does Not Exist Anywhere\");\n\texit;\n}\n\n# Handle on hold\nroute[MANAGE_ONHOLD] {\n\tif (!is_method(\"INVITE\")) {\n\t\treturn;\n\t}\n\n\t# handle sdp media direction for SBC's/proxies that require on reply\n\t# rtpengine by default will use a=sendrecv if valid sdp\n\tif (has_body(\"application/sdp\")) {\n\t\t$avp(sdp_media_direction) = $null;\n\n\t\tif (search_body(\"^a=inactive.*\")) {\n\t\t\t$avp(sdp_media_direction) = \"inactive\";\n\t\t}\n\t\telse if (search_body(\"^a=recvonly.*\")) {\n\t\t\t$avp(sdp_media_direction) = \"recvonly\";\n\t\t}\n\t\telse if (search_body(\"^a=sendonly.*\")) {\n\t\t\t$avp(sdp_media_direction) = \"sendonly\";\n\t\t}\n\t}\n}\n\n# Handle SIP registrations\nroute[REGISTRAR] {\n\tif (!is_method(\"REGISTER\")) {\n\t\treturn;\n\t}\n\n\t# Set the device type if a WS device\n\t# TODO: the type of UAC won't change in the middle of a transaction, marked for review/removal\n\tif (isflagset(FLT_SRC_WS)) {\n\t\tsetbflag(FLB_WS_DEVICE);\n\t}\n\n\t# TODO: why are we setting clientside NAT here if serverside NAT is enabled?\n#!ifdef WITH_SIGNAL_SERVERNAT\n\tsetbflag(FLB_NATB);\n#!endif\n#!ifdef WITH_SIGNAL_SERVERNAT6\n\tsetbflag(FLB_NATB);\n#!endif\n#!ifdef WITH_NAT\n\t# do SIP NAT pinging via OPTIONS messages\n\tsetbflag(FLB_NATSIPPING);\n#!endif\n\n\tif (isflagset(FLT_PBX_AUTH)) {\n\t\t# Handle Register Event - We are now acting as a REGISTRAR.\n\t\tif (!save(\"location\")) {\n\t\t\tsl_reply_error();\n\t\t}\n\n\t\t# TODO: can we move these to in memory changes\n\t\t# Update dr_gateways and dr_gw_lists accordingly\n\t\tif ($sel(contact.expires) == \"0\") {\n\t\t\txlog(\"L_DBG\", \"received an unregister request\\n\");\n\t\t\tif (!strempty($dlg_var(src_gwid))) {\n\t\t\t\txlog(\"L_DBG\", \"removing registration address $var(received_addr) from gateways for gwgroup $dlg_var(src_gwgroupid)\\n\");\n\n\t\t\t\tsql_query(\"kam\", \"DELETE FROM dr_gateways WHERE gwid='$dlg_var(src_gwid)'\");\n\t\t\t\tsql_query(\"kam\", \"UPDATE dr_gw_lists SET gwlist=REGEXP_REPLACE(REGEXP_REPLACE(gwlist, '([,;])?$dlg_var(src_gwid)', ''), '^([,;])', '') WHERE id=$dlg_var(src_gwgroupid)\");\n\n\t\t\t\tjsonrpc_exec('{\"jsonrpc\": \"2.0\", \"method\": \"drouting.reload\", \"id\": 1}');\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\txlog(\"L_DBG\", \"received an register request\\n\");\n\t\t\tif (strempty($dlg_var(src_gwid))) {\n\t\t\t\txlog(\"L_DBG\", \"adding registration address $var(received_addr) to gateways for gwgroup $dlg_var(src_gwgroupid)\\n\");\n\n\t\t\t\tsql_query(\"kam\", \"INSERT INTO dr_gateways(type,address,attrs,description) VALUES ($dlg_var(src_gwtype),'$var(received_addr)',',,,proxy,proxy','name:autoregister,type:$dlg_var(src_gwtype),gwgroup:$dlg_var(src_gwgroupid)');\");\n\t\t\t\tsql_query(\"kam\", \"UPDATE dr_gw_lists SET gwlist=REGEXP_REPLACE(CONCAT(gwlist,',',(SELECT MAX(gwid) FROM dr_gateways)), '^,', '') WHERE id=$dlg_var(src_gwgroupid)\");\n\n\t\t\t\tjsonrpc_exec('{\"jsonrpc\": \"2.0\", \"method\": \"drouting.reload\", \"id\": 1}');\n\t\t\t}\n\t\t}\n\n\t\texit;\n\t}\n\n\tif (isflagset(FLT_DOMAINROUTING) && !isflagset(FLT_EXTERNAL_AUTH)) {\n\t\t# Save the location, but DON'T send a 200 reply back.\n\t\t# Let the upstream PBX authenticate the UAC (aka endpoint)\n\n\t\tif (!save(\"location\", \"0x02\")) {\n\t\t\tsl_reply_error();\n\t\t}\n\n\t\t# Keep the origin request domain\n\t\t$var(rd_orig) = $rd;\n\n\t\t# Route to the endpoints defined in the Endpoint group (using dispatcher)\n\t\tif ($avp(domain_pbx_type) == \"2\") {\n\t\t\t#Forward the registration onto one of the servers in the cluster\n\t\t\tif (!strempty($avp(domain_dispatcher_set_id))) {\n\t\t\t\t$avp(dispatcher_setid) = $avp(domain_dispatcher_set_id);\n\t\t\t\tif (!strempty($avp(domain_dispatcher_reg_alg))) {\n\t\t\t\t\t# Set the registration algoritm\n\t\t\t\t\t$avp(dispatcher_alg) = $avp(domain_dispatcher_reg_alg);\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t# Set the dispatcher algorthim to round robin by default\n\t\t\t\t\t$avp(dispatcher_alg) = DSTALG_ROUND_ROBIN;\n\t\t\t\t}\n\n\t\t\t\tif (!strempty($sht(pass_thru_auth=>$ci))) {\n\t\t\t\t\t$du = $sht(pass_thru_auth=>$ci);\n\t\t\t\t\txlog(\"L_INFO\", \"DOMAINROUTING last du was $du\\n\");\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\troute(DISPATCHER_SELECT);\n\t\t\t\t\txlog(\"L_INFO\", \"DOMAINROUTING Routing to Endpoint Gateway List $avp(dispatcher_setid)\\n\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\t# Grab the value of the avp that contains the domain_pbx_ip.\n\t\t\t# This is where requests for that domain should be routed\n\n\t\t\t                  $var(rd) = $(avp(domain_pbx_ip){s.select,0,:});\n                        $var(portandtransport) = $(avp(domain_pbx_ip){s.select,1,:});\n\n                        if (strempty($var(portandtransport))) {\n                                $var(rp) = \"5060\";\n                        }\n                        else {\n\n                        \t$var(rp) = $(var(portandtransport){s.select,0,;});\n                        \t$var(transport) = $(var(portandtransport){s.select,1,;});\n\n                        }\n\n                        if (strempty($var(transport))) {\n\n\n                                $ru = $rz + \":\" + $var(rd) + \":\" + $var(rp);\n                        }\n                        else {\n                                $ru = $rz + \":\" + $var(rd) + \":\" + $var(rp) + \";\" + $var(transport);\n                        }\n\t\t}\n\t\n\t\t# Rewrite Contact based on the domain being routed to\n\t\t$var(ct_domain) = $fd;\n\t\troute(REPLACE_CONTACT_DOMAIN);\n\n\t\tif (isbflagset(FLB_WS_DEVICE)) {\n\t\t\tadd_path();\n\t\t}\n\t\telse {\n\t\t\t#Add the Path header for SIP UAS know how to route back\n                        if (!strempty($var(transport))) {\n\t\t\t\tadd_path($fU, $var(transport));\n\t\t\t}\n\t\t\telse {\n\t\t\t\tadd_path($fU);\n\n\t\t\t}\n\t\t}\n\n\t\t# Store the pbx ip to domain mapping so that SIP messages from the PBX can be rewritten\n\t\tif (!is_ip($(avp(domain_pbx_ip){s.select,0,:}))) {\n\t\t\tif (dns_query($(avp(domain_pbx_ip){s.select,0,:}), \"xyz\")) {\n\t\t\t\t$var(i) = 0;\n\t\t\t\twhile ($var(i) < $dns(xyz=>count)) {\n\t\t\t\t\t$sht(pass_thru_auth=>$dns(xyz=>addr[$var(i)])) = $var(rd_orig);\n\t\t\t\t\t$var(i) = $var(i) + 1;\n\t\t\t\t }\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\t$sht(pass_thru_auth=>$(avp(domain_pbx_ip){s.select,0,:})) = $var(rd_orig);\n\t\t}\n\n\t\t#We are going to pass this request on to the backend server\n\t\troute(RELAY);\n\t\texit;\n\t}\n\telse if (isflagset(FLT_DOMAINROUTING) && isflagset(FLT_EXTERNAL_AUTH)) {\n\t\tif (!save(\"location\")) {\n\t\t\tsl_reply_error();\n\t\t}\n\t\texit;\n\t}\n\n\n#!ifdef WITH_PUSH\n   if (($hdr(Expires) != \"0\") || !($hdr(Contact) =~ \"expires=0\") && ($sht(push=>join::$tU@td) != $null)) {\n\t\txlog(\"L_INFO\", \"[REGISTER] [PUSH] about to un-suspend transaction rm=$rm ru=$ru tU=$tU td=$td \\n\");\n  \t\troute(JOIN);\n\t}\n#!endif\n\n\t#    #Forward the registration onto one of the servers in the cluster\n\t#    if (!strempty($avp(domain_dispatcher_set_id))) {\n\t#        $avp(dispatcher_setid) = $avp(domain_dispatcher_set_id);\n\t#        if (!strempty($avp(domain_dispatcher_reg_alg))) {\n\t#            #Set the registration algoritm\n\t#            $avp(dispatcher_alg) = $avp(domain_dispatcher_reg_alg);\n\t#        }\n\t#        else {\n\t#            #Set the dispatcher algorthim to round robin by default\n\t#            $avp(dispatcher_alg) = DSTALG_ROUND_ROBIN;\n\t#        }\n\t#\n\t#        route(DISPATCHER_SELECT);\n\t#        route(RELAY);\n\t#    }\n\t#    exit;\n\t#}\n}\n\n# Dispatcher request load balancing (we don't route here)\nroute[DISPATCHER_SELECT] {\n\t# round robin dispatching on dispatcher gateways set\n\tif (!ds_select_dst($avp(dispatcher_setid), $avp(dispatcher_alg))) {\n\t\txlog(\"L_ERR\", \"no destination selected for domain: $fd\\n\");\n\t\tsend_reply(\"404\", \"Destination Not Found\");\n\t\texit;\n\t}\n\n\tif (strempty($xavp(dispatcher_dst=>uri)) && $avp(dispatcher_alg) == DSTALG_PARALLEL_FORKING) {\n\t\txlog(\"L_DBG\", \"sending to multiple servers in parallel\\n\");\n\t}\n\telse if (!strempty($xavp(dispatcher_dst=>uri))) {\n\t\txlog(\"L_DBG\", \"dispatcher selected $xavp(dispatcher_dst=>uri)\\n\");\n\t}\n\n\tt_on_failure(\"DISPATCHER_NEXT\");\n\treturn;\n}\n\nfailure_route[DISPATCHER_NEXT] {\n\t# try next destionations in failure route\n\tif (t_is_canceled()) {\n\t\troute(RTPENGINEDELETE);\n\t\texit;\n\t}\n\tif (t_check_status(\"401|407\")) {\n\t\txlog(\"L_INFO\", \"DOMAINROUTING 401 or 407 and the du was $du)\\n\");\n\t\t$sht(pass_thru_auth=>$ci) = $du;\n\t\treturn;\n\t}\n\t# next DST - only for 500 or local timeout\n\tif (t_check_status(\"4[0-9][2-6,8-9]|5[0-9][0-9]\") or (t_branch_timeout() and !t_branch_replied())) {\n\t\tif (ds_next_dst()) {\n\t\t\txlog(\"L_DBG\", \"dispatcher selected $xavp(dispatcher_dst=>uri)\\n\");\n\t\t\tt_on_failure(\"DISPATCHER_NEXT\");\n\t\t\troute(RELAY);\n\t\t\treturn;\n\t\t}\n\t\telse {\n\t\t\t# Drop the replies if this is a REGISTER\n\t\t\tif (is_method('REGISTER')) {\n\t\t\t\tt_drop_replies();\n\t\t\t\tt_reply(\"401\", \"Unauthorized\");\n\t\t\t}\n\t\t}\n\t}\n}\n\nroute[DISPATCHER_SELECT_LB] {\n\t# dispatch using the selected algorithm for this endpoint group\n\tif (!ds_select_dst($avp(dispatcher_setid), $avp(dispatcher_alg))) {\n\t\txlog(\"L_WARN\", \"no destination selected for dispatcher set $avp(dispatcher_setid)\\n\");\n\n\t\t# check failover forwarding, otherwise reply with an error\n\t\tif (!route(NEXTHOP_FAILOVER)) {\n\t\t\tsend_reply(\"600\", \"No Destination Found\");\n\t\t\texit;\n\t\t}\n\n\t\t# if for some reason the failover forwarding was successful but did not exit then we return to calling routine\n\t\treturn;\n\t}\n\n\t# rewrite domain / port in request URIs\n\t$ru = $rz + \":\" + $rU + \"@\" + $dd + \":\" + $dp;\n\t$tu = $rz + \":\" + $rU + \"@\" + $dd + \":\" + $dp;\n\t# we have to flush the buffer in case other changes are/were made\n\tmsg_apply_changes();\n\n\tt_on_failure(\"DISPATCHER_NEXT_LB\");\n\treturn;\n}\n\nfailure_route[DISPATCHER_NEXT_LB] {\n\t# try next destinations in failure route\n\tif (t_is_canceled()) {\n\t\troute(RTPENGINEDELETE);\n\t\texit;\n\t}\n\t# next DST \n\tif (t_check_status(\"4[0-9][1-6,7-9]|5[0-9][0-9]\") || (t_branch_timeout() && !t_branch_replied())) {\n\t\tif (ds_next_dst()) {\n\t\t \txlog(\"L_DBG\", \"trying next destination\\n\");\n\n\t\t\t# rewrite domain / port in request URIs\n\t\t\t$ru = $rz + \":\" + $rU + \"@\" + $dd + \":\" + $dp;\n\t\t\t$tu = $rz + \":\" + $rU + \"@\" + $dd + \":\" + $dp;\n\n\t\t\tt_on_failure(\"DISPATCHER_NEXT_LB\");\n\t\t\troute(RELAY);\n\t\t}\n\t\telse {\n\t\t\txlog(\"L_INFO\", \"all destinations unavailable for dispatcher set $avp(dispatcher_setid)\\n\");\n\n\t\t\t# check failover forwarding, otherwise reply with an error\n\t\t\tif (!route(NEXTHOP_FAILOVER)) {\n\t\t\t\tsend_reply(\"600\", \"No Destination Available\");\n\t\t\t\troute(RTPENGINEDELETE);\n\t\t\t\texit;\n\t\t\t}\n\n\t\t\t# if for some reason the failover forwarding was successful but did not exit then we return to calling routine\n\t\t\treturn;\n\t\t}\n\t}\n\texit;\n}\n\n# User location service\nroute[LOCATION] {\n\n\t# Return immediately if the source address is not a PBX.  Only PBX's should be trying to route to endpoints\n\tif (!isbflagset(FLB_SRC_PBX)) {\n\t\treturn;\n\t}\n\n\t# Emergency / N11 services should return immediately so that it can be routed to a carrier\n\t# ITU officially recognizes 911 (NA) and 112 (EU) as the international emergency numbers\n\t# However 999 (UK) and 000 (AU) are still commonly used\n\t# Emergency Numbers Overview: https://en.wikipedia.org/wiki/Emergency_telephone_number\n\t# N11 Ref: https://nationalnanpa.com/number_resource_info/n11_codes.html\n\t# On 2020-Jul-16 the FCC also adopted 988 as an N11 number\n\t# 988 Adoption Refs: https://www.fcc.gov/document/fcc-designates-988-national-suicide-prevention-lifeline\n\tif ($rU =~ $sel(cfg_get.server.emergency_numbers)) {\n\t\treturn;\n\t}\n\t\n\t# Set the extension and request domain for WebSocket request because some\n\t# WebSocket clients don't send the extension and domain during registeration\n\tif (!strempty($(ru{uri.param,domain}))) { \n\n\t\t$rU = $(ru{uri.param,exten});\n\t\t$rd = $(ru{uri.param,domain});\n\t} \n\n\t# Return if the rU is more then local calling maximum digits for the initiating PBX\n\tif ($(rU{s.len}) > $sel(cfg_get.server.pbx_max_local_digits)) {\n\t\treturn;\n\t}\n\n\t# If request is coming from a FreePBX or Asterisk server use the Pass-Thru htable\n\tif ($hdr(User-Agent) =~ \"FPBX.*|Asterisk.*\") {\n\t\tif ($sht(pass_thru_auth=>$si) != \"\") {\n\t\t\t$rd = $sht(pass_thru_auth=>$si);\n\t\t}\n\t}\n\n\t# Logic to to deal with a broken PATH implmentation in Asterisk PJSIP\n\tif (!strempty($(ru{uri.param,x-ast-orig-host}))) { \n\t\t$var(asterisk_domain) = $(ru{uri.param,x-ast-orig-host});\n\t\t$var(asterisk_domain) = $(var(asterisk_domain){re.subst,/^(.*):(.*)/\\1/});\n\t\n\t\tif ($var(asterisk_domain) != \"\") {\n\t\t\t$rd = $var(asterisk_domain);\n\t\t\tif (!msg_apply_changes()) {\n\t\t\t\txlog(\"L_ERR\", \"failed applying changes to message\\n\");\n\t\t\t}\n\t\t\txlog(\"L_INFO\", \"routing message for domain $var(asterisk_domain) to $rU@$rd\\n\");\n\t\t}\n\t\txlog(\"L_INFO\", \"routing message for domain $var(asterisk_domain) to $rU@$rd\\n\");\n\t}\n\n\t$avp(oexten) = $rU;\n\t# Lookup the location of the endpoint by username@request_domain\n\tif (!lookup(\"location\",\"sip:$rU@$rd\")) {\n\n#!ifdef WITH_PUSH\n\t\txlog(\"L_INFO\", \"  In the route[LOCATION] [PUSH] logic.\");\n\t\tsend_reply(\"100\", \"Suspending\");\n\t\troute(SENDPUSH);\n\t\troute(SUSPEND);\n#!endif\n\n\t\t# Lookup the location of the endpoint by username@from_domain\n\t\tif (!lookup(\"location\",\"sip:$rU@$fd\")) {\n\t\t\t# Check if coming from a Zoiper Push Server\n\t\t\t# If so, the username for the extension is part of the Route header, grab it\n\t\t\t$var(Route) = @hf_value.route.uri;\n\t\t\t$var(user) = $(var(Route){uri.user});\n\t\t\txlog(\"L_DBG\", \"$var(Route) / sip:$var(user)@$fd\\n\");\n\t\t\tif (!lookup(\"location\", \"sip:$var(user)@$fd\")) {\n\t\t\t\t$var(rc) = $rc;\n\t\t\t\troute(TOVOICEMAIL);\n\t\t\t\tt_newtran();\n\t\t\t\tswitch ($var(rc)) {\n\t\t\t\t\tcase -1:\n\t\t\t\t\tcase -3:\n\t\t\t\t\t\tsend_reply(\"404\", \"Not Found\");\n\t\t\t\t\t\texit;\n\t\t\t\t\tcase -2:\n\t\t\t\t\t\tsend_reply(\"405\", \"Method Not Allowed\");\n\t\t\t\t\t\texit;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\txlog(\"L_INFO\", \"ru: $ru, nh(u): $nh(u), WS:$var(WS_DEVICE)\\n\");\n\t\t}\n\t}\n\n\t$var(transport) = $(ru{uri.param,transport});\n\txlog(\"L_INFO\", \"The transport is $var(transport)\");\t\n\t# Set the Signalling and Media\n\tif ($(ru{uri.param,transport}) != $null) { \n\t\t$var(transport) = $(ru{uri.param,transport});\n\t\txlog(\"L_INFO\", \"The transport is $var(transport)\");\t\n\t\tswitch ($(var(transport){s.tolower}))  {\n\n\t\t\tcase \"wss\":\n\t\t\t\t$dlg_var(dst_signalling) = \"sips_wss\";\n\t\t\t\t$dlg_var(dst_media) = \"rtp_savp\";\n\t\t\t\tsetflag(FLB_WS_DEVICE);\n\t\t\t\tbreak;\n\t\t\tcase \"ws\":\n\t\t\t\t$dlg_var(dst_signalling) = \"sip_ws\";\n\t\t\t\t$dlg_var(dst_media) = \"rtp_savp\";\n\t\t\t\tsetflag(FLB_WS_DEVICE);\n\t\t\t\tbreak;\n\t\t\tcase \"udp\":\n\t\t\t\t$dlg_var(dst_media) = \"rtp_avp\";\n\t\t\t\t$dlg_var(dst_signalling) = \"udp\";\t\t\t\n\t\t\t\tbreak;\t\t\n\t\t\tdefault:\n\t\t\t\t$dlg_var(dst_media) = \"proxy\";\n\t\t\t\t$dlg_var(dst_signalling) = \"proxy\";\t\t\t\n\t\t\t\tbreak;\n\t\t\t\t\n\t\t}\n\t\t\n\t\t\n\t}\n\n\t# when routing via usrloc, log the missed calls also\n\tif (is_method(\"INVITE\")) {\n\t\tsetflag(FLT_ACCMISSED);\n\t}\n\t#Set the INVITE timeout for sending calls to invites\n\tt_set_fr(120000,10000);\n\n\troute(SET_CALLDST_INFO);\n\troute(RELAY);\n\texit;\n}\n\n#!ifdef WITH_PUSH\n# Suspend Transaction\nroute[SUSPEND] {\n\txlog(\"L_INFO\", \"suspending transaction\\n\");\n\tt_set_fr(30000);\n\n\tif (!t_suspend()) {\n\t\txlog(\"L_ERR\", \"failed suspending trasaction [$T(id_index):$T(id_label)]\\n\");\n\t\tsend_reply(\"501\", \"Unknown destination\");\n\t\texit;\n\t}\n\n\txlog(\"L_INFO\", \"suspended transaction [$T(id_index):$T(id_label)] $fU => $rU@$rd\\n\");\n\t$sht(push=>join::$rU@$rd) = \"\" + $T(id_index) + \":\" + $T(id_label);\n\txlog(\"L_INFO\", \"suspended htable key value [$sht(push=>join::$rU@$rd)]\\n\");\n\texit;\n}\n\n# Logic to invoke push\nroute[SENDPUSH] {\n\txlog(\"L_INFO\", \"sending the push notification\\n\");\n\t#rabbitmq_publish(\"kamailio\", \"routing_key\", \"application/json\", \"$avp(json_request)\");\n\n\t#$var(luaret) = 0;\n\t#if(lua_runstring(\"do_push([[$hdr(X-VxTo)]], [[$tU]], [[$hdr(X-VxFrom)]], [[$fU]], [[$ci]])\")<0){\n\t#\tsend_reply(\"501\", \"No link to destination\");\n\t#\texit;\n\t#}\n\treturn;\n}\n\n# Suspend\nroute[JOIN] {\n \txlog(\"L_INFO\", \"  In the [PUSH] route[JOIN] logic.\");\n\t$var(index)=(int) $(sht(push=>join::$tU@$td){s.select,0,:});\n\t$var(label)=(int) $(sht(push=>join::$tU@$td){s.select,1,:});\n\txlog(\"L_INFO\", \"[JOIN] [PUSH] suspend $var(index) $var(label)\");\n\tt_set_fr(30000);\n\tt_continue(\"$var(index)\", \"$var(label)\", \"RESUME\");\n}\n\n# Resume\nroute[RESUME] {\n\txlog(\"L_INFO\", \"resuming transaction\");\n\n\txlog(\"L_INFO\", \"values before lookup: rm=$rm ru=$rU rd=$rd du=$du \\n\");\n\tif (!lookup(\"location\",\"sip:$rU@$rd\")) {\n\t\tswitch ($retcode) {\n\t\t\tcase 1:\n\t\t\t\txlog(\"L_INFO\", \"values after lookup rm=$rm ru=$rU rd=$rd du=$du \\n\");\n\t\t\tcase -1:\n\t\t\tcase -3:\n\t\t\t\tsl_send_reply(\"404\", \"Not Found\");\n\t\t\t\texit;\n\t\t\t\tbreak;\n\t\t\tcase -2:\n\t\t\t\tsl_send_reply(\"405\", \"Not Found\");\n\t\t\t\texit;\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\txlog(\"L_INFO\",\"[RESUME] [PUSH] suspend rm=$rm ru=$rU rd=$rd du=$du \\n\");\n\trecord_route();\n\tt_relay();\n\texit;\n}\n#!endif\n\n\n# Presence server processing\nroute[PRESENCE] {\n\tif (!is_method(\"PUBLISH|SUBSCRIBE\")) {\n\t\treturn;\n\t}\n\n\tif (is_method(\"SUBSCRIBE\") && $hdr(Event)==\"message-summary\") {\n\t\troute(TOVOICEMAIL);\n\t\t# returns here if no voicemail server is configured\n\t\tsl_send_reply(\"404\", \"No voicemail service\");\n\t\texit;\n\t}\n\n#!ifdef WITH_PRESENCE\n\tif (isflagset(FLT_DOMAINROUTING) && !isflagset(FLT_EXTERNAL_AUTH)) {\n\t\t# Rewrite Contact based on the domain being routed to\n\t\t$var(ct_domain) = $fd;\n\t\troute(REPLACE_CONTACT_DOMAIN);\n    }\n\tif (!t_newtran()) {\n\t\tsl_reply_error();\n\t\texit;\n\t}\n\n\tif (is_method(\"PUBLISH\")) {\n\t\thandle_publish();\n\t\tt_release();\n\t}\n\telse if (is_method(\"SUBSCRIBE\")) {\n\t\thandle_subscribe();\n\t\tt_release();\n\t}\n\texit;\n#!endif\n\n\t# if presence enabled, this part will not be executed\n\tif (is_method(\"PUBLISH\") || $rU == $null) {\n\t\tsl_send_reply(\"404\", \"Not here\");\n\t\texit;\n\t}\n\treturn;\n}\n\n# IP authorization and user authentication\nroute[AUTH] {\n#!ifdef WITH_AUTH\n\tif (is_myself(\"$si\")) {\n\t\treturn;\n\t}\n\n\t# AUTH route logic summary:\n\t# 1) attempt domain auth\n\t# 2) Check if request is coming from a carrier that's using username/password auth (remote or local)\n\t# 3) attempt IP auth\n\t# 4) attempt username/password auth against local subscriber database\n\n\t#=================\n\t# Domain AUTH\n\t#=================\n\t# Check if this is any type of SIP request from a known domain only if the role of the server is not \"inout\".\n\t# The role of \"inout\" means that the role of this Kamailio instance is to just route calls inbound and outbound\n\t# using only IP auth or username/password auth\n\n\tif (lookup_domain(\"$fd\", \"domain_\") && ($sel(cfg_get.server.role) != 'inout')) {\n\t\t# Turn on domain routing by setting the FLT_DOMAINROUTING flag\n\t\tsetflag(FLT_DOMAINROUTING);\n\n\t\t# If the domain is mapped to single PBX then route to the PBX IP for authentication\n\t\tif ($avp(domain_domain_auth) == \"passthru\") {\n\t\t\tsetflag(FLT_PASSTHRU_AUTH);\n\t\t\txlog(\"L_INFO\", \"DOMAIN_AUTH $tU@$fd will be routed to $avp(domain_pbx_ip)\\n\");\n\t\t\treturn;\n\t\t}\n\t\t#Check if the domain is configured to route to a cluster of PBX's by checking if the dispatcher set_id is set\n\t\t#If so, we need to auth the user against an external database or local subscriber database\n\t\t#This will allow INVITE requests to be sent to any backend PBX's because we have validated the user\n\t\t#Hence, the backend PBX's should be setup only to trust SIP connections from dSIPRouter instances\n\t\telse if (!strempty($avp(domain_dispatcher_set_id))) {\n\t\t\t$avp(dispatcher_setid) = $avp(domain_dispatcher_set_id);\n\t\t\tif (is_method(\"REGISTER|INVITE\") || from_uri==myself) {\n\t\t\t\t# Each domain has a auth type thats external to the backend destination server\n\t\t\t\t# 1 = Kamailo Subscriber table\n\t\t\t\t# 2 = Asterisk DB\n\n\t\t\t\tsetflag(FLT_EXTERNAL_AUTH);\n\n\t\t\t\txlog(\"L_INFO\", \"Generic Domain Routing for $tU@$fd - the defined auth type for $fd is $avp(domain_domain_auth)\\n\");\n\n\t\t\t\tif ($avp(domain_domain_auth) == \"realtime\") {\n\t\t\t\t\txlog(\"L_INFO\", \"DOMAIN_AUTH Asterisk Realtime auth is being used\\n\");\n\t\t\t\t\t# Load data needed for custom SIP headers\n\t\t\t\t\tif ($avp(domain_enrich_headers) == 1) {\n\t\t\t\t\t\t$var(query) = \"select sippasswd,sipdomain from sipusers where name=$fU\";\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\t$var(query) = \"select sippasswd from sipusers where name=$fU\";\n\t\t\t\t\t}\n\n\t\t\t\t\t#Let's auth against the database defined by the domain attributes\n\t\t\t\t\tsql_xquery(\"asterisk\",\"$var(query)\",\"ra\");\n\t\t\t\t\t$var(sippasswd) = $xavp(ra=>sippasswd);\n\t\t\t\t\tsql_result_free(\"ra\");\n\t\t\t\t\txlog(\"L_DBG\", \"DOMAIN_AUTH The password for user $fU@$fd is $var(sippasswd)\\n\");\n\n\t\t\t\t\tif (!pv_auth_check(\"$fd\", \"$xavp(ra=>sippasswd)\", \"2\",\"0\")) {\n\t\t\t\t\t\tauth_challenge(\"$fd\", \"0\");\n\t\t\t\t\t\texit;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if ($avp(domain_domain_auth) == \"local\") {\n\t\t\t\t\txlog(\"L_INFO\", \"DOMAIN_AUTH Local auth is being used\\n\");\n\t\t\t\t\tif (!auth_check(\"$fd\", \"subscriber\", \"3\")) {\n\t\t\t\t\t   auth_challenge(\"$fd\", \"0\");\n\t\t\t\t\t   exit;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t# TODO: return error if domain_dispatcher_set_id is set and domain_domain_auth not realtime or local?\n\t\t\t}\n\t\t\t# user authenticated - remove auth header\n\t\t\tif (!is_method(\"REGISTER|PUBLISH\")) {\n\t\t\t\txlog(\"L_INFO\", \"DOMAIN_AUTH $tU@$fd was authenticated\\n\");\n\t\t\t\tconsume_credentials();\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\t}\n\n\t#=================\n\t# Digest AUTH\n\t#=================\n\tif (is_subscriber(\"$fu\", \"subscriber\", \"3\")) {\n\t\t# authenticate requests\n\t\tif (!auth_check(\"$fd\", \"subscriber\", \"3\")) {\n\t\t\tauth_challenge(\"$fd\", \"0\");\n\t\t\texit;\n\t\t}\n\t\t# user authenticated - remove Authorization header\n\t\tconsume_credentials();\n\n\t\t# set flags denoting what auth/src we have\n\t\tsetflag(FLT_PBX_AUTH);\n\t\tsetbflag(FLB_SRC_PBX);\n\t\treturn;\n\t}\n\n\t#=================\n\t# IP AUTH\n\t#=================\n#!ifdef WITH_IPAUTH\n\t# If domain not known, then check IP AUTH to see if the user if allowed to connect\n\t# Changed from allow_source_address to allow_source_addess_group because it will allow any addresses within any address group.\n\t# This means that both carriers and pbx's will be allowed to access the proxy with one function call\n\t# TODO: we already check source address in REQINIT, why are we checking again here?\n\tif (isflagset(FLT_SRC_ALLOWED)) {\n\t\t# source IP allowed\n\t\treturn;\n\t}\n#!endif\n\n\tif (is_method(\"REGISTER|INVITE\") || from_uri==myself) {\n\t\t# authenticate requests\n\t\tif (!auth_check(\"$fd\", \"subscriber\", \"3\")) {\n\t\t\tauth_challenge(\"$fd\", \"0\");\n\t\t\texit;\n\t\t}\n\t\t# user authenticated - remove auth header\n\t\tif (!is_method(\"REGISTER|PUBLISH\")) {\n\t\t\tconsume_credentials();\n\t\t}\n\t\t# Set a flag denoting that a PBX has authenticated with username/password\n\t\tsetflag(FLT_PBX_AUTH);\n\t}\n#!endif\n\n\treturn;\n}\n\nroute[SET_CALLSRC_INFO] {\n\t# source request info for manipulating what we send to UAC later in the transaction/dialog\n\t$dlg_var(src_oruri) = $ru;\n\t$dlg_var(src_ofuri) = $fu;\n\t$dlg_var(src_oturi) = $tu;\n\t$dlg_var(src_duri) = $sut;\n\n\t$var(received_addr) = $(su{re.subst,/^(sip:|sips:)?(.*)$/\\2/});\n\n\tif (isbflagset(FLB_SRC_MSTEAMS)) {\n\t\t$dlg_var(src_msteams_domain) = $td;\n\t}\n\n\t# Set call info for tracking call limits for username/pass auth\n\tif (isflagset(FLT_PBX_AUTH)) {\n\t\tif (sql_xquery(\"kam\", \"select rpid as gwgroupid from subscriber where username='$au'\", \"rows\") == 1) {\n\t\t\t$dlg_var(src_gwtype) = (str)FLT_PBX;\n\t\t\t$dlg_var(src_gwgroupid) = $xavp(rows=>gwgroupid);\n\t\t\tif (sql_xquery(\"kam\", \"select gwid from dr_gateways where address='$var(received_addr)' AND description REGEXP 'gwgroup:$dlg_var(src_gwgroupid)(,|$$)'\", \"rows\") == 1) {\n\t\t\t\t$dlg_var(src_gwid) = $xavp(rows=>gwid);\n\t\t\t}\n\t\t}\n\t}\n\t# TODO: these assumptions here are broken throughout the codebase, marked for review in v0.80\n\telse if (isflagset(FLT_DOMAINROUTING)) {\n\t\t$dlg_var(src_gwtype) = $avp(domain_pbx_type);\n\t\t$dlg_var(src_gwgroupid) = $avp(domain_pbx_list);\n\t}\n\t# Set call info for tracking call limits for ip auth\n\telse {\n\t\tif (sql_xquery(\"kam\",\"select type, gwid from dr_gateways where address like '$si%'\", \"rows\") == 1) {\n\t\t\t$dlg_var(src_gwtype) = $xavp(rows=>type);\n\t\t\t$dlg_var(src_gwid) = $xavp(rows=>gwid);\n\t\t\t$dlg_var(src_gwgroupid) = $sht(gw2gwgroup=>$dlg_var(src_gwid));\n\t\t}\n\t}\n\n#!ifdef WITH_CALL_SETTINGS\n\t$vn(call_settings) = $sht(call_settings=>$dlg_var(src_gwgroupid));\n\tif ($vn(call_settings) != $null) {\n\t\t$xavu(src_call_settings=>limit) = $(vn(call_settings){s.select,0,,});\n\t\t$xavu(src_call_settings=>timeout) = $(vn(call_settings){s.select,1,,});\n\t}\n\telse {\n\t\t$xavu(src_call_settings=>limit) = \"\";\n\t\t$xavu(src_call_settings=>timeout) = \"\";\n\t}\n#!endif\n\n\t# set call forwarding info to null by default\n\t$avp(hardfwdinfo) = $null;\n\t$avp(failfwdinfo) = $null;\n\n\treturn;\n}\n\n# once a destination is selected route here to get more detailed info about the gwgroup\nroute[SET_CALLDST_INFO] {\n\tif ($avp(dr_attrs) != $null) {\n\t\t$var(dst_gwid) = $(avp(dr_attrs){s.select,0,,});\n\t\t$var(dst_gwtype) = $(avp(dr_attrs){s.select,1,,});\n\t\t$var(dst_msteams_domain) = $(avp(dr_attrs){s.select,2,,});\n\t \t$var(dst_signalling) = $(avp(dr_attrs){s.select,3,,});\n\t \t$var(dst_media) = $(avp(dr_attrs){s.select,4,,});\n\t\t$var(dst_gwgroupid) = $sht(gw2gwgroup=>$var(dst_gwid));\n\t\t# by convention the setid is the same as the gwgroupid\n\t\tif (!strempty($var(dst_gwgroupid))) {\n\t\t\t$avp(dispatcher_setid) = $var(dst_gwgroupid);\n\t\t}\n\t\telse {\n\t\t\t$avp(dispatcher_setid) = $null;\n\t\t}\n\n\t\t# special use case: Set the media to rtp_avp if coming from MSTeams and the dst media is set to proxy\n\t\tif (!strempty($var(src_msteams_domain)) && $var(dst_media) == \"proxy\") {\n\t\t\t$var(dst_media) = \"rtp_avp\";\n\t\t}\n\t}\n\telse if (isflagset(FLT_PASSTHRU_AUTH)) {\n\t\t# passthru auth only maps to a single pbx\n\t\t$var(dst_gwid) = $(avp(domain_pbx_list){s.select,0,,});\n\t\tsql_pvquery(\n\t\t\t\"kam\",\n\t\t\t\"SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(attrs, ',', 2), ',', -1), SUBSTRING_INDEX(SUBSTRING_INDEX(attrs, ',', 3), ',', -1), SUBSTRING_INDEX(SUBSTRING_INDEX(attrs, ',', 4), ',', -1), SUBSTRING_INDEX(SUBSTRING_INDEX(attrs, ',', 5), ',', -1), REGEXP_REPLACE(description, '.*gwgroup:([0-9]+).*', '\\\\1') FROM dr_gateways WHERE gwid='$var(dst_gwid)'\",\n\t\t\t\"$var(dst_gwtype), $var(dst_msteams_domain), $var(dst_signalling), $var(dst_media), $var(dst_gwgroupid)\"\n\t\t);\n\t\t\n\t\t# Check if Source is WebSocket and $var(dst_media) = proxy.  If so, set $var(dst_media) = rtp_avp as the default\n\t\tif (isflagset(FLT_SRC_WS) && $var(dst_media) == \"proxy\")  {\n\t\t\t$var(dst_media) = \"rtp_avp\";\n\t\t}\n\t\n\t}\n\telse {\n\t\txlog(\"L_ERR\", \"can not lookup destination info\\n\");\n\t\treturn;\n\t}\n\n\t$vn(gwgroup2lb) = $sht(gwgroup2lb=>$var(dst_gwgroupid));\n\tif ($vn(gwgroup2lb) != $null) {\n\t\t$avp(lb_enabled) = (int)$(vn(gwgroup2lb){s.select,1,,});\n\t}\n\telse {\n\t\t$avp(lb_enabled) = 0;\n\t}\n\n#!ifdef WITH_CALL_SETTINGS\n\t$vn(call_settings) = $sht(call_settings=>$var(dst_gwgroupid));\n\tif ($vn(call_settings) != $null) {\n\t\t$xavu(dst_call_settings=>limit) = $(vn(call_settings){s.select,0,,});\n\t\t$xavu(dst_call_settings=>timeout) = $(vn(call_settings){s.select,1,,});\n\t}\n\telse {\n\t\t$xavu(dst_call_settings=>limit) = \"\";\n\t\t$xavu(dst_call_settings=>timeout) = \"\";\n\t}\n#!endif\n\n\t# dialog lookups are computationally expensive, therefore we set them last after checks above\n\t$dlg_var(dst_gwid) = $var(dst_gwid);\n\t$dlg_var(dst_gwtype) = $var(dst_gwtype);\n\t$dlg_var(dst_msteams_domain) = $var(dst_msteams_domain);\n\t$dlg_var(dst_signalling) = $var(dst_signalling);\n\t$dlg_var(dst_media) = $var(dst_media);\n\t$dlg_var(dst_gwgroupid) = $var(dst_gwgroupid);\n}\n\n# store the selected du/ru and reformatted contact\nroute[SET_CALLROUTE_INFO] {\n\t$dlg_var(dst_ruri) = $ru;\n\t$dlg_var(dst_duri) = $du;\n\t\n\tif (!has_totag() && is_method(\"INVITE\")) {\n\t\tif ($ct =~ \"<.*>\") {\n\t\t\t\n\t\t\t$dlg_var(dst_cturi) = $(ct{re.subst,/^<(.*)>/\\1/});\n\t\t}\n\t\telse {\n\t\t\t\n\t\t\t$dlg_var(dst_cturi) = $ct;\n\t\t}\t\n\t}\n\n}\n\n# TODO: we should avoid recreating the drouting algo here and instead check hardfwd and failfwd dr_group by default\n# drouting would be called 3 times by default in this case, satisfying the following logic:\n# 1. check hardfwd dr_group for prefix matches\n# 2. check default dr_group for prefix matches (pbx or carrier)\n# 3. check failfwd dr_group for prefix matches\n# this would avoid prefix match prediction as we do below and support time criteria by default\nroute[SET_CALLFWD_INFO] {\n\t# we need to know what prefix drouting will match before it runs\n\t# this only checks against inbound rules, this shouldn't be used for outbound\n\t# TODO: this algorithm ignores time criteria of the dr_rule\n\t# which could lead to false positives if using this setting in drouting\n\t$avp(dr_ruleid) = $null;\t\t\t\t# dr_ruleid matched\n\t$var(prefix_match_diff) = 1000;\t\t\t# last match prefix diff\n\t$var(diff) = 1000;\t\t\t\t\t\t# current entry prefix diff\n\t$var(prefix_match_priority) = 0;\t\t# last match priority\n\t$var(priority) = 0;\t\t\t\t\t\t# current entry priority\n\t$var(lookup) = $(rU{s.unescape.user});\t# dnid to match against\n\n\tsht_iterator_start(\"iter\", \"prefix_to_route\");\n\twhile(sht_iterator_next(\"iter\")) {\n\t\t# TODO: for now we use a literal prefix but we should change to supporting patterns\n\t\t# this would require updating drouting module to support pattern matching\n\t\t$var(regex) = $(shtitkey(iter){re.subst,/(\\+|\\*|\\#)/\\\\\\1/g}) + \"([0-9])*\";\n\t\tif ($var(lookup) =~ $var(regex)) {\n\t\t\txlog(\"L_DBG\", \"prefix match on $shtitkey(iter)\\n\");\n\t\t\t# if priority is less than last match we don't update match\n\t\t\t$var(priority) = $(shtitval(iter){s.select,1,,}{s.int});\n\t\t\tif ($var(priority) >= $var(prefix_match_priority)) {\n\t\t\t\t# get the difference between prefix and match\n\t\t\t\t$var(diff) = $(var(lookup){s.len}) - $(shtitkey(iter){s.len});\n\t\t\t\t# bitwise absolute value: ( mask = n>>31; (mask^n) - mask )\n\t\t\t\t# we are assuming 32 bit integers\n\t\t\t\t$var(mask) = $var(diff) >> 31;\n\t\t\t\t$var(diff) = ($var(mask) ^ $var(diff)) - $var(mask);\n\t\t\t\t# if priority is greater than last match or priority is equal and\n\t\t\t\t# diff is less than last match (closer match) we update match\n\t\t\t\tif ($var(priority) > $var(prefix_match_priority) || $var(diff) < $var(prefix_match_diff)) {\n\t\t\t\t\txlog(\"L_DBG\", \"prefix closer match priority=$var(priority) diff=$var(diff)\\n\");\n\t\t\t\t\t$avp(dr_ruleid) = $(shtitval(iter){s.select,0,,});\n\t\t\t\t\t$var(prefix_match_priority) = $var(priority);\n\t\t\t\t\t$var(prefix_match_diff) = $var(diff);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tsht_iterator_end(\"iter\");\n\n\tif ($avp(dr_ruleid) != $null) {\n\t\t$avp(hardfwdinfo) = $sht(inbound_hardfwd=>$avp(dr_ruleid));\n\t\t$avp(failfwdinfo) = $sht(inbound_failfwd=>$avp(dr_ruleid));\n\t}\n\n\treturn;\n}\n\nroute[CHECK_DLG_ALLOWED] {\n\t# only INVITE/SUBSCRIBE/REFER are allowed to create dialogs\n\t# ref: RFC 3261 / RFC 3515\n\tif (!is_method(\"INVITE|SUBSCRIBE|REFER\")) {\n\t\treturn;\n\t}\n\n\tsetflag(FLT_DLG_ALLOWED);\n}\n\n# Sets the following variables:\n# $avp(auth_user)\n# $avp(auth_pass)\n# $avp(auth_realm)\n# $dlg_var(src_auth_domain)\n# $dlg_var(src_auth_user)\n# $dlg_var(src_auth_pass)\n# $dlg_var(src_auth_realm)\n# $dlg_var(dst_auth_domain)\n# $dlg_var(dst_auth_user)\n# $dlg_var(dst_auth_pass)\n# $dlg_var(dst_auth_realm)\nroute[SETUP_CALLAUTH_INFO] {\n\t$vn(saved_ru) = $ru;\n\t$vn(saved_du) = $du;\n\tif (uac_reg_request_to($dlg_var(src_gwgroupid), 0)) {\n\t\t$dlg_var(src_auth_domain) = $rd;\n\t\t$dlg_var(src_auth_user) = $avp(auth_user);\n\t\t$dlg_var(src_auth_pass) = $avp(auth_pass);\n\t\t$dlg_var(src_auth_realm) = $avp(auth_realm);\n\t\t$avp(auth_user) = $null;\n\t\t$avp(auth_pass) = $null;\n\t\t$avp(auth_realm) = $null;\n\t}\n\tif (uac_reg_request_to($dlg_var(dst_gwgroupid), 0)) {\n\t\t$dlg_var(dst_auth_domain) = $rd;\n\t\t$dlg_var(dst_auth_user) = $avp(auth_user);\n\t\t$dlg_var(dst_auth_pass) = $avp(auth_pass);\n\t\t$dlg_var(dst_auth_realm) = $avp(auth_realm);\n\t}\n\t$ru = $vn(saved_ru);\n\t$du = $vn(saved_du);\n}\n\n# Sets the following variables:\n# $dlg_ctx(timeout)\n# $dlg_ctx(timeout_route)\nroute[SETUP_DIALOG] {\n\tif (!isflagset(FLT_DLG_ALLOWED)) {\n\t\treturn;\n\t}\n\n#!ifdef WITH_CALL_SETTINGS\n\t# set the dialog timeout to the smallest timeout between the src and dst gwgroups\n\tif (!strempty($xavu(dst_call_settings=>timeout))) {\n\t\t$vn(call_timeout) = (int)$xavu(dst_call_settings=>timeout);\n\t\tif (!strempty($xavu(src_call_settings=>timeout)) && $vn(call_timeout) > $xavu(src_call_settings=>timeout)) {\n\t\t\t$vn(call_timeout) = (int)$xavu(src_call_settings=>timeout);\n\t\t}\n\t}\n\telse if (!strempty($xavu(src_call_settings=>timeout))) {\n\t\t$vn(call_timeout) = (int)$xavu(src_call_settings=>timeout);\n\t}\n\tif ($vn(call_timeout) != $null) {\n\t\t$dlg_ctx(timeout) = $vn(call_timeout);\n\t\t$dlg_ctx(timeout_route) = \"DIALOG_TIMEOUT\";\n\t}\n#!endif\n\t# Create dialog if one doesn't already exists\n \tif (!is_known_dlg()) {\n\t\tdlg_manage();\n        }\n}\n\n\n# TODO: ipv6 support\nroute[SET_RECORD_ROUTE] {\n\t# remove any previous headers set\n\tremove_record_route();\n\n\t# TODO: someone give good explanation of the NLB record routing here\n\tif ((\"OUTBOUND_NLB_FQDN\" != \"\") && (\"INBOUND_NLB_FQDN\" != \"\")) {\n\t\trecord_route_preset(\"OUTBOUND_NLB_FQDN\",\"INBOUND_NLB_FQDN\");\n\t\t$dlg_var(sbc_translate) = \"0\";\n\t}\n\telse if ((\"OUTBOUND_NLB_FQDN\" == \"\") && (\"INBOUND_NLB_FQDN\" != \"\")) {\n\t\trecord_route_advertised_address(\"INBOUND_NLB_FQDN\");\n\t\t$dlg_var(sbc_translate) = \"0\";\n\t}\n#!ifdef WITH_MSTEAMS\n\t# TODO: needs check everytime this is called, can't store/check flag\n\t# MS Teams special use case - add hop from SIPS port to SIP port\n\telse if (isbflagset(FLB_DST_MSTEAMS)) {\n\t\t# TODO: if end-to-end encryption is used this may not work\n\t\t# TODO: we should be dynamically checking the transport for each side of the record route\n\t\t#\t\tin this case we are missing TLS <-> TCP connections and TLS <-> TLS connections\n\t\t#\t\twe should instead generalize the handling of different ports/protocols and whether r2=on should be added\n\t\t# TODO: not adding in check for IPv4 vs IPv6 vs Domain here since we are sending to ourself - is this assumption correct?\n\t\trecord_route_preset(\"$dlg_var(dst_msteams_domain):SIPS_PORT;transport=tls;r2=on\", \"INTERNAL_IP_ADDR:SIP_PORT;transport=udp;r2=on\");\n\t\tforce_send_socket(tls:INTERNAL_IP_ADDR:SIPS_PORT);\n\t\t$dlg_var(sbc_translate) = \"0\";\n\t}\n\t# If coming from MSTEAMS, change Record Route to match the domain it's coming from\n\telse if (isbflagset(FLB_SRC_MSTEAMS)) {\n\t\trecord_route_preset(\"$dlg_var(src_msteams_domain):SIP_PORT;transport=udp;r2=on\", \"$dlg_var(src_msteams_domain):SIPS_PORT;transport=tls;r2=on\");\n\t\t$dlg_var(sbc_translate) = 1;\n\t}\n#!endif\n\t# the source of the request will be following the Route / Record-Route headers\n\t# so we only need send them back to our internal ip if from within our subnet\n\telse if (isflagset(FLT_SRC_INTERNAL_IP)) {\n\t\tif (isflagset(FLT_DST_INTERNAL_IP)) {\n\t\t\tif (isflagset(FLT_SRC_IPV6)) {\n\t\t\t\trecord_route_preset(\"[INTERNAL_IP6_ADDR]:$Rp\");\n\t\t\t}\n\t\t\telse {\n\t\t\t\trecord_route_preset(\"INTERNAL_IP_ADDR:$Rp\");\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tif (isflagset(FLT_SRC_IPV6)) {\n\t\t\t\trecord_route_preset(\"[EXTERNAL_IP6_ADDR]:$Rp;r2=on\", \"[INTERNAL_IP6_ADDR]:$Rp;r2=on\");\n\t\t\t}\n\t\t\telse {\n\t\t\t\trecord_route_preset(\"EXTERNAL_IP_ADDR:$Rp;r2=on\", \"INTERNAL_IP_ADDR:$Rp;r2=on\");\n\t\t\t}\n\t\t}\n\t\t$dlg_var(sbc_translate) = \"0\";\n\t}\n\telse if (isflagset(FLT_DST_INTERNAL_IP)) {\n\t\tif (isflagset(FLT_SRC_IPV6)) {\n\t\t\trecord_route_preset(\"[INTERNAL_IP6_ADDR]:$Rp;r2=on\", \"[EXTERNAL_IP6_ADDR]:$Rp;r2=on\");\n\t\t}\n\t\telse {\n\t\t\trecord_route_preset(\"INTERNAL_IP_ADDR:$Rp;r2=on\", \"EXTERNAL_IP_ADDR:$Rp;r2=on\");\n\t\t}\n\t\t$dlg_var(sbc_translate) = \"0\";\n\t}\n\telse {\n\t\tif (isflagset(FLT_SRC_IPV6)) {\n\t\t\trecord_route_preset(\"[EXTERNAL_IP6_ADDR]:$Rp\");\n\t\t}\n\t\telse {\n\t\t\trecord_route_preset(\"EXTERNAL_IP_ADDR:$Rp\");\n\t\t}\n\t\t$dlg_var(sbc_translate) = \"0\";\n\t}\n\n\t# store the source and destination for later usage within the dialog\n\tif ($du == $null) {\n\t\t$dlg_var(dst_uri) = $(ru{uri.duri});\n\t}\n\telse {\n\t\t$dlg_var(dst_uri) = $du;\n\t}\n\t$dlg_var(src_uri) = $sut;\n\n\treturn;\n}\n\n# TODO: on configuration failure continue with next endpoint instead of routing to this endpoint\nroute[SET_DST_SIGNALLING] {\n\tswitch ($dlg_var(dst_signalling)) {\n\t\tcase \"proxy\":\n\t\t\t# TODO: in future releases \"proxy\" will mean record route through kamailio\n\t\t\t# \t\tand another option for bypassing kamailio will be available\n\t\t\tif ($du == $null) {\n\t\t\t\t$du = $(ru{uri.duri});\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"sip_udp\":\n\t\t\turi_param_rm(\"transport\");\n\t\t\tif ($du == $null) {\n\t\t\t\t$du = \"sip:\" + $sel(ruri.hostport) + \";transport=udp\";\n\t\t\t}\n\t\t\telse {\n\t\t\t\t$du = \"sip:\" + $sel(dst_uri.hostport) + \";transport=udp\";\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"sip_tcp\":\n\t\t\turi_param_rm(\"transport\");\n\t\t\tif ($du == $null) {\n\t\t\t\t$du = \"sip:\" + $sel(ruri.hostport) + \";transport=tcp\";\n\t\t\t}\n\t\t\telse {\n\t\t\t\t$du = \"sip:\" + $sel(dst_uri.hostport) + \";transport=tcp\";\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"sip_sctp\":\n\t\t\turi_param_rm(\"transport\");\n\t\t\tif ($du == $null) {\n\t\t\t\t$du = \"sip:\" + $sel(ruri.hostport) + \";transport=sctp\";\n\t\t\t}\n\t\t\telse {\n\t\t\t\t$du = \"sip:\" + $sel(dst_uri.hostport) + \";transport=sctp\";\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"sip_ws\":\n\t\t\turi_param_rm(\"transport\");\n\t\t\tif ($du == $null) {\n\t\t\t\t$du = \"sip:\" + $sel(ruri.hostport) + \";transport=ws\";\n\t\t\t}\n\t\t\telse {\n\t\t\t\t$du = \"sip:\" + $sel(dst_uri.hostport) + \";transport=ws\";\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"sips_tls\":\n\t\t\turi_param_rm(\"transport\");\n\t\t\tif ($du == $null) {\n\t\t\t\t$du = \"sips:\" + $sel(ruri.hostport) + \";transport=tls\";\n\t\t\t}\n\t\t\telse {\n\t\t\t\t$du = \"sips:\" + $sel(dst_uri.hostport) + \";transport=tls\";\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"sips_sctp\":\n\t\t\turi_param_rm(\"transport\");\n\t\t\tif ($du == $null) {\n\t\t\t\t$du = \"sips:\" + $sel(ruri.hostport) + \";transport=sctp\";\n\t\t\t}\n\t\t\telse {\n\t\t\t\t$du = \"sips:\" + $sel(dst_uri.hostport) + \";transport=sctp\";\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"sips_wss\":\n\t\t\turi_param_rm(\"transport\");\n\t\t\tif ($du == $null) {\n\t\t\t\t$du = \"sips:\" + $sel(ruri.hostport) + \";transport=ws\";\n\t\t\t}\n\t\t\telse {\n\t\t\t\t$du = \"sips:\" + $sel(dst_uri.hostport) + \";transport=ws\";\n\t\t\t}\n\t\t\tbreak;\n\t\tdefault:\n\t\t\txlog(\"L_WARN\", \"invalid signalling configuration requested for endpoint $dlg_var(dst_gwid)\\n\");\n\t\t\tbreak;\n\t}\n\n\treturn;\n}\n\n# TODO: on configuration failure continue with next endpoint instead of routing to this endpoint\nroute[SET_DST_MEDIA] {\n\n\tif (!has_body(\"application/sdp\"))  {\n\t\treturn;\n\t}\n\n\t# TODO: revisit $dlg_var(FLD_USE_RTPE) usage here\n\tswitch ($dlg_var(dst_media)) {\n\t\tcase \"proxy\":\n\t\t\tdlg_setflag(FLD_USE_RTPE);\n\t\t\t$dlg_var(dst_media_tp) = \"\";\n\t\t\t# Setting a dialog variable since dlg_setflag doesn't see to work on Manage Branches\n\t\t\t$dlg_var(FLD_USE_RTPE) = \"1\";\n\t\t\tsdp_transport(\"$var(src_media_tp)\");\n\t\t\tbreak;\n\t\tcase \"direct\":\n\t\t\tdlg_resetflag(FLD_USE_RTPE);\n\t\t\t$dlg_var(FLD_USE_RTPE) = \"0\";\n\t\t\t$dlg_var(dst_media_tp) = \"\";\n\t\t\t$var(src_media_tp) = \"\";\n\t\t\tbreak;\n\t\tcase \"rtp_avp\":\n\t\t\tdlg_setflag(FLD_USE_RTPE);\n\t\t\t$dlg_var(FLD_USE_RTPE) = \"1\";\n\t\t\t$dlg_var(dst_media_tp) = \"RTP/AVP\";\n\t\t\tsdp_transport(\"$var(src_media_tp)\");\n\t\t\tbreak;\n\t\tcase \"rtp_savp\":\n\t\t\tdlg_setflag(FLD_USE_RTPE);\n\t\t\t$dlg_var(FLD_USE_RTPE) = \"1\";\n\t\t\t$dlg_var(dst_media_tp) = \"RTP/SAVP\";\n\t\t\tsdp_transport(\"$var(src_media_tp)\");\n\t\t\tbreak;\n\t\tcase \"rtp_avpf\":\n\t\t\tdlg_setflag(FLD_USE_RTPE);\n\t\t\t$dlg_var(FLD_USE_RTPE) = \"1\";\n\t\t\t$dlg_var(dst_media_tp) = \"RTP/AVPF\";\n\t\t\tsdp_transport(\"$var(src_media_tp)\");\n\t\t\tbreak;\n\t\tcase \"rtp_savpf\":\n\t\t\tdlg_setflag(FLD_USE_RTPE);\n\t\t\t$dlg_var(FLD_USE_RTPE) = \"1\";\n\t\t\t$dlg_var(dst_media_tp) = \"RTP/SAVPF\";\n\t\t\tsdp_transport(\"$var(src_media_tp)\");\n\t\t\tbreak;\n\t\tcase \"rtp_avp_any\":\n\t\t\tdlg_setflag(FLD_USE_RTPE);\n\t\t\t$dlg_var(FLD_USE_RTPE) = \"1\";\n\t\t\t$dlg_var(dst_media_tp) = \"UDP/TLS/RTP/SAVP\";\n\t\t\tsdp_transport(\"$var(src_media_tp)\");\n\t\t\tbreak;\n\t\tcase \"rtp_avpf_any\":\n\t\t\tdlg_setflag(FLD_USE_RTPE);\n\t\t\t$dlg_var(FLD_USE_RTPE) = \"1\";\n\t\t\t$dlg_var(dst_media_tp) = \"UDP/TLS/RTP/SAVPF\";\n\t\t\tsdp_transport(\"$var(src_media_tp)\");\n\t\t\tbreak;\n\t\tcase \"udptl\":\n\t\t\tdlg_setflag(FLD_USE_RTPE);\n\t\t\t$dlg_var(FLD_USE_RTPE) = \"1\";\n\t\t\t$dlg_var(dst_media_tp) = \"T.38=force\";\n\t\t\tsdp_transport(\"$var(src_media_tp)\");\n\t\t\tbreak;\n\t\tcase \"osrtp_avp\":\n\t\t\tdlg_setflag(FLD_USE_RTPE);\n\t\t\t$dlg_var(FLD_USE_RTPE) = \"1\";\n\t\t\t$dlg_var(dst_media_tp) = \"transport-protocol=RTP/AVP OSRTP=offer\";\n\t\t\tsdp_transport(\"$var(src_media_tp)\");\n\t\t\tbreak;\n\t\tcase \"osrtp_avpf\":\n\t\t\tdlg_setflag(FLD_USE_RTPE);\n\t\t\t$dlg_var(FLD_USE_RTPE) = \"1\";\n\t\t\t$dlg_var(dst_media_tp) = \"transport-protocol=RTP/AVPF OSRTP=offer\";\n\t\t\tsdp_transport(\"$var(src_media_tp)\");\n\t\t\tbreak;\n\t\tdefault:\n\t\t\txlog(\"L_WARN\", \"invalid media configuration requested for endpoint $dlg_var(dst_gwid). Defaulting to RTPEngine enabled.\\n\");\n\t\t\tdlg_setflag(FLD_USE_RTPE);\n\t\t\t$dlg_var(dst_media_tp) = \"\";\n\t\t\t$var(src_media_tp) = \"\";\n\t\t\tbreak;\n\t}\n\n\t$dlg_var(src_media_tp) = $var(src_media_tp);\n\n\treturn;\n}\n\n# Caller NAT detection\nroute[NATDETECT] {\n#!ifdef WITH_NAT\n\tif (nat_uac_test(\"19\")) {\n\t\tsetbflag(FLB_NATB);\n\t}\n#!endif\n\n\treturn;\n}\n\n# Server / DMZ NAT detection\n# Determine NAT requirements for destination and source and set flags for later usage\n# TODO: source self is known after REQINIT, consider copying over transaction flags such as FLT_SRC_SELF (multiple transactions in flow)\nroute[SERVERNATDETECT] {\n\t# default to NULL\n\t$vn(dst_ipv4) = $null;\n\t$vn(dst_ipv6) = $null;\n\n\t# always reset flags when called\n\tresetflag(FLT_SRC_INTERNAL_IP);\n\tresetflag(FLT_DST_INTERNAL_IP);\n\tresetflag(FLT_SRC_IPV4);\n\tresetflag(FLT_DST_IPV4);\n\tresetflag(FLT_SRC_IPV6);\n\tresetflag(FLT_DST_IPV6);\n\n\tif ($dd == $null) {\n\t\t$var(dst) = $rd;\n\t}\n\telse {\n\t\t$var(dst) = $dd;\n\t}\n\tif (is_ipv4($var(dst))) {\n\t\t$vn(dst_ipv4) = $var(dst);\n\t}\n\telse if (is_ipv6($var(dst))) {\n\t\t$vn(dst_ipv6) = $var(dst);\n\t}\n\telse {\n\t\tif (dns_query($var(dst), \"dst\")) {\n\t\t\t$var(i) = 0;\n\t\t\twhile ($var(i) < $dns(dst=>count)) {\n\t\t\t\tif ($vn(dst_ipv4) == $null && $dns(dst=>type[$var(i)]) == 4) {\n\t\t\t\t\t$vn(dst_ipv4) = $dns(dst=>addr[$var(i)]);\n\t\t\t\t}\n\t\t\t\telse if ($vn(dst_ipv6) == $null && $dns(dst=>type[$var(i)]) == 6) {\n\t\t\t\t\t$vn(dst_ipv6) = $dns(dst=>addr[$var(i)]);\n\t\t\t\t}\n\t\t\t\t$var(i) = $var(i) + 1;\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\txlog(\"L_ERR\", \"dns query failed for $var(dst)\\n\");\n\t\t}\n\t}\n\n#!ifdef WITH_SIGNAL_SERVERNAT\n\t# source does not change throughout\n\tif (is_in_subnet($si, \"INTERNAL_IP_NET\") || is_myself(\"$si\")) {\n\t\tsetflag(FLT_SRC_INTERNAL_IP);\n\t\tsetflag(FLT_SRC_IPV4);\n\t}\n\tif (is_in_subnet($vn(dst_ipv4), \"INTERNAL_IP_NET\") || is_myself(\"$var(dst)\")) {\n\t\tsetflag(FLT_DST_INTERNAL_IP);\n\t\tsetflag(FLT_DST_IPV4);\n\t}\n#!endif\n\n#!ifdef WITH_SIGNAL_SERVERNAT6\n\t# source does not change throughout\n\tif (!isflagset(FLT_SRC_INTERNAL_IP) && is_in_subnet($si, \"INTERNAL_IP6_NET\")) {\n\t\tsetflag(FLT_SRC_INTERNAL_IP);\n\t\tsetflag(FLT_SRC_IPV6);\n\t}\n\tif (!isflagset(FLT_DST_INTERNAL_IP) && is_in_subnet($vn(dst_ipv6), \"INTERNAL_IP6_NET\")) {\n\t\tsetflag(FLT_DST_INTERNAL_IP);\n\t\tsetflag(FLT_DST_IPV6);\n\t}\n#!endif\n\n#!ifdef WITH_DMZ\n\t# source does not change throughout\n\tif (!isflagset(FLT_SRC_INTERNAL_IP)) {\n\t\tif (is_in_subnet($si, \"INTERNAL_IP_NET\")) {\n\t\t\tsetflag(FLT_SRC_INTERNAL_IP);\n\t\t\tsetflag(FLT_SRC_IPV4);\n\t\t}\n#!ifdef WITH_IPV6\n\t\telse if (is_in_subnet($si, \"INTERNAL_IP6_NET\")) {\n\t\t\tsetflag(FLT_SRC_INTERNAL_IP);\n\t\t\tsetflag(FLT_SRC_IPV6);\n\t\t}\n#!endif\n\t}\n\tif (!isflagset(FLT_DST_INTERNAL_IP)) {\n\t\tif (is_in_subnet($vn(dst_ipv4), \"INTERNAL_IP_NET\")) {\n\t\t\tsetflag(FLT_DST_INTERNAL_IP);\n\t\t\tsetflag(FLT_DST_IPV4);\n\t\t}\n#!ifdef WITH_IPV6\n\t\telse if (is_in_subnet($vn(dst_ipv4), \"INTERNAL_IP6_NET\")) {\n\t\t\tsetflag(FLT_DST_INTERNAL_IP);\n\t\t\tsetflag(FLT_DST_IPV6);\n\t\t}\n#!endif\n\t}\n#!endif\n\n\treturn;\n}\n\n# RTPProxy control and signaling updates for NAT traversal\nroute[NATMANAGE] {\n#!ifdef WITH_NAT\n\tif (is_request() && isflagset(FLT_HAS_TOTAG) && check_route_param(\"nat=yes\")) {\n\t\tsetbflag(FLB_NATB);\n\t}\n\n\tif (!isbflagset(FLB_NATB)) {\n\t\treturn;\n\t}\n\n\t# handle clientside NAT traversal (requests)\n\tif (is_request()) {\n\t\t# initial requests we can change some headers for the rest of the transaction\n\t\tif (!isflagset(FLT_HAS_TOTAG)) {\n\t\t\t# Contact and Via updates\n\t\t\tif (isflagset(FLT_SRC_SIP) && !isbflagset(FLB_SRC_MSTEAMS)) {\n\t\t\t\tfix_nated_contact();\n\t\t\t\tmsg_apply_changes();\n\t\t\t\tforce_rport();\n\t\t\t}\n\t\t\t# registration Contact update\n\t\t\tif (is_method(\"REGISTER\")) {\n\t\t\t\tfix_nated_register();\n\t\t\t}\n\t\t\t# Route updates\n\t\t\tif (t_is_branch_route()) {\n\t\t\t\tadd_rr_param(\";nat=yes\");\n\t\t\t}\n\t\t}\n\t\t# for dialog NAT traversal\n\t\t# only forward requests if there is an existing connection to the destination\n\t\telse {\n\t\t\tset_forward_no_connect();\n\t\t}\n\t}\n\t# handle clientside NAT traversal (replies)\n\telse {\n\t\t# only set contact alias on replies if B-Leg of call is NATed as well\n\t\tif (is_first_hop() && nat_uac_test(\"1\")) {\n\t\t\tset_contact_alias();\n\t\t}\n\t\t# Do NAT traversal stuff for replies to a WebSocket connection\n\t\t# - even if it is not behind a NAT!\n\t\t# This won't be needed in the future if Kamailio and the\n\t\t# WebSocket client support Outbound and Path.\n#!ifdef WITH_WEBSOCKETS\n\t\tif (isflagset(FLT_SRC_WS) && nat_uac_test(\"64\")) {\n\t\t\tadd_contact_alias();\n\t\t}\n#!endif\n\t}\n\n#!endif\n\n\treturn;\n}\n\n# should only be called within request routes\nroute[RTPENGINEOFFER] {\n#!ifdef WITH_RTPENGINE\n\tif (!dlg_isflagset(FLD_USE_RTPE)) {\n\t\txlog(\"L_INFO\", \"RTPEngine is disabled\\n\");\n\t\treturn;\n\t}\n\txlog(\"L_INFO\", \"RTPEngine is enabled\\n\");\n\n\t# - Web to web\n\tif (isflagset(FLT_SRC_WS) && isbflagset(FLB_WS_DEVICE)) {\n\t\t$var(reflags) = \"trust-address replace-origin replace-session-connection SDES-off ICE=force \" + $dlg_var(dst_media_tp);\n\t}\n\t# - Web to SIP\n\telse if (isflagset(FLT_SRC_WS)) {\n\t\t$var(reflags) = \"trust-address replace-origin replace-session-connection rtcp-mux-demux ICE=remove \" + $dlg_var(dst_media_tp);\n\t}\n\t# - SIP to web\n\telse if (isbflagset(FLB_WS_DEVICE)) {\n\t\t$var(reflags) = \"trust-address replace-origin replace-session-connection rtcp-mux-offer ICE=force transcode-PCMU transcode-G722 SDES-off \" + $dlg_var(dst_media_tp);\n\t}\n\t# - MSTEAMS to SIP using RTP/AVP\n\telse if (isbflagset(FLB_SRC_MSTEAMS)) {\n\t\t$var(reflags) = \"trust-address replace-origin replace-session-connection rtcp-mux-offer port-latching ICE=remove \" + $dlg_var(dst_media_tp);\n#\t\t$var(reflags) = \"trust-address replace-origin replace-session-connection rtcp-mux-offer ICE=remove RTP/AVP original-sendrecv\";\n\t}\n\t# - MSTEAMS to SIP ONHOLD using RTP/AVP\n\telse if (isbflagset(FLB_SRC_MSTEAMS_ONHOLD)) {\n\t\t$var(reflags) = \"trust-address replace-origin replace-session-connection rtcp-mux-accept ICE=remove \" + $dlg_var(dst_media_tp);\n#\t\t$var(reflags) = \"trust-address replace-origin replace-session-connection rtcp-mux-accept ICE=remove RTP/AVP original-sendrecv\";\n\t}\n\t# - SIP to MSTEAMS\n\telse if (isbflagset(FLB_DST_MSTEAMS)) {\n \t\tif (has_totag()) {\n\n                        $var(reflags) = \"trust-address replace-origin replace-session-connection rtcp-mux-accept ICE=force transcode-PCMU transcode-G722 \" + $dlg_var(dst_media_tp);\n                }\n                else {\n\n                        $var(reflags) = \"trust-address replace-origin replace-session-connection rtcp-mux-offer ICE=force transcode-PCMU transcode-G722 \" + $dlg_var(dst_media_tp);\n                }\n\n\t}\n\t# - SIP to SIP\n\telse {\n\t\t$var(reflags) = \"trust-address replace-origin replace-session-connection rtcp-mux-demux ICE=remove \" + $dlg_var(dst_media_tp);\n\t}\n\n#!ifdef WITH_MEDIA_SERVERNAT\n\t# for serverside NAT we may need to use one of the internal IPs as the media address\n\tif (isflagset(FLT_DST_INTERNAL_IP)) {\n\t\tif (isflagset(FLT_DST_IPV6)) {\n\t\t\t$var(reflags) = $var(reflags) + \" media-address=INTERNAL_IP6_ADDR\";\n\t\t}\n\t\telse {\n\t\t\t$var(reflags)= $var(reflags) + \" media-address=INTERNAL_IP_ADDR\";\n\t\t}\n\t}\n\telse {\n\t\tif (isflagset(FLT_DST_IPV6)) {\n\t\t\t$var(reflags) = $var(reflags) + \" media-address=EXTERNAL_IP6_ADDR\";\n\t\t}\n\t\telse {\n\t\t\t$var(reflags)= $var(reflags) + \" media-address=EXTERNAL_IP_ADDR\";\n\t\t}\n\t}\n\n#!ifdef WITH_DMZ\n\tif (isflagset(FLT_SRC_INTERNAL_IP) && !isflagset(FLT_DST_INTERNAL_IP))  {\n\t\t$var(reflags)= $var(reflags) + \" direction=private direction=public\";\n\t}\n\telse if (!isflagset(FLT_SRC_INTERNAL_IP) && isflagset(FLT_DST_INTERNAL_IP)) {\n\t\t$var(reflags)= $var(reflags) + \" direction=public direction=private\";\n\t}\n#!else\n#!ifdef WITH_IPV6\n\t# select interface within rtpengine based on IP versions (by default will use the IPv4 interface)\n\t# only needed when not using the default interface (1st listen interface for rtpengine)\n    if (isflagset(FLT_SRC_IPV4) && isflagset(FLT_DST_IPV6)) {\n\t\t$var(reflags)= $var(reflags) + \" direction=ipv4 direction=ipv6\";\n\t}\n\telse if (isflagset(FLT_SRC_IPV6) && isflagset(FLT_DST_IPV4)) {\n\t\t$var(reflags)= $var(reflags) + \" direction=ipv6 direction=ipv4\";\n\t}\n\telse if (isflagset(FLT_SRC_IPV6) && isflagset(FLT_DST_IPV6)) {\n\t\t$var(reflags)= $var(reflags) + \" direction=ipv6 direction=ipv6\";\n\t}\n\telse {\n\t\t$var(reflags)= $var(reflags) + \" direction=ipv4 direction=ipv4\";\n\t}\n#!endif\n#!endif\n#!endif\n\n\txlog(\"L_INFO\", \"reflags: $var(reflags)\\n\");\n\tif (!rtpengine_offer(\"$var(reflags)\")) {\n\t\tsend_reply(\"503\", \"Service not available\");\n\t\texit;\n\t}\n#!endif\n\n\treturn;\n}\n\n# URI update for dialog requests\nroute[DLGURI] {\n#!ifdef WITH_NAT\n\tif(!isdsturiset()) {\n\t\thandle_ruri_alias();\n\t}\n#!endif\n\n\tif (check_route_param(\"rwdst\")) {\n\t\tif (is_direction(\"downstream\")) {\n\t\t\t$du = $dlg_var(dst_uri);\n\t\t}\n\t\telse {\n\t\t\t$du = $dlg_var(src_uri);\n\t\t}\n\t}\n}\n\n\nroute[RTPENGINEANSWER] {\n#!ifdef WITH_RTPENGINE\n\tif ($dlg_var(FLD_USE_RTPE) == \"0\") {\n\t\txlog(\"L_INFO\", \"RTPEngine is disabled\\n\");\n\t\treturn;\n\t}\n\txlog(\"L_INFO\", \"RTPEngine is enabled\\n\");\n\n\t# - Web to web\n\tif (isflagset(FLT_SRC_WS) && isbflagset(FLB_WS_DEVICE)) {\n\t\t$var(reflags) = \"trust-address replace-origin replace-session-connection SDES-off ICE=force \" + $dlg_var(src_media_tp);\n\t}\n\t# - Web to SIP\n\telse if (isflagset(FLT_SRC_WS)) {\n\t\t$var(reflags) = \"trust-address replace-origin replace-session-connection rtcp-mux-require ICE=force \" + $dlg_var(src_media_tp);\n\t}\n\t# - MSTEAMS to SIP using RTP/AVP\n\telse if (isbflagset(FLB_DST_MSTEAMS)) {\n\t\t$var(reflags) = \"trust-address replace-origin replace-session-connection rtcp-mux-offer ICE=remove \" + $dlg_var(src_media_tp);\n\t}\n\t# - SIP to MSTEAMS\n\telse if (isbflagset(FLB_SRC_MSTEAMS)) {\n\t\t$var(reflags) = \"trust-address replace-origin replace-session-connection rtcp-mux-require ICE=force transcode-PCMU transcode-G722 SDES-off \" + $dlg_var(src_media_tp);\n#\t\t$var(reflags) = \"trust-address replace-origin replace-session-connection rtcp-mux-require ICE=force codec-transcode=PCMU codec-transcode=PCMA codec-transcode=G722 codec-transcode=G729 SDES-off RTP/SAVP original-sendrecv\";\n\t}\n\t# - SIP to MSTEAMS ONHOLD\n\telse if (isbflagset(FLB_SRC_MSTEAMS_ONHOLD)) {\n\t\txlog(\"L_DBG\", \"ONHOLD - ANSWER\\n\");\n\t\t$var(reflags) = \"trust-address replace-origin replace-session-connection ICE=remove transcode-PCMU transcode-G722 SDES-off \" + $dlg_var(src_media_tp);\n#\t\t$var(reflags) = \"trust-address replace-origin replace-session-connection rtcp-mux-require ICE=force codec-transcode=PCMU codec-transcode=PCMA codec-transcode=G722 codec-transcode=G729 SDES-off RTP/SAVP original-sendrecv\";\n\t}\n\t# - SIP to SIP\n\telse {\n\t\t$var(reflags) = \"trust-address replace-origin replace-session-connection rtcp-mux-demux ICE=remove \" + $dlg_var(src_media_tp);\n\t}\n\n#!ifdef WITH_MEDIA_SERVERNAT\n\t# NOTE: no need to set direction= here, direction will be determined from the offer\n\t# for serverside NAT we may need to use one of the internal IPs as the media address\n\tif (isflagset(FLT_SRC_INTERNAL_IP)) {\n\t\tif (isflagset(FLT_SRC_IPV6)) {\n\t\t\t$var(reflags)= $var(reflags) + \" media-address=INTERNAL_IP6_ADDR\";\n\t\t}\n\t\telse {\n\t\t\t$var(reflags)= $var(reflags) + \" media-address=INTERNAL_IP_ADDR\";\n\t\t}\n\t}\n\telse {\n\t\tif (isflagset(FLT_SRC_IPV6)) {\n\t\t\t$var(reflags)= $var(reflags) + \" media-address=EXTERNAL_IP6_ADDR\";\n\t\t}\n\t\telse {\n\t\t\t$var(reflags)= $var(reflags) + \" media-address=EXTERNAL_IP_ADDR\";\n\t\t}\n\t}\n#!endif\n\n\txlog(\"L_INFO\", \"reflags: $var(reflags)\\n\");\n\tif (!rtpengine_answer(\"$var(reflags)\")) {\n\t\tsend_reply(\"503\", \"Service not available\");\n\t\troute(RTPENGINEDELETE);\n\t\texit;\n\t}\n#!endif\n\n\treturn;\n}\n\nroute[RTPENGINEDELETE] {\n#!ifdef WITH_RTPENGINE\n\tif (!dlg_isflagset(FLD_USE_RTPE)) {\n\t\txlog(\"L_INFO\", \"RTPEngine is disabled\\n\");\n\t\treturn;\n\t}\n\txlog(\"L_INFO\", \"RTPEngine is enabled\\n\");\n\n\trtpengine_delete();\n#!endif\n\n\treturn;\n}\n\n# XMLRPC routing\n#!ifdef WITH_XMLRPC\nroute[XMLRPC] {\n\t# allow XMLRPC from localhost\n\tif ((method==\"POST\" || method==\"GET\") && (src_ip==127.0.0.1)) {\n\t\t# close connection only for xmlrpclib user agents (there is a bug in\n\t\t# xmlrpclib: it waits for EOF before interpreting the response).\n\t\tif ($hdr(User-Agent) =~ \"xmlrpclib\")\n\t\t\tset_reply_close();\n\t\tset_reply_no_connect();\n\t\tdispatch_rpc();\n\t\texit;\n\t}\n\n\tsend_reply(\"403\", \"Forbidden\");\n\texit;\n}\n#!endif\n\n# Routing to voicemail server\nroute[TOVOICEMAIL] {\n#!ifdef WITH_VOICEMAIL\n\tif (!is_method(\"INVITE|SUBSCRIBE\")) {\n\t\treturn;\n\t}\n\n\t# check if VoiceMail server IP is defined\n\tif (strempty($sel(cfg_get.voicemail.srv_ip))) {\n\t\txlog(\"L_ERR\", \"VoiceMail routing enabled but IP not defined\\n\");\n\t\treturn;\n\t}\n\n\tif (is_method(\"INVITE\")) {\n\t\tif ($avp(oexten) == $null) {\n\t\t\treturn;\n\t\t}\n\t\t$ru = \"sip:\" + $avp(oexten) + \"@\" + $sel(cfg_get.voicemail.srv_ip) + \":\" + $sel(cfg_get.voicemail.srv_port);\n\t}\n\telse {\n\t\tif ($rU == $null) {\n\t\t\treturn;\n\t\t}\n\t\t$ru = \"sip:\" + $rU + \"@\" + $sel(cfg_get.voicemail.srv_ip) + \":\" + $sel(cfg_get.voicemail.srv_port);\n\t}\n\n\troute(RELAY);\n\texit;\n#!endif\n\n\treturn;\n}\n\n# Populate CDRs Table\n#!ifdef WITH_CDRS\nroute[CDRS] {\n\tsql_query(\"kam\",\"call kamailio_cdrs()\",\"rb\");\n\t# we are not using billing features\n\t#sql_query(\"kam\",\"call kamailio_rating('default')\",\"rb\");\n}\n#!endif\n\n# send async http request for notifications\n# required:     $avp(notification_type)\n# required:     $avp(notification_gwgroupid)\n# optional:     $avp(notification_gwid)\nroute[SEND_NOTIFICATION] {\n\tif ($avp(notification_type) != $null && $avp(notification_gwgroupid) != $null) {\n\t\t$http_req(method) = \"POST\";\n\t\t$http_req(hdr) = \"User-Agent: http_async_client\";\n\t\t$http_req(hdr) = \"Authorization: Bearer \" + $sel(cfg_get.server.api_token);\n\t\t$http_req(body) = '{\"gwgroupid\":' + $avp(notification_gwgroupid) + ', \"type\":' + $avp(notification_type) +\n\t\t\t\t', \"gwid\":' + $avp(notification_gwid) + ', \"text_body\":\"Gateway Group [' + $avp(notification_gwgroupid) +\n\t\t\t\t'] triggered the following notification for Gateway [' + $avp(notification_gwid) + ']\"}';\n\t\t$http_req(suspend) = 0;\n\n\t\txlog(\"L_INFO\", \"Sending request to $sel(cfg_get.server.api_server)/api/v1/notification/gwgroup for type $avp(notification_type)\\n\");\n\t\thttp_async_query(\"$sel(cfg_get.server.api_server)/api/v1/notification/gwgroup\", \"HTTP_REPLY\");\n\t}\n\telse {\n\t\txlog(\"L_ERR\", \"avp 'notification_type' and 'notification_gwgroupid' are required for notification sending\\n\");\n\t}\n\n\t$avp(notification_type) = $null;\n\t$avp(notification_gwgroupid) = $null;\n\t$avp(notification_gwid) = $null;\n}\n\nroute[HTTP_REPLY] {\n\tif ($http_ok) {\n\t\txlog(\"L_INFO\", \"status: $http_rs\\n\");\n\t\txlog(\"L_DBG\", \"body: $http_rb\\n\");\n\t}\n\telse {\n\t\txlog(\"L_ERR\", \"error: $http_err)\\n\");\n\t}\n}\n\nroute[DIALOG_TIMEOUT] {\n\t# TODO: handle edge cases\n\t# \t\twhen we rewrite the reply this may cause issues\n\t# \t\tdialog module is using tm underneath to send these requests\n  \t#\t\tit seems to be using the contact from either side to teardown the call\n\t#if ($dlg_var(sbc_translate) == \"1\") {\n\t#\n\t#}\n\t#else {\n\t#\tdlg_bye(\"all\");\n\t#}\n\tdlg_bye(\"all\");\n\n\t# we still need to cleanup rtpengine sessions\n\troute(RTPENGINEDELETE);\n}\n\n# executed when 200 OK reply for INVITE is processed\nevent_route[dialog:start] {\n#!ifdef WITH_CALL_SETTINGS\n\t# increment the concurrent calls htable\n\tif ($dlg_var(src_gwgroupid) != $dlg_var(dst_gwgroupid)) {\n\t\tsht_lock(\"concurrent_calls=>$dlg_var(src_gwgroupid)\");\n\t\t$sht(concurrent_calls=>$dlg_var(src_gwgroupid)) = $sht(concurrent_calls=>$dlg_var(src_gwgroupid)) + 1;\n\t\tsht_unlock(\"concurrent_calls=>$dlg_var(src_gwgroupid)\");\n\t}\n\tsht_lock(\"concurrent_calls=>$dlg_var(dst_gwgroupid)\");\n\t$sht(concurrent_calls=>$dlg_var(dst_gwgroupid)) = $sht(concurrent_calls=>$dlg_var(dst_gwgroupid)) + 1;\n\tsht_unlock(\"concurrent_calls=>$dlg_var(dst_gwgroupid)\");\n#!endif\n\n\treturn;\n}\n\n# executed when dialog is not completed (300 or greater reply code to INVITE)\n#event_route[dialog:failed] {\n#\treturn;\n#}\n\n# executed when BYE is processed or dialog timed out\nevent_route[dialog:end] {\n#!ifdef WITH_CALL_SETTINGS\n\t# decrement the concurrent calls htable\n\tif ($dlg_var(src_gwgroupid) != $dlg_var(dst_gwgroupid)) {\n\t\tsht_lock(\"concurrent_calls=>$dlg_var(src_gwgroupid)\");\n\t\t$sht(concurrent_calls=>$dlg_var(src_gwgroupid)) = $sht(concurrent_calls=>$dlg_var(src_gwgroupid)) - 1;\n\t\tsht_unlock(\"concurrent_calls=>$dlg_var(src_gwgroupid)\");\n\t}\n\tsht_lock(\"concurrent_calls=>$dlg_var(dst_gwgroupid)\");\n\t$sht(concurrent_calls=>$dlg_var(dst_gwgroupid)) = $sht(concurrent_calls=>$dlg_var(dst_gwgroupid)) - 1;\n\tsht_unlock(\"concurrent_calls=>$dlg_var(dst_gwgroupid)\");\n#!endif\n\n\treturn;\n}\n\nevent_route[uac:reply] {\n\txlog(\"L_DBG\", \"Request sent to $uac_req(ruri) with event code $uac_req(evcode)\\n\");\n}\n\nevent_route[xhttp:request] {\n\tif ($hu =~ \"^/api/kamailio\" && dst_ip==127.0.0.1) {\n\t\tjsonrpc_dispatch();\n\t}\n#!ifdef WITH_WEBSOCKETS\n\telse if ($Rp == \"WSS_PORT\") {\n\t\tif ($hdr(Upgrade) =~ \"websocket\" && $hdr(Connection) =~ \"Upgrade\" && $rm =~ \"GET\") {\n\t\t\tif (ws_handle_handshake()) {\n\t\t\t\t# Optional... cache some information about the\n\t\t\t\t# successful connection\n\t\t\t\texit;\n\t\t\t}\n\t\t}\n\t}\n\telse {\n\t\txhttp_reply(\"403\", \"Forbidden\", \"text/html\",\n\t\t\t\"<html><body>Will only communicate on the local interface or WebSocket Port WSS_PORT</body></html>\");\n\t\texit;\n\t}\n#!endif\n\n\treturn;\n}\n\n# executed for tm locally created requests\nevent_route[tm:local-request] {\n#!ifdef WITH_MSTEAMS\n\tif (is_method(\"OPTIONS\") && $ru =~ \"pstnhub.microsoft.com\") {\n\t\tappend_hf(\"Contact: <sip:$fd:SIPS_PORT;transport=tls>\\r\\n\");\n\t\txlog(\"L_DBG\", \"Changed contact to $ct\\n\");\n\t}\n#!endif\n\n\t# TODO: why are we changing the contact here?\n\t# Get destination IP\n\t$var(destIP)=$(du{s.select,1,:});\n\n\t# Only change the contact if an Inound NLB is set and the Register is going to a carrier\n\tif (is_method(\"REGISTER\") && (\"INBOUND_NLB_FQDN\" != \"\") && !allow_address(FLT_PBX, \"$var(destIP)\", 0)) {\n\t\tif (subst('/^Contact: <sip:([0-9]+)@(.*)$/Contact: <sip:\\1@INBOUND_NLB_FQDN>/ig')) {\n\t\t\txlog(\"L_DBG\", \"Changed REGISTER contact to load balancer address: $mb\\n\");\n\t\t}\n\t}\n}\n\n# executed for tm locally created responses\n#event_route[tm:local-response] {\n#\n#}\n\n# executed for sl received (and ignored) ACK responses\n#event_route[sl:filtered-ack] {\n#\n#}\n\n# executed for sl locally created responses\n#event_route[sl:local-response] {\n#\n#}\n\nevent_route[usrloc:contact-expired] {\n\tif (sql_xquery(\"kam\", \"SELECT rpid AS gwgroupid FROM subscriber WHERE username='$(ulc(exp=>addr){uri.user})'\", \"rows\") == 1) {\n\t\t$var(src_gwgroupid) = $xavp(rows=>gwgroupid);\n\t\t$var(received_addr) = $(ulc(exp=>received){re.subst,/^(sip:|sips:)?(.*)$/\\2/});\n\n\t\tif (sql_xquery(\"kam\", \"SELECT gwid FROM dr_gateways WHERE address='$var(received_addr)' AND description REGEXP 'gwgroup:$var(src_gwgroupid)(,|$$)'\", \"rows\") == 1) {\n\t\t\t$var(src_gwid) = $xavp(rows=>gwid);\n\n\t\t\txlog(\"L_DBG\", \"found gateway $var(src_gwid) for address $var(received_addr), removing from gwgroup $var(src_gwgroupid)\\n\");\n\t\t\tsql_query(\"kam\", \"DELETE FROM dr_gateways WHERE gwid='$var(src_gwid)'\");\n\t\t\tsql_query(\"kam\", \"UPDATE dr_gw_lists SET gwlist=REGEXP_REPLACE(REGEXP_REPLACE(gwlist, '([,;])?$var(src_gwid)', ''), '^([,;])', '') WHERE id=$var(src_gwgroupid)\");\n\n\t\t\tjsonrpc_exec('{\"jsonrpc\": \"2.0\", \"method\": \"drouting.reload\", \"id\": 1}');\n\t\t}\n\t}\n}\n\nroute[REMOVE_REFER] {\n\tif (subst_hf(\"Allow\", \"/(.+)(REFER,)\\s?(.+)/\\1\\3/\", \"f\")) {\n\t\txlog(\"L_INFO\", \"Removing REFER from Accepted Method to $du\\n\");\n\t\tif (!msg_apply_changes()) {\n\t\t\txlog(\"L_ERR\", \"failed applying changes to message\\n\");\n\t\t}\n\t}\n}\n\n# CUSTOM: stateful SBC translations\nroute[SBC_TRANSLATE_LR] {\n#!ifdef WITH_NAT\n\tif (!isdsturiset()) {\n\t\thandle_ruri_alias();\n\t}\n#!endif\n\n\txlog(\"L_DBG\", \"request values before SBC translations: ru=$ru, du=$du dlg_var(src_cturi)=$dlg_var(src_cturi) dlg_var(dst_duri)=$dlg_var(dst_duri) dlg_var(dst_cturi)=$dlg_var(dst_cturi)\\n\");\n\tif (is_direction(\"downstream\")) {\n\t\t$ru = $dlg_var(src_cturi);\n\t\t$du = $dlg_var(dst_duri);\n\t\tsubst_hf(\"Contact\", \"/.*/<$dlg_var(dst_cturi)>/\", \"f\");\n\t\tif (!strempty($dlg_var(dst_auth_user))) {\n\t\t\t$avp(auth_user) = $dlg_var(dst_auth_user);\n\t\t\t$avp(auth_pass) = $dlg_var(dst_auth_pass);\n\t\t\t$avp(auth_realm) = $dlg_var(dst_auth_realm);\n\t\t}\n\t}\n\telse if (is_direction(\"upstream\")) {\n\t\t$ru = $dlg_var(dst_cturi);\n\t\t$du = $dlg_var(src_duri);\n\t\tsubst_hf(\"Contact\", \"/.*/<$dlg_var(src_cturi)>/\", \"f\");\n\t\tif (!strempty($dlg_var(src_auth_user))) {\n\t\t\t$avp(auth_user) = $dlg_var(src_auth_user);\n\t\t\t$avp(auth_pass) = $dlg_var(src_auth_pass);\n\t\t\t$avp(auth_realm) = $dlg_var(src_auth_realm);\n\t\t}\n\t}\n\telse {\n\t\txlog(\"L_WARN\", \"could not determine call direction\\n\");\n\t}\n\txlog(\"L_DBG\", \"request values after SBC translations: ru=$ru, du=$du\\n\");\n}\n\n\n\nroute[SBC_TRANSLATE_SR] {\n#!ifdef WITH_NAT\n\tif (!isdsturiset()) {\n\t\thandle_ruri_alias();\n\t}\n#!endif\n\n\txlog(\"L_DBG\", \"request values before SBC translations: ru=$ru, du=$du dlg_var(src_cturi)=$dlg_var(src_cturi) dlg_var(dst_duri)=$dlg_var(dst_duri) dlg_var(dst_cturi)=$dlg_var(dst_cturi)\\n\");\n\tif ($ft == $dlg(from_tag)) {\n\t\t$ru = $dlg_var(src_cturi);\n\t\t$du = $dlg_var(dst_duri);\n\t\tsubst_hf(\"Contact\", \"/.*/<$dlg_var(dst_cturi)>/\", \"f\");\n\t\tif (!strempty($dlg_var(dst_auth_user))) {\n\t\t\t$avp(auth_user) = $dlg_var(dst_auth_user);\n\t\t\t$avp(auth_pass) = $dlg_var(dst_auth_pass);\n\t\t\t$avp(auth_realm) = $dlg_var(dst_auth_realm);\n\t\t}\n\t}\n\telse if ($ft == $dlg(to_tag)) {\n\t\t$ru = $dlg_var(dst_cturi);\n\t\t$du = $dlg_var(src_duri);\n\t\tsubst_hf(\"Contact\", \"/.*/<$dlg_var(src_cturi)>/\", \"f\");\n\t\tif (!strempty($dlg_var(src_auth_user))) {\n\t\t\t$avp(auth_user) = $dlg_var(src_auth_user);\n\t\t\t$avp(auth_pass) = $dlg_var(src_auth_pass);\n\t\t\t$avp(auth_realm) = $dlg_var(src_auth_realm);\n\t\t}\n\t}\n\telse {\n\t\txlog(\"L_WARN\", \"could not determine call direction\\n\");\n\t}\n\txlog(\"L_DBG\", \"request values after SBC translations: ru=$ru, du=$du\\n\");\n}\n\n# Rewrite the Contact domain with given one\n# required:     $var(ct_domain)\n\n# optional:     $var(transport)\n\nroute[REPLACE_CONTACT_DOMAIN] {\n\tif (!strempty($sel(contact.uri.type))) {\n\t\t$var(ct_uri) = $sel(contact.uri.type) + \":\";\n\t}\n\telse {\n\t\t$var(ct_uri) = \"sip:\";\n\t}\n\tif (!strempty($sel(contact.uri.user))) {\n\t\t$var(ct_uri) = $var(ct_uri) + $sel(contact.uri.user) + \"@\" + $var(ct_domain);\n\t}\n\telse  {\n\t\t$var(ct_uri) = $var(ct_uri) + \"@\" + $var(ct_domain);\n\t}\n\tif (!strempty($sel(contact.uri.port))) {\n\t\t$var(ct_uri) = $var(ct_uri) + \":\" + $sel(contact.uri.port);\n\t}\n\t\n  if (!strempty($var(transport))) {\n\n                $var(ct_uri) = $var(ct_uri) + \";\" + $var(transport);\n  }\n  \n  if (!strempty($sel(contact.uri.params))) {\n\n                $var(ct_uri) = $var(ct_uri) + \";\" + $sel(contact.uri.params);\n \n\t\tif (isbflagset(FLB_WS_DEVICE)) {\n\t\t\t$var(ct_uri) = $var(ct_uri) + \";\" + \"domain=\" + $var(ct_domain) + \";\" + \"exten=\" + $fU;\n\t\t }\n  }\n  else {\n\t\tif (isbflagset(FLB_WS_DEVICE)) {\n\t\t\t$var(ct_uri) = $var(ct_uri) + \";\" + \"domain=\" + $var(ct_domain) + \";\" + \"exten=\" + $fU;\n\t\t }\n  }\n  \n\n\tif (subst_hf(\"Contact\", \"/<([^>]+)>(.*)/<$var(ct_uri)>\\2/\", \"f\")) {\n\t\txlog(\"L_INFO\", \"changed contact to match From domain\\n\");\n\t\tif (!msg_apply_changes()) {\n\t\t\txlog(\"L_ERR\", \"failed applying changes to message\\n\");\n\t\t}\n\t}\n\n\n\t\n}\n\n# Manage outgoing branches\nbranch_route[MANAGE_BRANCH] {\n\txlog(\"L_DBG\", \"new branch [$T_branch_idx] created to $ru via $du\\n\");\n\n\tif (isbflagset(FLB_WS_DEVICE)) {\n\t\t$var(FLB_WS_DEVICE) = 1;\n\t}\n\telse {\n\t\t$var(FLB_WS_DEVICE) = 0;\n\t}\n\n\tif (has_body(\"application/sdp\") || isbflagset(FLB_SRC_MSTEAMS)) {\n\t\troute(RTPENGINEOFFER);\n\t\t# if you do not want the extra RTPEngine param then uncomment this line\n\t\t#sdp_remove_line_by_prefix(\"a=rtpengine\");\n\t}\n}\n\n# Manage incoming replies\nonreply_route[MANAGE_REPLY] {\n\txlog(\"L_DBG\", \"incoming reply from source address $si:$sp\\n\");\n\n\t# handle non-error replies\n\tif (t_check_status(\"[12][0-9][0-9]\")) {\n\t\tif ($dlg_var(sbc_translate) == \"1\") {\n\t\t\t# let UAC think we are communicating with original request values\n\t\t\t# we will handle translations on our end\n\t\t\t$fu = $dlg_var(src_ofuri);\n\t\t\t$tu = $dlg_var(src_oturi);\n\t\t\tsubst_hf(\"Contact\", \"/.*/<$dlg_var(src_oruri)>/\", \"f\");\n\t\t}\n\n\t\t# handle clientside NAT\n\t\troute(NATMANAGE);\n\n\t\t# handle SDP munging with rtpengine\n\t\tif (has_body(\"application/sdp\")) {\n\t\t\troute(RTPENGINEANSWER);\n\t\t\tif ($avp(sdp_media_direction) != $null) {\n\t\t\t\tif (!msg_apply_changes()) {\n\t\t\t\t\txlog(\"L_ERR\", \"could not update sdp\");\n\t\t\t\t}\n\t\t\t\tif (!subst(\"/^a=(sendrecv|recvonly|sendonly|inactive).*/a=$avp(sdp_media_direction)/\")) {\n\t\t\t\t\t#search_append_body(\"^a=.+\", \"a=$avp(sdp_media_direction)\");\n\t\t\t\t\txlog(\"L_ERR\", \"could not update sdp\\n\");\n\t\t\t\t}\n\t\t\t\tif (!msg_apply_changes()) {\n\t\t\t\t\txlog(\"L_ERR\", \"could not update sdp\\n\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t# TODO: why are we dropping 183 replies to MSTEAMS? - marked for review/validation\n\tif (t_check_status(\"183\") && isbflagset(FLB_DST_MSTEAMS)) {\n\t\tdrop();\n\t}\n\t\n\tif (t_check_status(\"200\") && !strempty($dlg_var(src_msteams_domain))) {\t\n\t\tif (is_present_hf(\"ALLOW\")) {\n\n\t\t\t#Prevent MSTeams from sending REFER requests\n                        if ((int)$sel(cfg_get.server.msteams_disable_refer)) {\n\t\troute(REMOVE_REFER);\n                        }\n                }\n                else {\n                        append_hf(\"ALLOW: INVITE,ACK,OPTIONS,CANCEL,BYE,NOTIFY\\r\\n\");\n               }\n\t\txlog(\"L_INFO\", \"+++ Remove REFER\\n\");\n\t}\n\n\t\t# set the final ru for subsequent requests to the contact from the 200\n\t\tif (t_check_status(\"200\")) {\n\t\t\tif ($ct =~ \"<.*>\") {\n\t\t\t\t$dlg_var(src_cturi) = $(ct{s.select,0,>}{s.select,1,<});\n\t\t\t}\n\t\t\telse {\n\t\t\t\t$dlg_var(src_cturi) = $ct;\n\t\t\t}\n\t\t}\n\n#\tif (t_check_status(\"100|180|181|183\") && $avp(calltype) == \"inbound\") {\n#\t\t# Increase the lifetime of the current INVITE to pbx_invite_timeout_aftertry if endpoint returns 100/180/181/183.\n#\t\t# This means that the endpoint is at least trying to establish the call. So, we will extend the timeout.\n#\n#\t\t$var(pbx_invite_timeout) = (int)$sel(cfg_get.server.pbx_invite_timeout_aftertry);\n#\t\tt_set_max_lifetime($var(pbx_invite_timeout), 0);\n#\t\txlog(\"L_DBG\", \"Increasing the Invite Timeout for <$ci> to <$var(pbx_invite_timeout)>\\n\");\n#\t}\n}\n\n# Manage failure routing cases\nfailure_route[MANAGE_FAILURE] {\n\t# Capture the Failure in the CDR\n\tsetflag(FLT_ACCFAILED);\n\n\troute(NATMANAGE);\n\n\tif (t_is_canceled()) {\n\t\troute(RTPENGINEDELETE);\n\t\texit;\n\t}\n\n#!ifdef WITH_BLOCK3XX\n\t# block call redirect based on 3xx replies.\n\tif (t_check_status(\"3[0-9][0-9]\")) {\n\t\tt_reply(\"403\", \"Redirect Forbidden\");\n\t\troute(RTPENGINEDELETE);\n\t\texit;\n\t}\n#!endif\n\n\t# use uac credentials if set by in SET_CALLAUTH_INFO\n\tif (t_check_status(\"401|407\") && !strempty($avp(auth_user))) {\n\t\tt_drop_replies();\n\n#!ifdef WITH_UAC\n\t\tif (!(uac_auth() && t_relay())) {\n\t\t\txlog(\"L_INFO\", \"UAC Authentication failed\\n\");\n\t\t\tt_reply(\"503\",\"Service not available\");\n\t\t\troute(RTPENGINEDELETE);\n\t\t}\n\t\texit;\n#!else\n\t\txlog(\"L_WARN\", \"destination requested UAC auth when it is not enabled\\n\");\n\t\tt_reply(\"503\", \"Service not available\");\n\t\troute(RTPENGINEDELETE);\n\t\texit;\n#!endif\n\t}\n\n\t# if using pass thru auth relay the reply\n\tif (t_check_status(\"401|407\") && isflagset(FLT_PASSTHRU_AUTH)) {\n\t\tt_relay();\n\t\texit;\n\t}\n\n#!ifdef WITH_MSTEAMS\n \tif (t_check_status(\"4[0-9][0-9]\") && isbflagset(FLB_DST_MSTEAMS)) {\n \t\tt_relay();\n \t\texit;\n \t}\n#!endif\n\n#!ifdef WITH_DROUTE\n\tif (t_check_status(\"[0-6][0-9][0-9]\") || !t_any_replied()) {\n\t\tif (use_next_gw()) {\n\t\t\t# set du to match what drouting selected\n\t\t\t$du = $(ru{uri.duri});\n\n\t\t\t# Set INVITE  max lifetime to ensure Primary and Secondary PBX server feature works.\n\t\t\tif (isbflagset(FLB_SRC_CARRIER)) {\n\t\t\t\tt_set_fr((int)$sel(cfg_get.server.pbx_invite_timeout_aftertry), (int)$sel(cfg_get.server.pbx_invite_timeout));\n\t\t\t}\n\t\t\t\t\n\t\t\t$du = $ru;\n\t\t\troute(RELAY);\n\t\t\texit;\n\t\t}\n\t\telse {\n\t\t\t# Only intervene on a request from the Carrier\n\t\t\tif (isbflagset(FLB_SRC_CARRIER)) {\n\t\t\t\t# this route will check for failover and return false if it does not route the call\n\t\t\t\t# if we are not failover routing, then none of the routes were successful and we send back an error\n\t\t\t\tif (!route(NEXTHOP_FAILOVER)) {\n\t\t\t\t\tt_reply(\"503\",\"Service not available\");\n\n\t\t\t\t\t# we can only send notification if mapped to endpoint group\n\t\t\t\t\tif ($dlg_var(src_gwtype) == FLT_PBX) {\n\t\t\t\t\t\t$avp(notification_type) = NOTIFICATION_GWFAILURE;\n\t\t\t\t\t\t$avp(notification_gwgroupid) = $dlg_var(src_gwgroupid);\n\t\t\t\t\t\t$avp(notification_gwid) = $dlg_var(src_gwid);\n\t\t\t\t\t\troute(SEND_NOTIFICATION);\n\t\t\t\t\t}\n\t\t\t\t\telse if ($dlg_var(dst_gwtype) == FLT_PBX) {\n\t\t\t\t\t\t$avp(notification_type) = NOTIFICATION_GWFAILURE;\n\t\t\t\t\t\t$avp(notification_gwgroupid) = $dlg_var(dst_gwgroupid);\n\t\t\t\t\t\t$avp(notification_gwid) = $dlg_var(dst_gwid);\n\t\t\t\t\t\troute(SEND_NOTIFICATION);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t# Don't teardown the RTPEngine if coming or going to MSTeams\n\t\t\tif (!isbflagset(FLB_SRC_MSTEAMS)) {\n\t\t\t\txlog(\"L_DBG\", \"Shutting Down RTPEngine due to receiving reply code: $rs from $si\");\n\t\t\t\troute(RTPENGINEDELETE);\n\t\t\t}\n\t\t\texit;\n\t\t}\n\t}\n#!endif\n\n\tif (t_branch_timeout()) {\n\t\troute(RTPENGINEDELETE);\n\t\texit;\n\t}\n\n#!ifdef WITH_VOICEMAIL\n\t# serial forking\n\t# - route to voicemail on busy or no answer (timeout)\n\tif (t_check_status(\"486|408\")) {\n\t\t$du = $null;\n\t\troute(TOVOICEMAIL);\n\t\troute(RTPENGINEDELETE);\n\t\texit;\n\t}\n#!endif\n}\n\n# TODO: FLB_SRC_PBX is not set within in-dialog requests\nroute[PBX_TO_ENDPOINT_LOOKUP] {\n\t# Lookup the actual location of endpoint if\n\t# coming from a PBX and the request domain is a local ip address\n\tif (isbflagset(FLB_SRC_PBX) && is_ip_rfc1918($rd)) {\n\t\tif (lookup(\"location\",\"sip:$rU@$fd\")) {\n\t\t\txlog(\"L_DBG\", \"Looking up the domain and getting domain: $fd\\n\");\n\t\t}\n\t}\n}\n\n# TODO: dynamically get user_tn / pilot_tn from user configs\n# Carrier Enrichment for CenturyLink\n# validated SIP carriers:\n# voip.centurylink.com\n# 65.149.22.7, 65.149.23.7, 65.149.24.7, 65.149.25.7\n# 216.206.64.7, 216.206.64.71, 216.206.64.91\n# 216.206.66.7, 216.206.66.71, 216.206.66.91\nroute[ENRICH_CARRIER_CENTURYLINK_OUTBOUND] {\n\t$var(domain) = \"voip.centurylink.com\";\n\t$var(user_tn) = \"6467687570\";\n\t$var(pilot_tn) = \"6467687572\";\n\n\tif ($rd =~ \"$var(domain).*|65.149.22.7.*|65.149.23.7.*|65.149.24.7.*|65.149.25.7.*|216.206.64.7.*|216.206.64.71.*|216.206.64.91.*|216.206.66.7.*|216.206.66.71.*|216.206.66.91.*\") {\n\t\txlog(\"L_INFO\", \"centurylink carrier match\\n\");\n\t\t$du = $ru;\n\t\t$rd = $var(domain);\n\t\t# Remove the port\n\t\t$rp = \"\";\n\t\t# Change the from and to domain to match the carrier domain name\n\t\t$td = $var(domain);\n\t\t$fd = $var(domain);\n\t\t# Uncomment this line if you need to change the from user to the user_tn\n\t\t#$fU = $var(user_tn);\n\t\t# Uncomment this line if you need to change the domain of the contact - this is not recommended\n\t\t#subst('/^Contact: <sip:([0-9]+)@(.*)$/Contact: <sip:\\1@107.21.184.251>/ig')\n\t\t# Add P-Asserted-Identity per the carriers requirement\n\t\tappend_hf(\"P-Asserted-Identity: <sip:$var(pilot_tn)@$var(domain)>\\r\\n\");\n\t\tif (!msg_apply_changes()) {\n\t\t\txlog(\"L_ERR\", \"failed applying changes\\n\");\n\t\t}\n\t}\n}\n\nroute[ENRICH_CARRIER_SIGNALWIRE_INBOUND] {\n\t$var(domain_lookup) = \".+sip.signalwire.com\";\n\n\txlog(\"L_DBG\", \"before transform:\\n$mb\\n\");\n\n\tif ($td =~ $var(domain_lookup)) {\n\t\txlog(\"L_INFO\", \"signalwire carrier match\\n\");\n\n\t\t# Change the \"request username\" to \"to username\"\n\t\t$rU = $tU;\n\t\tif (!msg_apply_changes()) {\n\t\t\txlog(\"L_ERR\", \"failed applying changes\\n\");\n\t\t}\n\n\t\txlog(\"L_DBG\", \"after transform:\\n$mb\\n\");\n\t}\n}\n\nroute[ENRICH_CARRIER_SIGNALWIRE_OUTBOUND] {\n\t$var(domain) = \"sip.signalwire.com\";\n\t$var(domain_lookup) = \".+sip.signalwire.com\";\n\t$var(user) = $fU;\n\n\txlog(\"L_DBG\", \"before transform:\\n$mb\\n\");\n\n\tif ($rd =~ $var(domain_lookup)) {\n\t\txlog(\"L_INFO\", \"signalwire carrier match\\n\");\n\n\t\t# Change the from domain to the request domain\n\t\t$fd = $rd;\n\t\t# Change the from user to the authenticated user\n\t\t$fU = $avp(auth_user);\n\n\t\t# Add the callerid\n\t\tappend_hf(\"P-Asserted-Identity: <sip:$var(user)@$var(domain)>\\r\\n\");\n\t\tif (!msg_apply_changes()) {\n\t\t\txlog(\"L_ERR\", \"failed applying changes\\n\");\n\t\t}\n\n\t\txlog(\"L_DBG\", \"after transform:\\n$mb\\n\");\n\t}\n}\n\nroute[ENRICH_CARRIER_INBOUND] {\n\troute(ENRICH_CARRIER_SIGNALWIRE_INBOUND);\n}\n\n# Carrier Enrichment\nroute[ENRICH_CARRIER_OUTBOUND] {\n\troute(ENRICH_CARRIER_CENTURYLINK_OUTBOUND);\n\troute(ENRICH_CARRIER_SIGNALWIRE_OUTBOUND);\n}\n\n####### CUSTOM_ROUTING_START #########\n# add custom routes here\n\n####### CUSTOM_ROUTING_END #########\n\n"
  },
  {
    "path": "kamailio/configs/stir-shaken.cfg",
    "content": "route[STIRSHAKEN_INBOUND] {\n    # Only route if request is coming from an Endpoint\n    if (!isbflagset(FLB_SRC_CARRIER)) {\n        xlog(\"L_INFO\", \"[STIRSHAKEN_INBOUND] <$ci> $si not allowed to talk with this server \\n\");\n        return;\n    }\n\n    xlog(\"L_INFO\", \"STIR/SHAKEN Inbound Logic\");\n\n    if ($hdrc(Identity) == 0 ){\n        xlog(\"L_INFO\", \"[STIRSHAKEN_INBOUND] Missing the Identity Header Skipping this logic \\n\");\n        return;\n    }\n\n    #Verify Call Identity\n    xlog(\"L_INFO\", \"Identity Header: $hdr(Identity),$hdr(Reason)\");\n    xlog(\"L_INFO\", \"Encoded Header 1:   $(hdr(Identity){s.select,0,.})\");\n    xlog(\"L_INFO\", \"Encoded Header 2:   $(hdr(Identity){s.select,1,.})\");\n    xlog(\"L_INFO\", \"Decoded Header 1:   $(hdr(Identity){s.select,0,.}{s.decode.base64}) \");\n    xlog(\"L_INFO\", \"Decoded Header 2:   $(hdr(Identity){s.select,1,.}{s.decode.base64}) \");\n\n    $var(json_payload) = $(hdr(Identity){s.select,1,.}{s.decode.base64});\n    jansson_get_field($var(json_payload), \"attest\", \"$var(attest_value)\");\n    xlog(\"L_INFO\", \"Attest Value:   $(var(attest_value))\");\n\n    if (stirshaken_check_identity() == 1) {\n        xlog(\"L_INFO\", \"Shaken Identity is OK\\n\");\n\n        if ($var(attest_value) == \"A\") {\n            uac_replace_from(\"$sel(cfg_get.stir_shaken.stir_shaken_prefix_a)  $fU\",\"\");\n        }\n        else if ($var(attest_value) == \"B\") {\n            uac_replace_from(\"$sel(cfg_get.stir_shaken.stir_shaken_prefix_b)  $fU\",\"\");\n        }\n        else if ($var(attest_value) == \"C\") {\n            uac_replace_from(\"$sel(cfg_get.stir_shaken.stir_shaken_prefix_c)  $fU\",\"\");\n        }\n        else {\n            uac_replace_from(\"VERIFIED  $fU\",\"\");\n        }\n    }\n    else {\n        if ($sel(cfg_get.stir_shaken.stir_shaken_block_invalid) == 1) {\n            sl_reply(\"488\", \"Blocked due to invalid or missing STIR/SHAKEN Identity Header\");\n            exit;\n        }\n        xlog(\"L_INFO\", \"Shaken Identity is invalid\\n\");\n        uac_replace_from(\"$sel(cfg_get.stir_shaken.stir_shaken_prefix_invalid)  $fU\",\"\");\n    }\n}\n\nroute[STIRSHAKEN_OUTBOUND] {\n    # Only route if request is coming from an Endpoint\n    # and is an INVITE\n    if (isbflagset(FLB_SRC_CARRIER)) {\n        return;\n    }\n    if (!is_method(\"INVITE\")) {\n        return;\n    }\n\n    xlog(\"L_INFO\", \"STIRSHAKEN Outbound Logic - method: $rm\");\n\n    # get the outbound caller id number\n    $var(caller_from_number) = $fU;\n\n    if (1 == stirshaken_add_identity_with_key($sel(cfg_get.stir_shaken.stir_shaken_cert_url ), \"A\", $fU, $rU, \"\" , $sel(cfg_get.stir_shaken.stir_shaken_key_path) )) {\n        xlog(\"L_INFO\", \"Shaken authentication added (SIP Identity Header created)\\n\");\n    }\n    else {\n        xlog(\"L_INFO\", \"Failed to add identity\\n\");\n    }\n}\n"
  },
  {
    "path": "kamailio/configs/tls.cfg",
    "content": "#=======================================================\n# This is the default server domain profile.\n# Settings in this domain will be used for all incoming\n# connections that do not match any other server\n# domain in this configuration file.\n#\n# We do not enable anything else than TLSv1.2+\n# over the public internet. Clients do not have\n# to present client certificates by default.\n#=======================================================\n[server:default]\nmethod = TLSv1.2+\nverify_certificate = yes\nrequire_certificate = yes\nprivate_key = /etc/dsiprouter/certs/dsiprouter-key.pem\ncertificate = /etc/dsiprouter/certs/dsiprouter-cert.pem\nca_list = /etc/dsiprouter/certs/ca-list.pem\nca_path = /etc/dsiprouter/certs/ca\n#crl = /etc/dsiprouter/certs/crl.pem\n\n#========== webrtc_ipv4_start ==========#\n#[server:127.0.0.1:4443]\n#method = TLSv1.2+\n#verify_certificate = no\n#require_certificate = no\n#private_key = /etc/dsiprouter/certs/dsiprouter-key.pem\n#certificate = /etc/dsiprouter/certs/dsiprouter-cert.pem\n#ca_list = /etc/dsiprouter/certs/ca-list.pem\n#crl = /etc/dsiprouter/certs/crl.pem\n#========== webrtc_ipv4_stop ==========#\n\n#========== webrtc_ipv6_start ==========#\n#[server:[::1]:4443]\n#method = TLSv1.2+\n#verify_certificate = no\n#require_certificate = no\n#private_key = /etc/dsiprouter/certs/dsiprouter-key.pem\n#certificate = /etc/dsiprouter/certs/dsiprouter-cert.pem\n#ca_list = /etc/dsiprouter/certs/ca-list.pem\n#crl = /etc/dsiprouter/certs/crl.pem\n#========== webrtc_ipv6_stop ==========#\n\n#=======================================================\n# This is the default client domain profile.\n# Settings in this domain will be used for all outgoing\n# TLS connections that do not match any other\n# client domain in this configuration file.\n# We require that servers present valid certificate.\n#=======================================================\n[client:default]\nmethod = TLSv1.2+\nverify_certificate = yes\nrequire_certificate = yes\nprivate_key = /etc/dsiprouter/certs/dsiprouter-key.pem\ncertificate = /etc/dsiprouter/certs/dsiprouter-cert.pem\nca_list = /etc/dsiprouter/certs/ca-list.pem\nca_path = /etc/dsiprouter/certs/ca\n#crl = /etc/dsiprouter/certs/crl.pem\n\n#=======================================================\n# Other domain profiles may be added here\n#=======================================================\n"
  },
  {
    "path": "kamailio/configs/transnexus.cfg",
    "content": "route[TRANSNEXUS_OUTBOUND] {\n\txlog(\"L_INFO\", \"Transnexus Outbound Logic - original: $var(orig_rU)\");\n\n\t# Return if emergency route\n\tif ($var(orig_rU) =~ $sel(cfg_get.server.emergency_numbers)) {\n\t\treturn;\n\t}\n\n\t# Only route if request is coming from an Endpoint\n\t# and is an INVITE\n\n\tif (isbflagset(FLB_SRC_CARRIER)) {\n\t\treturn;\n\t}\n\n\tif (!is_method(\"INVITE\")) {\n\t\treturn;\n\t}\n\n\txlog(\"L_INFO\", \"Transnexus Outbound Logic - method: $rm\");\n\n\t#Store the $ru that was selected\n\t$avp(dr_orig_ruri) = $ru;\n\t$avp(dr_orig_du) = $du;\n\n\t#Send call to clearip server\n\t$ru = \"sip:\" + $rU + \"@\" + $sel(cfg_get.transnexus.authservice_host) + \";transport=tcp\";\n\t$du = $ru;\n\n\t#Set a failure route to get the 302\n\tif (is_method(\"INVITE\")) {\n\t\tt_on_reply(\"TRANSNEXUS_OUTBOUND_REPLY\");\n\t\tt_on_failure(\"TRANSNEXUS_OUTBOUND_CARRIER\");\n\t}\n}\n\nfailure_route[TRANSNEXUS_OUTBOUND_CARRIER] {\n\tif (t_check_status(\"302|403|503\")) {\n\t\txlog(\"L_INFO\", \"Transnexus Outbound Failure Logic\");\n\n\t\t#Append Identity Header\n\t\tif ($avp(Identity) != \"\") {\n\t\t\tappend_hf(\"Identity: $avp(Identity)\\r\\n\");\n\t\t}\n\n\t\t#Check if contact contains\n\t\tif ((int)$sel(cfg_get.transnexus.authservice_lrn_enabled)) {\n\t\t\t#Remove the main branch\n\t\t\t$ru = $null;\n\n\t\t\t#Get Redirects from Transnexus\n\t\t\tget_redirects(\"*\");\n\n\t\t\t#Try one contact at a time\n\t\t\tif (!t_load_contacts()) {\n\t\t\t\tt_reply(\"500\", \"Server Internal Error - Cannot load contacts from TransNexus. Check TransNexus config!\");\n\t\t\t\texit;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tt_next_contacts();\n\t\t\t\tt_on_failure(\"TRANSNEXUS_OUTBOUND_CARRIER_NEXT\");\n\t\t\t\tt_relay();\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\t#Send call outbound using dSIPRouter Routes\n\t\t\t$ru = $avp(dr_orig_ruri);\n\t\t\t$du = $avp(dr_orig_du);\n\t\t\tt_relay();\n\t\t\texit;\n\t\t}\n\t}\n}\n\nfailure_route[TRANSNEXUS_OUTBOUND_CARRIER_NEXT] {\n\tif (!t_next_contacts()) {\n\t\tt_reply(\"408\", \"Request Timeout\");\n\t}\n\telse {\n\t\tt_on_failure(\"TRANSNEXUS_OUTBOUND_CARRIER_NEXT\");\n\t\tt_relay();\n\t}\n}\n\nonreply_route[TRANSNEXUS_OUTBOUND_REPLY] {\n\tif (t_check_status(\"302|403|503\")) {\n\t\t#Get Identity header and append it to header.  Look for Identity or X-Identity\n\t\tif ($hdr(Identity) != \"\") {\n\t\t\t$avp(Identity) = $hdr(Identity);\n\t\t}\n\t\telse if ($hdr(X-Identity) != \"\") {\n\t\t\t$avp(Identity) = $hdr(X-Identity);\n\t\t}\n\n\t\tif ($avp(Identity) != \"\") {\n\t\t\txlog(\"L_INFO\", \"Identity Header Found: $avp(Identity),$hdr(Reason)\");\n\t\t}\n\t\telse {\n\t\t\txlog(\"L_INFO\", \"Identity Header Not Found\");\n\t\t}\n\t}\n\n\tif (is_method(\"BYE\")) {\n\t\txlog(\"L_INFO\", \"*******TRANSNEXUS_OUTBOUND_REPLY BYE*******\");\n\t}\n}\n\nonreply_route[TRANSNEXUS_CARRIER_REPLY] {\n\txlog(\"L_INFO\", \"*******TRANSNEXUS_CARRIER_REPLY*******\");\n}\n\nroute[TRANSNEXUS_INBOUND] {\n\t # Only route if request is coming from an Endpoint\n\tif (!isbflagset(FLB_SRC_CARRIER)) {\n\t\txlog(\"L_INFO\", \"[TRANSNEXUS_INBOUND] <$ci> $si not allowed to talk with Transnexus \\n\");\n\t\treturn;\n\t}\n\n\txlog(\"L_INFO\", \"[TRANSNEXUS_INBOUND] Transnexus Inbound Logic\");\n\n\t#Store the $ru that was selected\n\t$avp(dr_orig_ruri) = $ru;\n\n\t#Send call to clearip server for validation\n\t$ru = \"sip:\" + $rU + \"@\" + $sel(cfg_get.transnexus.verifyservice_host) + \";transport=tcp\";\n\n\txlog(\"L_INFO\", \"[TRANSNEXUS_INBOUND] ru = $ru\");\n\t#Set a failure route to get the 302\n\tif (is_method(\"INVITE\")) {\n\t\tt_on_reply(\"TRANSNEXUS_INBOUND_REPLY\");\n\t\tt_on_failure(\"TRANSNEXUS_INBOUND_FAILURE\");\n\t}\n\n\tt_relay();\n\texit;\n}\n\nonreply_route[TRANSNEXUS_INBOUND_REPLY] {\n\tif (t_check_status(\"302\")) {\n\t\t#Get Identity header and append it to header\n\t\txlog(\"L_INFO\", \"Identity Header: $hdr(Identity),$hdr(Reason)\");\n\t\t$avp(X-P-Asserted-Identity) = $hdr(P-Asserted-Identity);\n\t}\n}\n\nfailure_route[TRANSNEXUS_INBOUND_FAILURE] {\n\tif (t_check_status(\"302\")) {\n\t\txlog(\"L_INFO\", \"Transnexus Inbound Failure Logic\");\n\t\tt_drop_replies();\n\t\tsend_reply(\"100\", \"Trying\");\n\t\tappend_hf(\"X-P-Asserted-Identity: $avp(X-P-Asserted-Identity)\\r\\n\");\n\n\t\t# Replace with the original URI\n\t\t$ru = $avp(dr_orig_ruri);\n\n\t\t# Import custom logic to route the inbound call\n\t\timport_file \"transnexus_inbound_custom.cfg\"\n\n\t\troute(NEXTHOP);\n\t\treturn;\n\t}\n}\n\n"
  },
  {
    "path": "kamailio/debian/10.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    local KAM_SOURCES_LIST=\"/etc/apt/sources.list.d/kamailio.list\"\n    local KAM_PREFS_CONF=\"/etc/apt/preferences.d/kamailio.pref\"\n    local NPROC=$(nproc)\n\n    # nf_tables is the default fw on debian but it has too many bugs at this time\n    # instead we will use legacy iptables until these issues are ironed out\n    update-alternatives --set iptables /usr/sbin/iptables-legacy\n    update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy\n\n    # Install Dependencies and remove any conflicting packages\n    apt-get remove -y ufw &&\n    apt-get install -y curl wget sed gawk vim perl uuid-dev libssl-dev logrotate rsyslog \\\n        libcurl4-openssl-dev libjansson-dev cmake build-essential pkg-config \\\n        zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libsqlite3-dev libreadline-dev \\\n        libffi-dev libbz2-dev libpq-dev libev-dev uuid-runtime tar dpkg &&\n    apt-get install -t bullseye -y firewalld python3 python3-venv\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # we need a newer version of certbot than the distro repos offer\n    apt-get remove -y *certbot*\n    python3 -m venv /opt/certbot/\n    /opt/certbot/bin/pip install --upgrade pip\n    /opt/certbot/bin/pip install certbot\n    ln -sf /opt/certbot/bin/certbot /usr/bin/certbot\n\n    # create kamailio user and group\n    mkdir -p /var/run/kamailio\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    # allow root to fix permissions before starting services (required to work with SELinux enabled)\n    usermod -a -G kamailio root\n\n    # add repo sources to apt\n    mkdir -p /etc/apt/sources.list.d\n    (cat << EOF\n# kamailio repo's\ndeb https://deb-archive.kamailio.org/repos/kamailio-${KAM_VERSION} buster main\n#deb-src https://deb-archive.kamailio.org/repos/kamailio-${KAM_VERSION} buster main\nEOF\n    ) > ${KAM_SOURCES_LIST}\n\n    # give higher precedence to packages from kamailio repo\n    mkdir -p /etc/apt/preferences.d\n    (cat << 'EOF'\nPackage: *\nPin: origin deb-archive.kamailio.org\nPin-Priority: 1000\nEOF\n    ) > ${KAM_PREFS_CONF}\n\n    # Add Key for Kamailio Repo\n    wget -O- https://deb-archive.kamailio.org/kamailiodebkey.gpg | apt-key add -\n\n    # Update repo sources cache\n    apt-get update -y\n\n    # Install Kamailio packages\n    ## the kamailio deb must be updated so we don't overwrite the compiled python version\n    (\n        TMP_DIR=$(mktemp -d) &&\n        cd /tmp &&\n        apt-get download kamailio &&\n        KAM_DEB=$(ls kamailio_*.deb) &&\n        dpkg-deb -R $KAM_DEB $TMP_DIR &&\n        rm -f $KAM_DEB &&\n        perl -i -pe 's%(Depends: .*)python3, (.*)%\\1\\2%g' $TMP_DIR/DEBIAN/control &&\n        dpkg-deb -b $TMP_DIR kamailio.deb &&\n        apt-get install -y ./kamailio.deb &&\n        rm -f kamailio.deb\n    ) || {\n        printerr 'Failed installing kamailio from packages'\n        return 1\n    }\n    apt-get install -y --allow-downgrades kamailio-mysql-modules kamailio-extra-modules kamailio-tls-modules \\\n        kamailio-websocket-modules kamailio-presence-modules kamailio-json-modules \\\n        kamailio-sctp-modules\n\n    if (( $? != 0 )); then\n        printerr \"failed installing kamailio modules from packages\"\n        return 1\n    fi\n\n    # get info about the kamailio install for later use in script\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n    # create kamailio tmp files\n    echo \"d /run/kamailio 0750 kamailio kamailio\" > /etc/tmpfiles.d/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR}\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    (cat << EOF\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n#STORE_PLAINTEXT_PW=0\nEOF\n    ) > ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create || {\n        printerr 'Failed creating kamailio database'\n        return 1\n    }\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Configure Kamailio systemd service\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio-v2.service /lib/systemd/system/kamailio.service\n    chmod 644 /lib/systemd/system/kamailio.service\n    systemctl daemon-reload\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-certificates.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libjwt (version in repos is too old)\n    if [[ ! -d ${SRC_DIR}/libjwt ]]; then\n        git clone --depth 1 -c advice.detachedHead=false -b v2.1.1 https://github.com/devopsec/libjwt.git ${SRC_DIR}/libjwt\n    fi\n    (\n        cd ${SRC_DIR}/libjwt &&\n        autoreconf -i &&\n        ./configure --prefix=/usr &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libjwt'\n        return 1\n    }\n\n    ## compile and install libks\n    if [[ ! -d ${SRC_DIR}/libks ]]; then\n        git clone --single-branch -c advice.detachedHead=false https://github.com/signalwire/libks -b v1.8.3 ${SRC_DIR}/libks\n    fi\n    (\n        cd ${SRC_DIR}/libks &&\n        cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release . &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libks'\n        return 1\n    }\n\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    (\n        # TODO: commit updates to upstream to fix EVP_PKEY_cmp being deprecated\n        cd ${SRC_DIR}/libstirshaken &&\n        ./bootstrap.sh &&\n        ./configure --prefix=/usr CFLAGS='-Wno-deprecated-declarations' LDFLAGS='-L/usr/lib' &&\n        make -j $NPROC &&\n        make -j $NPROC install &&\n        ldconfig\n    ) || {\n        printerr 'Failed to compile and install libstirshaken'\n        return 1\n    }\n\n    ## compile and install STIR/SHAKEN module\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/stirshaken &&\n        make -j $NPROC\n    ) &&\n    cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/ || {\n        printerr 'Failed to compile and install STIR/SHAKEN module'\n        return 1\n    }\n\n    # patch uac module to support reload_delta\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/uac &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/uac.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/uac/uac.so ${KAM_MODULES_DIR}/\n    ) || {\n        printerr 'Failed to patch uac module'\n        return 1\n    }\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop and disable services\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    cp -rf ${SYSTEM_KAMAILIO_CONFIG_DIR}/. ${BACKUPS_DIR}/kamailio/\n    rm -rf ${SYSTEM_KAMAILIO_CONFIG_DIR}\n\n    # Uninstall Stirshaken Required Packages\n    ( cd ${SRC_DIR}/libjwt; make uninstall; exit $?; ) && rm -rf ${SRC_DIR}/libjwt\n    ( cd ${SRC_DIR}/libks; make uninstall; exit $?; ) && rm -rf ${SRC_DIR}/libks\n    ( cd ${SRC_DIR}/libstirshaken; make uninstall;exit $?; ) && rm -rf ${SRC_DIR}/libstirshaken\n    rm -rf ${SRC_DIR}/kamailio\n\n    # Uninstall Kamailio modules\n    apt-get -y remove --purge kamailio\\*\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "kamailio/debian/11.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local KAM_SOURCES_LIST=\"/etc/apt/sources.list.d/kamailio.list\"\n    local KAM_PREFS_CONF=\"/etc/apt/preferences.d/kamailio.pref\"\n    local NPROC=$(nproc)\n\n    # nf_tables is the default fw on debian but it has too many bugs at this time\n    # instead we will use legacy iptables until these issues are ironed out\n    update-alternatives --set iptables /usr/sbin/iptables-legacy\n    update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy\n\n    # Install Dependencies and remove any conflicting packages\n    apt-get remove -y ufw &&\n    apt-get install -y curl wget sed gawk vim perl uuid-dev libssl-dev logrotate rsyslog \\\n        libcurl4-openssl-dev libjansson-dev cmake firewalld python3 build-essential python3-venv\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # we need a newer version of certbot than the distro repos offer\n    apt-get remove -y *certbot*\n    python3 -m venv /opt/certbot/\n    /opt/certbot/bin/pip install --upgrade pip\n    /opt/certbot/bin/pip install certbot\n    ln -sf /opt/certbot/bin/certbot /usr/bin/certbot\n\n    # create kamailio user and group\n    mkdir -p /var/run/kamailio\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel kamailio &>/dev/null; groupdel kamailio &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    # allow root to fix permissions before starting services (required to work with SELinux enabled)\n    usermod -a -G kamailio root\n\n    # add repo sources to apt\n    mkdir -p /etc/apt/sources.list.d\n    (cat << EOF\n# kamailio repo's\ndeb https://deb-archive.kamailio.org/repos/kamailio-${KAM_VERSION} bullseye main\n#deb-src https://deb-archive.kamailio.org/repos/kamailio-${KAM_VERSION} bullseye main\nEOF\n    ) > ${KAM_SOURCES_LIST}\n\n    # give higher precedence to packages from kamailio repo\n    mkdir -p /etc/apt/preferences.d\n    (cat << 'EOF'\nPackage: *\nPin: origin deb-archive.kamailio.org\nPin-Priority: 1000\nEOF\n    ) > ${KAM_PREFS_CONF}\n\n    # Add Key for Kamailio Repo\n    wget -O- https://deb-archive.kamailio.org/kamailiodebkey.gpg | apt-key add -\n\n    # Update repo sources cache\n    apt-get update -y\n\n    # Install Kamailio packages\n    apt-get install -y --allow-downgrades kamailio kamailio-mysql-modules kamailio-extra-modules \\\n        kamailio-tls-modules kamailio-websocket-modules kamailio-presence-modules \\\n        kamailio-json-modules kamailio-sctp-modules\n\n    # get info about the kamailio install for later use in script\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n    # create kamailio tmp files\n    echo \"d /run/kamailio 0750 kamailio kamailio\" > /etc/tmpfiles.d/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR} ${BACKUPS_DIR}/kamailio\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${BACKUPS_DIR}/kamailio/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    (cat << EOF\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n#STORE_PLAINTEXT_PW=0\nEOF\n    ) > ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create || {\n        printerr 'Failed creating kamailio database'\n        return 1\n    }\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Configure Kamailio systemd service\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio-v2.service /lib/systemd/system/kamailio.service\n    chmod 644 /lib/systemd/system/kamailio.service\n    systemctl daemon-reload\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-certificates.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libjwt (version in repos is too old)\n    if [[ ! -d ${SRC_DIR}/libjwt ]]; then\n        git clone --depth 1 -c advice.detachedHead=false -b v2.1.1 https://github.com/devopsec/libjwt.git ${SRC_DIR}/libjwt\n    fi\n    (\n        cd ${SRC_DIR}/libjwt &&\n        autoreconf -i &&\n        ./configure --prefix=/usr &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libjwt'\n        return 1\n    }\n\n    ## compile and install libks\n    if [[ ! -d ${SRC_DIR}/libks ]]; then\n        git clone --single-branch -c advice.detachedHead=false https://github.com/signalwire/libks -b v1.8.3 ${SRC_DIR}/libks\n    fi\n    (\n        cd ${SRC_DIR}/libks &&\n        cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release . &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libks'\n        return 1\n    }\n\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    (\n        # TODO: commit updates to upstream to fix EVP_PKEY_cmp being deprecated\n        cd ${SRC_DIR}/libstirshaken &&\n        ./bootstrap.sh &&\n        ./configure --prefix=/usr CFLAGS='-Wno-deprecated-declarations' LDFLAGS='-L/usr/lib' &&\n        make -j $NPROC &&\n        make -j $NPROC install &&\n        ldconfig\n    ) || {\n        printerr 'Failed to compile and install libstirshaken'\n        return 1\n    }\n\n    ## compile and install STIR/SHAKEN module\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/stirshaken &&\n        make -j $NPROC\n    ) &&\n    cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/ || {\n        printerr 'Failed to compile and install STIR/SHAKEN module'\n        return 1\n    }\n\n    # patch uac module to support reload_delta\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/uac &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/uac.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/uac/uac.so ${KAM_MODULES_DIR}/\n    ) || {\n        printerr 'Failed to patch uac module'\n        return 1\n    }\n\n    return 0\n}\n\nfunction uninstall() {\n    # Stop and disable services\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    cp -rf ${SYSTEM_KAMAILIO_CONFIG_DIR}/. ${BACKUPS_DIR}/kamailio/\n    rm -rf ${SYSTEM_KAMAILIO_CONFIG_DIR}\n\n    # Uninstall Stirshaken Required Packages\n    ( cd ${SRC_DIR}/libjwt; make uninstall; exit $?; ) && rm -rf ${SRC_DIR}/libjwt\n    ( cd ${SRC_DIR}/libks; make uninstall; exit $?; ) && rm -rf ${SRC_DIR}/libks\n    ( cd ${SRC_DIR}/libstirshaken; make uninstall;exit $?; ) && rm -rf ${SRC_DIR}/libstirshaken\n    rm -rf ${SRC_DIR}/kamailio\n\n    # Uninstall Kamailio modules\n    apt-get -y remove --purge kamailio\\*\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "kamailio/debian/12.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local KAM_SOURCES_LIST=\"/etc/apt/sources.list.d/kamailio.list\"\n    local KAM_PREFS_CONF=\"/etc/apt/preferences.d/kamailio.pref\"\n    local NPROC=$(nproc)\n\n    # Install Dependencies and remove any conflicting packages\n    { dpkg -l ufw &>/dev/null && apt-get remove -y ufw || :; } &&\n    apt-get install -y curl wget sed gawk vim perl uuid-dev libssl-dev logrotate rsyslog \\\n        libcurl4-openssl-dev libjansson-dev cmake firewalld build-essential certbot \\\n        pkg-config dh-autoreconf\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # Configure OpenSSL to a default provider\n    sed -i -e 's/# providers =/providers =/' -e 's/# \\[provider_sect/\\[provider_sect/' -e 's/# default =/default =/' -e 's/# \\[default_sect/\\[default_sect/' -e 's/# activate/activate/' /etc/ssl/openssl.cnf\n\n    # create kamailio user and group\n    mkdir -p /var/run/kamailio\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel kamailio &>/dev/null; groupdel kamailio &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    # allow root to fix permissions before starting services (required to work with SELinux enabled)\n    usermod -a -G kamailio root\n\n    # add repo sources to apt\n    mkdir -p /etc/apt/sources.list.d\n    (cat << EOF\n# kamailio repo's\ndeb https://deb-archive.kamailio.org/repos/kamailio-${KAM_VERSION} bookworm main\n#deb-src https://deb-archive.kamailio.org/repos/kamailio-${KAM_VERSION} bookworm main\nEOF\n    ) > ${KAM_SOURCES_LIST}\n\n    # give higher precedence to packages from kamailio repo\n    mkdir -p /etc/apt/preferences.d\n    (cat << 'EOF'\nPackage: *\nPin: origin deb-archive.kamailio.org\nPin-Priority: 1000\nEOF\n    ) > ${KAM_PREFS_CONF}\n\n    # Add Key for Kamailio Repo\n    curl -s https://deb.kamailio.org/kamailiodebkey.gpg | gpg --dearmor >/etc/apt/trusted.gpg.d/kamailiodebkey.gpg\n\n    # Update repo sources cache\n    apt-get update -y\n\n    \n    # Install Kamailio packages\n    apt-get install -y --allow-downgrades kamailio kamailio-mysql-modules kamailio-extra-modules \\\n        kamailio-tls-modules kamailio-websocket-modules kamailio-presence-modules \\\n        kamailio-json-modules kamailio-sctp-modules\n\n    # get info about the kamailio install for later use in script\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n    # create kamailio tmp files\n    echo \"d /run/kamailio 0750 kamailio kamailio\" > /etc/tmpfiles.d/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR} ${BACKUPS_DIR}/kamailio\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${BACKUPS_DIR}/kamailio/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    (cat << EOF\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTHOST=\"${ROOT_DB_HOST}\"\nDBROOTPORT=\"${ROOT_DB_PORT}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nEXTRA_MODULES=\"imc cpl siptrace domainpolicy carrierroute drouting userblocklist htable purple uac pipelimit mtree sca mohqueue rtpproxy rtpengine secfilter\"\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n#STORE_PLAINTEXT_PW=0\nEOF\n    ) > ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\n\n    # in mariadb ver >= 10.6.1 --port= now defaults to transport=tcp\n    # we want socket connections for root as default so apply our patch to kamdbctl\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd /usr/lib/x86_64-linux-gnu/kamailio/kamctl &&\n        patch -p3 -N <${DSIP_PROJECT_DIR}/kamailio/kamdbctl.patch\n    )\n    if (( $? > 1 )); then\n        printerr 'Failed patching kamdbctl'\n        return 1\n    fi\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create || {\n        printerr 'Failed creating kamailio database'\n        return 1\n    }\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=22/tcp --permanent\n    firewall-cmd --reload\n\n    systemctl start firewalld\n\n    # Configure Kamailio systemd service\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio-v2.service /lib/systemd/system/kamailio.service\n    chmod 644 /lib/systemd/system/kamailio.service\n    systemctl daemon-reload\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-certificates.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libjwt (version in repos is too old)\n    if [[ ! -d ${SRC_DIR}/libjwt ]]; then\n        git clone --depth 1 -c advice.detachedHead=false -b v2.1.1 https://github.com/devopsec/libjwt.git ${SRC_DIR}/libjwt\n    fi\n    (\n        cd ${SRC_DIR}/libjwt &&\n        autoreconf -i &&\n        ./configure --prefix=/usr &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libjwt'\n        return 1\n    }\n\n    ## compile and install libks\n    if [[ ! -d ${SRC_DIR}/libks ]]; then\n        git clone --single-branch -c advice.detachedHead=false https://github.com/signalwire/libks -b v1.8.3 ${SRC_DIR}/libks\n    fi\n    (\n        cd ${SRC_DIR}/libks &&\n        cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release . &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libks'\n        return 1\n    }\n\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    (\n        # TODO: commit updates to upstream to fix EVP_PKEY_cmp being deprecated\n        cd ${SRC_DIR}/libstirshaken &&\n        ./bootstrap.sh &&\n        ./configure --prefix=/usr CFLAGS='-Wno-deprecated-declarations' LDFLAGS='-L/usr/lib' &&\n        make -j $NPROC &&\n        make -j $NPROC install &&\n        ldconfig\n    ) || {\n        printerr 'Failed to compile and install libstirshaken'\n        return 1\n    }\n\n    ## compile and install STIR/SHAKEN module\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/stirshaken &&\n        make -j $NPROC\n    ) &&\n    cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/ || {\n        printerr 'Failed to compile and install STIR/SHAKEN module'\n        return 1\n    }\n\n    # patch uac module to support reload_delta\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/uac &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/uac.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/uac/uac.so ${KAM_MODULES_DIR}/\n    ) || {\n        printerr 'Failed to patch uac module'\n        return 1\n    }\n\n    return 0\n}\n\nfunction uninstall() {\n    # Stop and disable services\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    cp -rf ${SYSTEM_KAMAILIO_CONFIG_DIR}/. ${BACKUPS_DIR}/kamailio/\n    rm -rf ${SYSTEM_KAMAILIO_CONFIG_DIR}\n\n    # Uninstall Stirshaken Required Packages\n    ( cd ${SRC_DIR}/libjwt; make uninstall; exit $?; ) && rm -rf ${SRC_DIR}/libjwt\n    ( cd ${SRC_DIR}/libks; make uninstall; exit $?; ) && rm -rf ${SRC_DIR}/libks\n    ( cd ${SRC_DIR}/libstirshaken; make uninstall;exit $?; ) && rm -rf ${SRC_DIR}/libstirshaken\n    rm -rf ${SRC_DIR}/kamailio\n\n    # Uninstall Kamailio modules\n    apt-get -y remove --purge kamailio\\*\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "kamailio/debian/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    local KAM_SOURCES_LIST=\"/etc/apt/sources.list.d/kamailio.list\"\n    local KAM_PREFS_CONF=\"/etc/apt/preferences.d/kamailio.pref\"\n\n    # Install Dependencies and remove any conflicting packages\n    apt-get remove -y ufw &&\n    apt-get install -y curl wget sed gawk vim perl uuid-dev libssl-dev logrotate rsyslog \\\n        libcurl4-openssl-dev libjansson-dev cmake firewalld python3 python3-venv &&\n    apt-get install -y -t buster build-essential\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    # we need a newer version of certbot than the distro repos offer\n    apt-get remove -y *certbot*\n    python3 -m venv /opt/certbot/\n    /opt/certbot/bin/pip install --upgrade pip\n    /opt/certbot/bin/pip install certbot\n    ln -sf /opt/certbot/bin/certbot /usr/bin/certbot\n\n    # create kamailio user and group\n    mkdir -p /var/run/kamailio\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    # allow root to fix permissions before starting services (required to work with SELinux enabled)\n    usermod -a -G kamailio root\n\n    # add repo sources to apt\n    mkdir -p /etc/apt/sources.list.d\n    (cat << EOF\n# kamailio repo's\ndeb https://deb-archive.kamailio.org/repos/kamailio-${KAM_VERSION} stretch main\n#deb-src https://deb-archive.kamailio.org/repos/kamailio-${KAM_VERSION} stretch main\nEOF\n    ) > ${KAM_SOURCES_LIST}\n\n    # give higher precedence to packages from kamailio repo\n    mkdir -p /etc/apt/preferences.d\n    (cat << 'EOF'\nPackage: *\nPin: origin deb-archive.kamailio.org\nPin-Priority: 1000\nEOF\n    ) > ${KAM_PREFS_CONF}\n\n    # Add Key for Kamailio Repo\n    wget -O- https://deb-archive.kamailio.org/kamailiodebkey.gpg | apt-key add -\n\n    # Update repo sources cache\n    apt-get update -y\n\n    # Install Kamailio packages\n    apt-get install -y --allow-downgrades kamailio kamailio-mysql-modules kamailio-extra-modules kamailio-tls-modules \\\n        kamailio-websocket-modules kamailio-presence-modules kamailio-json-modules\n\n    # get info about the kamailio install for later use in script\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n    # create kamailio tmp files\n    echo \"d /run/kamailio 0750 kamailio kamailio\" > /etc/tmpfiles.d/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR}\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    (cat << EOF\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n#STORE_PLAINTEXT_PW=0\nEOF\n    ) > ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create || {\n        printerr 'Failed creating kamailio database'\n        return 1\n    }\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Configure Kamailio systemd service\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio-v1.service /lib/systemd/system/kamailio.service\n    chmod 644 /lib/systemd/system/kamailio.service\n    systemctl daemon-reload\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-certificates.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n    # setup dSIPRouter module for kamailio\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libjwt\n    if [[ ! -d ${SRC_DIR}/libjwt ]]; then\n        git clone --depth 1 -c advice.detachedHead=false -b v2.1.1 https://github.com/devopsec/libjwt.git ${SRC_DIR}/libjwt\n    fi\n\n    (\n        cd ${SRC_DIR}/libjwt &&\n        autoreconf -i &&\n        ./configure --prefix=/usr &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libjwt'\n        return 1\n    }\n\n    ## compile and install libks\n    if [[ ! -d ${SRC_DIR}/libks ]]; then\n        git clone --single-branch -c advice.detachedHead=false https://github.com/signalwire/libks -b v1.8.3 ${SRC_DIR}/libks\n    fi\n    ( cd ${SRC_DIR}/libks && cmake -DCMAKE_INSTALL_PREFIX=/usr . && make install; exit $?; ) ||\n    { printerr 'Failed to compile and install libks'; return 1; }\n\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    ( cd ${SRC_DIR}/libstirshaken && ./bootstrap.sh && ./configure --prefix=/usr &&\n        make && make install && ldconfig; exit $?;\n    ) || { printerr 'Failed to compile and install libstirshaken'; return 1; }\n\n    ## compile and install STIR/SHAKEN module\n    ( cd ${SRC_DIR}/kamailio/src/modules/stirshaken && make; exit $?; ) &&\n    cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/ ||\n    { printerr 'Failed to compile and install STIR/SHAKEN module'; return 1; }\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop and disable services\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    cp -rf ${SYSTEM_KAMAILIO_CONFIG_DIR}/. ${BACKUPS_DIR}/kamailio/\n    rm -rf ${SYSTEM_KAMAILIO_CONFIG_DIR}\n\n    # Uninstall Stirshaken Required Packages\n    ( cd ${SRC_DIR}/libjwt; make uninstall; exit $?; ) && rm -rf ${SRC_DIR}/libjwt\n    ( cd ${SRC_DIR}/libks; make uninstall; exit $?; ) && rm -rf ${SRC_DIR}/libks\n    ( cd ${SRC_DIR}/libstirshaken; make uninstall;exit $?; ) && rm -rf ${SRC_DIR}/libstirshaken\n    rm -rf ${SRC_DIR}/kamailio\n\n    # Uninstall Kamailio modules\n    apt-get -y remove --purge kamailio\\*\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "kamailio/defaults/address.csv",
    "content": "1;FLT_MSTEAMS;52.114.132.46;32;0;name:msteams-sbc,gwgroup:0\n2;FLT_MSTEAMS;52.114.14.70;32;0;name:msteams-sbc,gwgroup:0\n3;FLT_MSTEAMS;52.114.148.0;32;0;name:msteams-sbc,gwgroup:0\n4;FLT_MSTEAMS;52.114.16.74;32;0;name:msteams-sbc,gwgroup:0\n5;FLT_MSTEAMS;52.114.20.29;32;0;name:msteams-sbc,gwgroup:0\n6;FLT_MSTEAMS;52.114.7.24;32;0;name:msteams-sbc,gwgroup:0\n7;FLT_MSTEAMS;52.114.75.24;32;0;name:msteams-sbc,gwgroup:0\n8;FLT_MSTEAMS;52.114.76.76;32;0;name:msteams-sbc,gwgroup:0\n9;FLT_MSTEAMS;52.127.64.33;32;0;name:msteams-sbc,gwgroup:0\n10;FLT_MSTEAMS;52.127.64.34;32;0;name:msteams-sbc,gwgroup:0\n11;FLT_MSTEAMS;52.127.88.59;32;0;name:msteams-sbc,gwgroup:0\n12;FLT_MSTEAMS;52.127.92.64;32;0;name:msteams-sbc,gwgroup:0\n13;FLT_MSTEAMS;sip.pstnhub.microsoft.com;32;0;name:msteams-sbc,gwgroup:0\n14;FLT_MSTEAMS;sip2.pstnhub.microsoft.com;32;0;name:msteams-sbc,gwgroup:0\n15;FLT_MSTEAMS;sip3.pstnhub.microsoft.com;32;0;name:msteams-sbc,gwgroup:0\n16;FLT_MSTEAMS;sip.pstnhub.dod.teams.microsoft.us;32;0;name:msteams-sbc,gwgroup:0\n17;FLT_MSTEAMS;sip.pstnhub.gov.teams.microsoft.us;32;0;name:msteams-sbc,gwgroup:0\n18;FLT_CARRIER;54.172.60.0;32;0;name:Twilio NA Inbound Carrier,gwgroup:1\n19;FLT_CARRIER;54.172.60.1;32;0;name:Twilio NA Inbound Carrier,gwgroup:1\n20;FLT_CARRIER;54.172.60.2;32;0;name:Twilio NA Inbound Carrier,gwgroup:1\n21;FLT_CARRIER;54.172.60.3;32;0;name:Twilio NA Inbound Carrier,gwgroup:1\n22;FLT_CARRIER;54.244.51.0;32;0;name:Twilio NA Inbound Carrier,gwgroup:1\n23;FLT_CARRIER;54.244.51.1;32;0;name:Twilio NA Inbound Carrier,gwgroup:1\n24;FLT_CARRIER;54.244.51.2;32;0;name:Twilio NA Inbound,gwgroup:1\n25;FLT_CARRIER;54.244.51.3;32;0;name:Twilio NA Inbound Carrier,gwgroup:1\n26;FLT_CARRIER;54.244.60.0;23;0;name:Twilio NA Inbound Carrier,gwgroup:1\n27;FLT_CARRIER;34.203.250.0;23;0;name:Twilio NA Inbound Carrier,gwgroup:1\n28;FLT_CARRIER;54.244.51.0;24;0;name:Twilio NA Inbound Carrier,gwgroup:1\n29;FLT_CARRIER;52.41.52.34;32;0;name:Skyetel North West Inbound,gwgroup:2\n30;FLT_CARRIER;52.8.201.128;32;0;name:Skyetel South West Inbound,gwgroup:2\n31;FLT_CARRIER;52.60.138.31;32;0;name:Skyetel North East Inbound,gwgroup:2\n32;FLT_CARRIER;50.17.48.216;32;0;name:Skyetel South East Inbound,gwgroup:2\n33;FLT_CARRIER;35.156.192.164;32;0;name:Skyetel Europe Inbound,gwgroup:2\n34;FLT_CARRIER;term.skyetel.com;32;0;name:Skyetel 1st Priority Outbound Call,gwgroup:2\n35;FLT_CARRIER;52.41.52.34;32;0;name:Skyetel 2nd Priority Outbound Call,gwgroup:2\n36;FLT_CARRIER;52.8.201.128;32;0;name:Skyetel 3rd Priority Outbound Call,gwgroup:2\n37;FLT_CARRIER;50.17.48.216;32;0;name:Skyetel 4rd Priority Outbound Call,gwgroup:2\n38;FLT_CARRIER;52.32.223.28;32;0;name:Skyetel North West High Cost Outbound Traffic,gwgroup:2\n39;FLT_CARRIER;52.4.178.107;32;0;name:Skyetel South East High Cost Outbound Traffic,gwgroup:2\n41;FLT_CARRIER;34.210.91.112;28;0;name:Flowroute US-West-OR,gwgroup:3\n43;FLT_CARRIER;34.226.36.32;28;0;name:Flowroute US-East-VA,gwgroup:3\n44;FLT_CARRIER;81.201.82.45;32;0;name:Voxbone Belgium,gwgroup:4\n45;FLT_CARRIER;81.201.84.195;32;0;name:Voxbone LA,gwgroup:4\n46;FLT_CARRIER;81.201.85.45;32;0;name:Voxbone NYC,gwgroup:4\n47;FLT_CARRIER;81.201.83.45;32;0;name:Voxbone Germany,gwgroup:4\n48;FLT_CARRIER;81.201.86.45;32;0;name:Voxbone Hong Kong,gwgroup:4\n49;FLT_CARRIER;81.201.84.195;32;0;name:Voxbone Australia,gwgroup:4\n50;FLT_CARRIER;64.136.174.30;32;0;name:VoIP Innovations Inbound Carrier,gwgroup:5\n51;FLT_CARRIER;64.136.173.22;32;0;name:VoIP Innovations Inbound Carrier,gwgroup:5\n52;FLT_CARRIER;209.166.128.200;32;0;name:VoIP Innovations Inbound Carrier,gwgroup:5\n53;FLT_CARRIER;192.240.151.100;32;0;name:VoIP Innovations Inbound Carrier,gwgroup:5\n54;FLT_CARRIER;64.136.173.31;32;0;name:VoIP Innovations Inbound Carrier,gwgroup:5\n55;FLT_CARRIER;64.136.174.30;32;0;name:VoIP Innovations Inbound Carrier,gwgroup:5\n56;FLT_CARRIER;64.136.174.20;32;0;name:VoIP Innovations Inbound Carrier,gwgroup:5\n57;FLT_CARRIER;209.166.154.70;32;0;name:VoIP Innovations Inbound Carrier,gwgroup:5\n58;FLT_CARRIER;64.136.174.65;32;0;name:VoIP Innovations Inbound Carrier,gwgroup:5\n59;FLT_CARRIER;64.136.173.23;32;0;name:VoIP Innovations Inbound Carrier,gwgroup:5\n60;FLT_CARRIER;209.166.128.201;32;0;name:VoIP Innovations Inbound Carrier,gwgroup:5\n61;FLT_CARRIER;192.240.151.101;32;0;name:VoIP Innovations Inbound Carrier,gwgroup:5\n62;FLT_CARRIER;64.136.173.65;32;0;name:VoIP Innovations Inbound Carrier,gwgroup:5\n63;FLT_CARRIER;64.136.174.65;32;0;name:VoIP Innovations Inbound Carrier,gwgroup:5\n64;FLT_CARRIER;64.136.174.21;32;0;name:VoIP Innovations Inbound Carrier,gwgroup:5\n65;FLT_CARRIER;209.166.154.71;32;0;name:VoIP Innovations Inbound Carrier,gwgroup:5\n66;FLT_CARRIER;64.136.174.30;32;0;name:VoIP Innovations Outbound Conversational Carrier,gwgroup:6\n67;FLT_CARRIER;64.136.173.22;32;0;name:VoIP Innovations Outbound Conversational Carrier,gwgroup:6\n68;FLT_CARRIER;209.166.128.200;32;0;name:VoIP Innovations Outbound Conversational Carrier,gwgroup:6\n69;FLT_CARRIER;192.240.151.100;32;0;name:VoIP Innovations Outbound Conversational Carrier,gwgroup:6\n70;FLT_CARRIER;72.15.219.140;32;0;name:Thinq Carrier,gwgroup:7\n71;FLT_CARRIER;216.147.191.157;32;0;name:Voxtelesys Carrier,gwgroup:8\n72;FLT_CARRIER;64.34.181.47;32;0;name:Les.net Carrier,gwgroup:9\n73;FLT_CARRIER;206.80.250.100;32;0;name:ThinkTel,gwgroup:10\n74;FLT_CARRIER;208.68.17.52;32;0;name:ThinkTel,gwgroup:10\n75;FLT_CARRIER;209.197.130.80;32;0;name:ThinkTel,gwgroup:10\n"
  },
  {
    "path": "kamailio/defaults/address.sql",
    "content": "-- update address schema\nALTER TABLE address\n  MODIFY COLUMN `ip_addr` VARCHAR(253) NOT NULL,\n  MODIFY COLUMN `tag` VARCHAR(255) NOT NULL DEFAULT '';"
  },
  {
    "path": "kamailio/defaults/dispatcher.csv",
    "content": "1,1,sip:54.172.60.0:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Twilio NA-Virginia Carrier;gwid=1\n2,1,sip:54.172.60.1:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Twilio NA-Virginia  Carrier;gwid=2\n3,1,sip:54.172.60.2:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Twilio NA-Virginia Carrier;gwid=3\n4,1,sip:54.172.60.3:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Twilio NA-Virginia Carrier;gwid=4\n5,1,sip:54.244.51.0:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Twilio NA-Oregon Carrier;gwid=5\n6,1,sip:54.244.51.1:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Twilio NA-Oregon  Carrier;gwid=6\n7,1,sip:54.244.51.2:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Twilio NA-Oregon Carrier;gwid=7\n8,1,sip:54.244.51.3:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Twilio NA-Oregon Carrier;gwid=8\n9,2,sip:52.41.52.34:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Skyetel North West Inbound;gwid=9\n10,2,sip:52.8.201.128:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Skyetel South West Inbound;gwid=10\n11,2,sip:52.60.138.31:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Skyetel North East Inbound;gwid=11\n12,2,sip:50.17.48.216:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Skyetel South East Inbound;gwid=12\n13,2,sip:35.156.192.164:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Skyetel Europe Inbound;gwid=13\n14,2,sip:term.skyetel.com:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Skyetel 1st Priority Outbound Call;gwid=14\n15,2,sip:52.41.52.34:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Skyetel 2nd Priority Outbound Call;gwid=15\n16,2,sip:52.8.201.128:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Skyetel 3rd Priority Outbound Call;gwid=16\n17,2,sip:50.17.48.216:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Skyetel 4rd Priority Outbound Call;gwid=17\n18,2,sip:52.32.223.28:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Skyetel North West High Cost Outbound Traffic;gwid=18\n19,2,sip:52.4.178.107:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Skyetel South East High Cost Outbound Traffic;gwid=19\n20,3,sip:147.75.60.160:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Flowroute US-West-WA;gwid=20\n21,3,sip:34.210.91.112:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Flowroute US-West-OR;gwid=21\n22,3,sip:147.75.65.192:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Flowroute US-East-NJ;gwid=22\n23,3,sip:34.226.36.32:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Flowroute US-East-VA;gwid=23\n24,4,sip:81.201.82.45:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Voxbone Belgium;gwid=24\n25,4,sip:81.201.84.195:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Voxbone LA;gwid=25\n26,4,sip:81.201.85.45:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Voxbone NYC;gwid=26\n27,4,sip:81.201.83.45:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Voxbone Germany;gwid=27\n28,4,sip:81.201.86.45:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Voxbone Hong Kong;gwid=28\n29,4,sip:81.201.84.195:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Voxbone Australia;gwid=29\n30,5,sip:64.136.174.30:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Inbound Carrier;gwid=30\n31,5,sip:64.136.173.22:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Inbound Carrier;gwid=31\n32,5,sip:209.166.128.200:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Inbound Carrier;gwid=32\n33,5,sip:192.240.151.100:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Inbound Carrier;gwid=33\n34,5,sip:64.136.173.31:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Inbound Carrier;gwid=34\n35,5,sip:64.136.174.30:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Inbound Carrier;gwid=35\n36,5,sip:64.136.174.20:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Inbound Carrier;gwid=36\n37,5,sip:209.166.154.70:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Inbound Carrier;gwid=37\n38,5,sip:64.136.174.65:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Inbound Carrier;gwid=38\n39,5,sip:64.136.173.23:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Inbound Carrier;gwid=39\n40,5,sip:209.166.128.201:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Inbound Carrier;gwid=40\n41,5,sip:192.240.151.101:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Inbound Carrier;gwid=41\n42,5,sip:64.136.173.65:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Inbound Carrier;gwid=42\n43,5,sip:64.136.174.65:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Inbound Carrier;gwid=43\n44,5,sip:64.136.174.21:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Inbound Carrier;gwid=44\n45,5,sip:209.166.154.71:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Inbound Carrier;gwid=45\n46,6,sip:64.136.174.30:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Outbound Conversational Carrier;gwid=46\n47,6,sip:64.136.173.22:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Outbound Conversational Carrier;gwid=47\n48,6,sip:209.166.128.200:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Outbound Conversational Carrier;gwid=48\n49,6,sip:192.240.151.100:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=VoIP Innovations Outbound Conversational Carrier;gwid=49\n50,7,sip:72.15.219.140:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Thinq Carrier;gwid=50\n51,8,sip:216.147.191.157:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Voxtelesys Carrier;gwid=51\n52,9,sip:64.34.181.47:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=Les.net Carrier;gwid=52\n53,10,sip:206.80.250.100:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=ThinkTel;gwid=53\n54,10,sip:208.68.17.52:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=ThinkTel;gwid=54\n55,10,sip:209.197.130.80:5060,0,0,signalling=proxy;media=proxy;rweight=1,name=ThinkTel;gwid=55\n"
  },
  {
    "path": "kamailio/defaults/dispatcher.sql",
    "content": "-- update dispatcher schema to fit our storage requirements\nALTER TABLE dispatcher\n  MODIFY description varchar(255) NOT NULL DEFAULT '';"
  },
  {
    "path": "kamailio/defaults/dr_gateways.csv",
    "content": "1;FLT_CARRIER;54.172.60.0;0;;1,FLT_CARRIER,,proxy,proxy;name:Twilio NA-Virginia Carrier,gwgroup:1,addr_id:18\n2;FLT_CARRIER;54.172.60.1;0;;2,FLT_CARRIER,,proxy,proxy;name:Twilio NA-Virginia  Carrier,gwgroup:1,addr_id:19\n3;FLT_CARRIER;54.172.60.2;0;;3,FLT_CARRIER,,proxy,proxy;name:Twilio NA-Virginia Carrier,gwgroup:1,addr_id:20\n4;FLT_CARRIER;54.172.60.3;0;;4,FLT_CARRIER,,proxy,proxy;name:Twilio NA-Virginia Carrier,gwgroup:1,addr_id:21\n5;FLT_CARRIER;54.244.51.0;0;;5,FLT_CARRIER,,proxy,proxy;name:Twilio NA-Oregon Carrier,gwgroup:1,addr_id:22\n6;FLT_CARRIER;54.244.51.1;0;;6,FLT_CARRIER,,proxy,proxy;name:Twilio NA-Oregon  Carrier,gwgroup:1,addr_id:23\n7;FLT_CARRIER;54.244.51.2;0;;7,FLT_CARRIER,,proxy,proxy;name:Twilio NA-Oregon Carrier,gwgroup:1,addr_id:24\n8;FLT_CARRIER;54.244.51.3;0;;8,FLT_CARRIER,,proxy,proxy;name:Twilio NA-Oregon Carrier,gwgroup:1,addr_id:25\n9;FLT_CARRIER;52.41.52.34;0;;9,FLT_CARRIER,,proxy,proxy;name:Skyetel North West Inbound,gwgroup:2,addr_id:29\n10;FLT_CARRIER;52.8.201.128;0;;10,FLT_CARRIER,,proxy,proxy;name:Skyetel South West Inbound,gwgroup:2,addr_id:30\n11;FLT_CARRIER;52.60.138.31;0;;11,FLT_CARRIER,,proxy,proxy;name:Skyetel North East Inbound,gwgroup:2,addr_id:31\n12;FLT_CARRIER;50.17.48.216;0;;12,FLT_CARRIER,,proxy,proxy;name:Skyetel South East Inbound,gwgroup:2,addr_id:32\n13;FLT_CARRIER;35.156.192.164;0;;13,FLT_CARRIER,,proxy,proxy;name:Skyetel Europe Inbound,gwgroup:2,addr_id:33\n14;FLT_CARRIER;term.skyetel.com;0;;14,FLT_CARRIER,,proxy,proxy;name:Skyetel 1st Priority Outbound Call,gwgroup:2,addr_id:34\n15;FLT_CARRIER;52.41.52.34;0;;15,FLT_CARRIER,,proxy,proxy;name:Skyetel 2nd Priority Outbound Call,gwgroup:2,addr_id:35\n16;FLT_CARRIER;52.8.201.128;0;;16,FLT_CARRIER,,proxy,proxy;name:Skyetel 3rd Priority Outbound Call,gwgroup:2,addr_id:36\n17;FLT_CARRIER;50.17.48.216;0;;17,FLT_CARRIER,,proxy,proxy;name:Skyetel 4rd Priority Outbound Call,gwgroup:2,addr_id:37\n18;FLT_CARRIER;52.32.223.28;0;;18,FLT_CARRIER,,proxy,proxy;name:Skyetel North West High Cost Outbound Traffic,gwgroup:2,addr_id:38\n19;FLT_CARRIER;52.4.178.107;0;;19,FLT_CARRIER,,proxy,proxy;name:Skyetel South East High Cost Outbound Traffic,gwgroup:2,addr_id:39\n21;FLT_CARRIER;34.210.91.112;0;;21,FLT_CARRIER,,proxy,proxy;name:Flowroute US-West-OR,gwgroup:3,addr_id:41\n23;FLT_CARRIER;34.226.36.32;0;;23,FLT_CARRIER,,proxy,proxy;name:Flowroute US-East-VA,gwgroup:3,addr_id:43\n24;FLT_CARRIER;81.201.82.45;0;;24,FLT_CARRIER,,proxy,proxy;name:Voxbone Belgium,gwgroup:4,addr_id:44\n25;FLT_CARRIER;81.201.84.195;0;;25,FLT_CARRIER,,proxy,proxy;name:Voxbone LA,gwgroup:4,addr_id:45\n26;FLT_CARRIER;81.201.85.45;0;;26,FLT_CARRIER,,proxy,proxy;name:Voxbone NYC,gwgroup:4,addr_id:46\n27;FLT_CARRIER;81.201.83.45;0;;27,FLT_CARRIER,,proxy,proxy;name:Voxbone Germany,gwgroup:4,addr_id:47\n28;FLT_CARRIER;81.201.86.45;0;;28,FLT_CARRIER,,proxy,proxy;name:Voxbone Hong Kong,gwgroup:4,addr_id:48\n29;FLT_CARRIER;81.201.84.195;0;;29,FLT_CARRIER,,proxy,proxy;name:Voxbone Australia,gwgroup:4,addr_id:49\n30;FLT_CARRIER;64.136.174.30;0;;30,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Inbound Carrier,gwgroup:5,addr_id:50\n31;FLT_CARRIER;64.136.173.22;0;;31,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Inbound Carrier,gwgroup:5,addr_id:51\n32;FLT_CARRIER;209.166.128.200;0;;32,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Inbound Carrier,gwgroup:5,addr_id:52\n33;FLT_CARRIER;192.240.151.100;0;;33,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Inbound Carrier,gwgroup:5,addr_id:53\n34;FLT_CARRIER;64.136.173.31;0;;34,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Inbound Carrier,gwgroup:5,addr_id:54\n35;FLT_CARRIER;64.136.174.30;0;;35,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Inbound Carrier,gwgroup:5,addr_id:55\n36;FLT_CARRIER;64.136.174.20;0;;36,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Inbound Carrier,gwgroup:5,addr_id:56\n37;FLT_CARRIER;209.166.154.70;0;;37,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Inbound Carrier,gwgroup:5,addr_id:57\n38;FLT_CARRIER;64.136.174.65;0;;38,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Inbound Carrier,gwgroup:5,addr_id:58\n39;FLT_CARRIER;64.136.173.23;0;;39,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Inbound Carrier,gwgroup:5,addr_id:59\n40;FLT_CARRIER;209.166.128.201;0;;40,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Inbound Carrier,gwgroup:5,addr_id:60\n41;FLT_CARRIER;192.240.151.101;0;;41,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Inbound Carrier,gwgroup:5,addr_id:61\n42;FLT_CARRIER;64.136.173.65;0;;42,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Inbound Carrier,gwgroup:5,addr_id:62\n43;FLT_CARRIER;64.136.174.65;0;;43,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Inbound Carrier,gwgroup:5,addr_id:63\n44;FLT_CARRIER;64.136.174.21;0;;44,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Inbound Carrier,gwgroup:5,addr_id:64\n45;FLT_CARRIER;209.166.154.71;0;;45,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Inbound Carrier,gwgroup:5,addr_id:65\n46;FLT_CARRIER;64.136.174.30;0;;46,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Outbound Conversational Carrier,gwgroup:6,addr_id:66\n47;FLT_CARRIER;64.136.173.22;0;;47,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Outbound Conversational Carrier,gwgroup:6,addr_id:67\n48;FLT_CARRIER;209.166.128.200;0;;48,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Outbound Conversational Carrier,gwgroup:6,addr_id:68\n49;FLT_CARRIER;192.240.151.100;0;;49,FLT_CARRIER,,proxy,proxy;name:VoIP Innovations Outbound Conversational Carrier,gwgroup:6,addr_1d:69\n50;FLT_CARRIER;72.15.219.140;0;;50,FLT_CARRIER,,proxy,proxy;name:Thinq Carrier,gwgroup:7,addr_id:70\n51;FLT_CARRIER;216.147.191.157;0;;51,FLT_CARRIER,,proxy,proxy;name:Voxtelesys Carrier,gwgroup:8,addr_id:71\n52;FLT_CARRIER;64.34.181.47;0;;52,FLT_CARRIER,,proxy,proxy;name:Les.net Carrier,gwgroup:9,addr_id:72\n53;FLT_CARRIER;206.80.250.100;0;;53,FLT_CARRIER,,proxy,proxy;name:ThinkTel,gwgroup:10,addr_id:73\n54;FLT_CARRIER;208.68.17.52;0;;54,FLT_CARRIER,,proxy,proxy;name:ThinkTel,gwgroup:10,addr_id:74\n55;FLT_CARRIER;209.197.130.80;0;;55,FLT_CARRIER,,proxy,proxy;name:ThinkTel,gwgroup:10,addr_id:75\n"
  },
  {
    "path": "kamailio/defaults/dr_gateways.sql",
    "content": "-- update dr_gateways schema\nALTER TABLE dr_gateways\n  MODIFY COLUMN `address` VARCHAR(253) NOT NULL,\n  MODIFY COLUMN `pri_prefix` VARCHAR(64) NOT NULL DEFAULT '',\n  MODIFY COLUMN `attrs` VARCHAR(255) NOT NULL DEFAULT '',\n  MODIFY COLUMN `description` VARCHAR(255) NOT NULL DEFAULT '';\n\n-- update dr_gateways attrs column when entry created\nDROP TRIGGER IF EXISTS insert_dr_gateways;\nDELIMITER //\nCREATE TRIGGER insert_dr_gateways\n  BEFORE INSERT\n  ON dr_gateways\n  FOR EACH ROW\nBEGIN\n\n  -- set explicit defaults\n  IF (NEW.gwid = 0) THEN\n    SET NEW.gwid = NULL;\n  END IF;\n  IF (NEW.attrs IS NULL) THEN\n    SET NEW.attrs = '';\n  END IF;\n\n  SET @new_gwid := COALESCE(NEW.gwid, @new_gwid, (\n    SELECT auto_increment\n    FROM information_schema.tables\n    WHERE table_name = 'dr_gateways' AND table_schema = DATABASE()));\n\n  -- only rewrite gwid,type part of attrs\n  SET NEW.attrs = CONCAT(CAST(@new_gwid AS char), ',', CAST(NEW.type AS char),\n                         SUBSTRING(NEW.attrs, LENGTH(SUBSTRING_INDEX(NEW.attrs, ',', 2)) + 1));\n  SET @new_gwid = @new_gwid + 1;\n\nEND;//\nDELIMITER ;\n\n-- update dr_gateways attrs column when entry updated\nDROP TRIGGER IF EXISTS update_dr_gateways;\nDELIMITER //\nCREATE TRIGGER update_dr_gateways\n  BEFORE UPDATE\n  ON dr_gateways\n  FOR EACH ROW\nBEGIN\n\n  -- only rewrite gwid,type part of attrs\n  SET NEW.attrs = CONCAT(CAST(NEW.gwid AS char), ',', CAST(NEW.type AS char),\n                         SUBSTRING(NEW.attrs, LENGTH(SUBSTRING_INDEX(NEW.attrs, ',', 2)) + 1));\n\nEND;//\nDELIMITER ;\n"
  },
  {
    "path": "kamailio/defaults/dr_gw_lists.csv",
    "content": "1;1,2,3,4,5,6,7,8;name:Twilio NA Inbound CarrierGroup,type:8,lb:1\n2;9,10,11,12,13,14,15,16,17,18,19;name:Skyetel CarrierGroup,type:8,lb:2\n3;21,23;name:Flowroute CarrierGroup,type:8,lb:3\n4;24,25,26,27,28,29;name:Voxbone CarrierGroup,type:8,lb:4\n5;30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45;name:VoIP Innovations Inbound CarrierGroup,type:8,lb:5\n6;46,47,48,49;name:VoIP Innovations Outbound Conversational CarrierGroup,type:8,lb:6\n7;50;name:Thinq CarrierGroup,type:8,lb:7\n8;51;name:Voxtelesys CarrierGroup,type:8,lb:8\n9;52;name:Les.net CarrierGroup,type:8,lb:9\n10;53,54,55;name:ThinkTel CarrierGroup,type:8,lb:10\n"
  },
  {
    "path": "kamailio/defaults/dr_gw_lists.sql",
    "content": "-- update dr_gw_lists schema to fit our storage requirements\nALTER TABLE dr_gw_lists\n  MODIFY description varchar(255) NOT NULL DEFAULT '';"
  },
  {
    "path": "kamailio/defaults/dr_rules.csv",
    "content": "1;FLT_OUTBOUND;;;;;#2;name:Default Outbound Route\n"
  },
  {
    "path": "kamailio/defaults/dr_rules.sql",
    "content": "-- update dr_rules schema to fit our storage requirements\nALTER TABLE dr_rules\n  MODIFY description varchar(255) NOT NULL DEFAULT '';"
  },
  {
    "path": "kamailio/defaults/dsip_call_settings.sql",
    "content": "DROP TABLE IF EXISTS `dsip_call_settings`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_call_settings` (\n    `gwgroupid` INT UNSIGNED NOT NULL,\n    `limit` INT UNSIGNED DEFAULT NULL,\n    `timeout` INT UNSIGNED DEFAULT NULL,\n    PRIMARY KEY (`gwgroupid`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\nDROP TABLE IF EXISTS `dsip_call_settings_h`;\nDROP VIEW IF EXISTS `dsip_call_settings_h`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE VIEW `dsip_call_settings_h` AS\n  SELECT\n    CAST(gwgroupid AS char) AS gwgroupid,\n    CAST(`limit` AS char) AS `limit`,\n    CAST(timeout AS char) AS timeout\n  FROM dsip_call_settings;\n/*!40101 SET character_set_client = @saved_cs_client */;"
  },
  {
    "path": "kamailio/defaults/dsip_cdrinfo.sql",
    "content": "DROP TABLE IF EXISTS `dsip_cdrinfo`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_cdrinfo` (\n  `gwgroupid` int(11) NOT NULL,\n  `email` varchar(255) NOT NULL DEFAULT '',\n  `send_interval` varchar(255) NOT NULL DEFAULT '* * 1 * *',\n  `last_sent` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n  PRIMARY KEY (`gwgroupid`)\n) ENGINE = InnoDB\n  DEFAULT CHARSET = utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n"
  },
  {
    "path": "kamailio/defaults/dsip_forwarding.sql",
    "content": "-- dr_ruleid refers to owning inbound rule\nDROP TABLE IF EXISTS dsip_hardfwd;\n/*!40101 SET @saved_cs_client = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE dsip_hardfwd (\n  dr_ruleid varchar(64) NOT NULL,\n  did varchar(64) NOT NULL,\n  dr_groupid varchar(64) NOT NULL,\n  key_type varchar(64) NOT NULL DEFAULT '0',\n  value_type varchar(64) NOT NULL DEFAULT '0',\n  PRIMARY KEY (dr_ruleid)\n) ENGINE = InnoDB\n  DEFAULT CHARSET = utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n-- dr_ruleid refers to owning inbound rule\nDROP TABLE IF EXISTS dsip_failfwd;\n/*!40101 SET @saved_cs_client = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE dsip_failfwd (\n  dr_ruleid varchar(64) NOT NULL,\n  did varchar(64) NOT NULL,\n  dr_groupid varchar(64) NOT NULL,\n  key_type varchar(64) NOT NULL DEFAULT '0',\n  value_type varchar(64) NOT NULL DEFAULT '0',\n  PRIMARY KEY (dr_ruleid)\n) ENGINE = InnoDB\n  DEFAULT CHARSET = utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\nDROP TABLE IF EXISTS dsip_prefix_mapping;\nDROP VIEW IF EXISTS dsip_prefix_mapping;\n/*!40101 SET @saved_cs_client = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE VIEW dsip_prefix_mapping AS\n  SELECT\n    prefix,\n    CAST(ruleid AS char) AS ruleid,\n    CAST(priority AS char) AS priority,\n    '0' AS key_type,\n    '0' AS value_type\n  FROM dr_rules\n  WHERE groupid='FLT_INBOUND_REPLACE';\n/*!40101 SET character_set_client = @saved_cs_client */;\n"
  },
  {
    "path": "kamailio/defaults/dsip_gw2gwgroup.sql",
    "content": "DROP TABLE IF EXISTS dsip_gw2gwgroup;\n/*!40101 SET @saved_cs_client = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE dsip_gw2gwgroup (\n  gwid varchar(64) NOT NULL,\n  gwgroupid varchar(64) NOT NULL,\n  key_type varchar(64) NOT NULL DEFAULT '0',\n  value_type varchar(64) NOT NULL DEFAULT '0',\n  PRIMARY KEY (gwid, gwgroupid)\n) ENGINE = InnoDB\n  DEFAULT CHARSET = utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n\n-- create gw2gwgroup entries when dr_gw_lists entry created\nDROP TRIGGER IF EXISTS insert_gw2gwgroup;\nDELIMITER //\nCREATE TRIGGER insert_gw2gwgroup\n  AFTER INSERT\n  ON dr_gw_lists\n  FOR EACH ROW\nBEGIN\n\n  DECLARE num_gws int DEFAULT 0;\n  DECLARE gw_index int DEFAULT 1;\n\n  IF CHAR_LENGTH(NEW.gwlist) > 0 THEN\n    SET num_gws := (CHAR_LENGTH(NEW.gwlist) - CHAR_LENGTH(REPLACE(NEW.gwlist, ',', '')) + 1);\n\n    -- loop through gwlist (1-based index)\n    WHILE gw_index <= num_gws\n      DO\n        INSERT IGNORE INTO dsip_gw2gwgroup\n        VALUES (SUBSTRING_INDEX(SUBSTRING_INDEX(NEW.gwlist, ',', gw_index), ',', -1), cast(NEW.id AS char(64)), DEFAULT,\n                DEFAULT);\n        SET gw_index := gw_index + 1;\n      END WHILE;\n  END IF;\n\nEND;//\nDELIMITER ;\n\n-- update gw2gwgroup entries when dr_gw_lists entry updated\nDROP TRIGGER IF EXISTS update_gw2gwgroup;\nDELIMITER //\nCREATE TRIGGER update_gw2gwgroup\n  AFTER UPDATE\n  ON dr_gw_lists\n  FOR EACH ROW\nBEGIN\n\n  DECLARE num_gws int DEFAULT 0;\n  DECLARE gw_index int DEFAULT 1;\n\n  -- best approach is to delete OLD rows and create NEW ones\n  IF NOT (NEW.gwlist <=> OLD.gwlist) THEN\n    DELETE FROM dsip_gw2gwgroup WHERE gwgroupid = cast(OLD.id AS char(64));\n\n    IF CHAR_LENGTH(NEW.gwlist) > 0 THEN\n      SET num_gws := (CHAR_LENGTH(NEW.gwlist) - CHAR_LENGTH(REPLACE(NEW.gwlist, ',', '')) + 1);\n\n      -- loop through gwlist (1-based index)\n      WHILE gw_index <= num_gws\n        DO\n          INSERT IGNORE INTO dsip_gw2gwgroup\n          VALUES (SUBSTRING_INDEX(SUBSTRING_INDEX(NEW.gwlist, ',', gw_index), ',', -1), cast(NEW.id AS char(64)),\n                  DEFAULT,\n                  DEFAULT);\n          SET gw_index := gw_index + 1;\n        END WHILE;\n    END IF;\n\n    -- only need to update gwid here\n  ELSEIF NOT (NEW.id <=> OLD.id) THEN\n    UPDATE dsip_gw2gwgroup SET gwgroupid = cast(NEW.id AS char(64)) WHERE gwgroupid = cast(OLD.id AS char(64));\n  END IF;\n\nEND;//\nDELIMITER ;\n\n-- delete gw2gwgroup entries when dr_gw_lists entry deleted\nDROP TRIGGER IF EXISTS delete_gw2gwgroup;\nDELIMITER //\nCREATE TRIGGER delete_gw2gwgroup\n  AFTER DELETE\n  ON dr_gw_lists\n  FOR EACH ROW\nBEGIN\n\n  DELETE FROM dsip_gw2gwgroup WHERE gwgroupid = cast(OLD.id AS char(64));\n\nEND;//\nDELIMITER ;\n"
  },
  {
    "path": "kamailio/defaults/dsip_gwgroup2lb.sql",
    "content": "DROP TABLE IF EXISTS dsip_gwgroup2lb;\nCREATE TABLE dsip_gwgroup2lb (\n  gwgroupid varchar(64) NOT NULL,\n  setid varchar(64) NOT NULL,\n  enabled char(1) NOT NULL DEFAULT '0',\n  key_type varchar(64) NOT NULL DEFAULT '0',\n  value_type varchar(64) NOT NULL DEFAULT '0',\n  PRIMARY KEY (gwgroupid)\n) ENGINE = InnoDB\n  DEFAULT CHARSET = utf8mb4;\n\n-- create dsip_gwgroup2lb entry when dr_gw_lists entry created with lb or lb_ext fields in description\nDROP TRIGGER IF EXISTS insert_gwgroup2lb;\nDELIMITER //\nCREATE TRIGGER insert_gwgroup2lb\n  AFTER INSERT\n  ON dr_gw_lists\n  FOR EACH ROW\nBEGIN\n  DECLARE v_setid varchar(64);\n\n  SET @new_gwgroupid := COALESCE(NEW.id, @new_gwgroupid, (\n    SELECT auto_increment\n    FROM information_schema.tables\n    WHERE table_name = 'dr_gw_lists' AND table_schema = DATABASE()));\n\n  IF NEW.description REGEXP '(?:lb:|lb_ext:)([0-9]+)' THEN\n    SET v_setid = REGEXP_REPLACE(NEW.description, '.*(?:lb:|lb_ext:)([0-9]+).*', '\\\\1');\n    REPLACE INTO dsip_gwgroup2lb\n      VALUES (CAST(@new_gwgroupid AS char), CAST(v_setid AS char), DEFAULT, DEFAULT, DEFAULT);\n  END IF;\n\n  SET @new_gwgroupid = @new_gwgroupid + 1;\nEND; //\nDELIMITER ;\n\n-- update dsip_gwgroup2lb entry when dr_gw_lists entry description is updated\nDROP TRIGGER IF EXISTS update_gwgroup2lb;\nDELIMITER //\nCREATE TRIGGER update_gwgroup2lb\n  AFTER UPDATE\n  ON dr_gw_lists\n  FOR EACH ROW\nBEGIN\n  DECLARE v_gwgroupid varchar(64) DEFAULT NULL;\n  DECLARE v_setid varchar(64) DEFAULT NULL;\n\n  -- always update description changed\n  IF NOT (NEW.description <=> OLD.description) THEN\n    -- in case the gwgroupid changed\n    SET v_gwgroupid = CAST(COALESCE(NEW.id, OLD.id) AS char);\n\n    -- make sure we have a setid\n    IF NEW.description REGEXP '(?:lb:|lb_ext:)([0-9]+)' THEN\n      SET v_setid = REGEXP_REPLACE(NEW.description, '.*(?:lb:|lb_ext:)([0-9]+).*', '\\\\1');\n      INSERT INTO dsip_gwgroup2lb VALUES(v_gwgroupid, v_setid, DEFAULT, DEFAULT, DEFAULT)\n                                  ON DUPLICATE KEY UPDATE setid=v_setid;\n    END IF;\n  END IF;\nEND; //\nDELIMITER ;\n\n-- delete dsip_gwgroup2lb entry when dr_gw_lists entry deleted\nDROP TRIGGER IF EXISTS delete_gwgroup2lb;\nDELIMITER //\nCREATE TRIGGER delete_gwgroup2lb\n  AFTER DELETE\n  ON dr_gw_lists\n  FOR EACH ROW\nBEGIN\n  DELETE FROM dsip_gwgroup2lb WHERE gwgroupid = cast(OLD.id AS char);\nEND; //\nDELIMITER ;\n\n-- update dsip_gwgroup2lb when dr_rules are created\nDROP TRIGGER IF EXISTS insert_rule_gwgroup2lb;\nDELIMITER //\nCREATE TRIGGER insert_rule_gwgroup2lb\n  AFTER INSERT\n  ON dr_rules\n  FOR EACH ROW\nBEGIN\n  -- only inbound routes can have load balancing associated with it\n  IF (NEW.groupid = 9000) THEN\n    IF (NEW.description REGEXP 'lb_enabled:1(,|$)') THEN\n      UPDATE dsip_gwgroup2lb SET enabled = '1' WHERE gwgroupid = REPLACE(NEW.gwlist, '#', '');\n    ELSE\n      UPDATE dsip_gwgroup2lb SET enabled = '0' WHERE gwgroupid = REPLACE(NEW.gwlist, '#', '');\n    END IF;\n  END IF;\nEND; //\nDELIMITER ;\n\n-- update dsip_gwgroup2lb when dr_rules are updated\nDROP TRIGGER IF EXISTS update_rule_gwgroup2lb;\nDELIMITER //\nCREATE TRIGGER update_rule_gwgroup2lb\n  AFTER UPDATE\n  ON dr_rules\n  FOR EACH ROW\nBEGIN\n  DECLARE v_gwgroupid varchar(64) DEFAULT NULL;\n  DECLARE v_description varchar(255) DEFAULT '';\n  DECLARE v_groupid varchar(255) DEFAULT '';\n\n  SET v_gwgroupid = REPLACE(COALESCE(NEW.gwlist, OLD.gwlist), '#', '');\n  SET v_description = COALESCE(NEW.description, OLD.description);\n  SET v_groupid = CAST(COALESCE(NEW.groupid, OLD.groupid) AS int);\n\n  -- only inbound routes can have load balancing associated with it\n  IF (v_groupid = 9000) THEN\n    IF (v_description REGEXP 'lb_enabled:1(,|$)') THEN\n      UPDATE dsip_gwgroup2lb SET enabled = '1' WHERE gwgroupid = v_gwgroupid;\n    ELSE\n      UPDATE dsip_gwgroup2lb SET enabled = '0' WHERE gwgroupid = v_gwgroupid;\n    END IF;\n  END IF;\nEND; //\nDELIMITER ;\n\n-- update dsip_gwgroup2lb when dr_rules are deleted\nDROP TRIGGER IF EXISTS delete_rule_gwgroup2lb;\nDELIMITER //\nCREATE TRIGGER delete_rule_gwgroup2lb\n  AFTER DELETE\n  ON dr_rules\n  FOR EACH ROW\nBEGIN\n  -- only inbound routes can have load balancing associated with it\n  IF (OLD.groupid = 9000) THEN\n    -- if it is the last rule for the gwgroup then delete load balancing entry\n    IF ((SELECT COUNT(ruleid) FROM dr_rules WHERE gwlist=OLD.gwlist AND groupid=OLD.groupid AND ruleid!=OLD.ruleid) = 0) THEN\n      DELETE FROM dsip_gwgroup2lb WHERE gwgroupid=REPLACE(OLD.gwlist, '#', '');\n    END IF;\n  END IF;\nEND; //\nDELIMITER ;\n\n"
  },
  {
    "path": "kamailio/defaults/dsip_lcr.sql",
    "content": "DROP TABLE IF EXISTS `dsip_lcr`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_lcr` (\n    `pattern` varchar(64) NOT NULL DEFAULT '',\n    `key_type` varchar(64) NOT NULL DEFAULT '0',\n    `dr_groupid` varchar(64) NOT NULL DEFAULT '',\n    `value_type` varchar(64) NOT NULL DEFAULT '0',\n    `cost` decimal(3,2) NOT NULL DEFAULT '0.0',\n    `from_prefix` varchar(64) NOT NULL DEFAULT '',\n    `expires` int(11) NOT NULL DEFAULT '0',\n    PRIMARY KEY (`pattern`)\n) ENGINE = InnoDB\n  DEFAULT CHARSET = utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n"
  },
  {
    "path": "kamailio/defaults/dsip_maintmode.sql",
    "content": "DROP TABLE IF EXISTS `dsip_maintmode`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_maintmode` (\n    `ipaddr` varchar(64) NOT NULL DEFAULT '',\n    `key_type` varchar(64) NOT NULL DEFAULT '0',\n    `gwid` varchar(64) NOT NULL DEFAULT '',\n    `value_type` varchar(64) NOT NULL DEFAULT '0',\n    `status` TINYINT NOT NULL DEFAULT '1',\n    PRIMARY KEY (`ipaddr`)\n) ENGINE = InnoDB\n  DEFAULT CHARSET = utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n"
  },
  {
    "path": "kamailio/defaults/dsip_notification.sql",
    "content": "DROP TABLE IF EXISTS `dsip_notification`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_notification` (\n  `gwgroupid` int(11) NOT NULL,\n  `type` int(11) NOT NULL,\n  `method` int(11) DEFAULT NULL,\n  `value` varchar(255) DEFAULT NULL,\n  `createdate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n  PRIMARY KEY (`gwgroupid`,`type`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n"
  },
  {
    "path": "kamailio/defaults/dsip_settings.sql",
    "content": "-- constant values must be added when pre-processing the file (by calling script)\n-- using user-defined variables allows us to keep syntax highlighting in our IDEs, but note they won't work without replacement\n-- the following strings are expected to be replaced by the pre-processing script (INCLUDE THE PRECEDING @ SYMBOL!):\n-- HASHED_CREDS_ENCODED_MAX_LEN\n-- AESCTR_CREDS_ENCODED_MAX_LEN\n\n-- DB representation of settings.py with non-db backed settings left out\nDROP TABLE IF EXISTS dsip_settings;\n/*!40101 SET @saved_cs_client = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE dsip_settings\n(\n  DSIP_ID                 VARBINARY(@HASHED_CREDS_ENCODED_MAX_LEN) COLLATE 'binary' NOT NULL,\n  DSIP_CLUSTER_ID         INT UNSIGNED                                              NOT NULL DEFAULT 1,\n  DSIP_CLUSTER_SYNC       TINYINT(1)                                                NOT NULL DEFAULT 1,\n  DSIP_PROTO              VARCHAR(16)                                               NOT NULL DEFAULT 'http',\n  DSIP_PORT               INT                                                       NOT NULL DEFAULT 5000,\n  DSIP_USERNAME           VARCHAR(255)                                              NOT NULL DEFAULT 'admin',\n  DSIP_PASSWORD           VARBINARY(@HASHED_CREDS_ENCODED_MAX_LEN) COLLATE 'binary' NOT NULL,\n  DSIP_IPC_PASS           VARBINARY(@AESCTR_CREDS_ENCODED_MAX_LEN) COLLATE 'binary' NOT NULL,\n  DSIP_API_PROTO          VARCHAR(16)                                               NOT NULL DEFAULT 'http',\n  DSIP_API_PORT           INT                                                       NOT NULL DEFAULT 5000,\n  DSIP_PRIV_KEY           VARCHAR(255)                                              NOT NULL DEFAULT '/etc/dsiprouter/privkey',\n  DSIP_PID_FILE           VARCHAR(255)                                              NOT NULL DEFAULT '/run/dsiprouter/dsiprouter.pid',\n  DSIP_UNIX_SOCK          VARCHAR(255)                                              NOT NULL DEFAULT '/run/dsiprouter/dsiprouter.sock',\n  DSIP_IPC_SOCK           VARCHAR(255)                                              NOT NULL DEFAULT '/run/dsiprouter/ipc.sock',\n  DSIP_API_TOKEN          VARBINARY(@AESCTR_CREDS_ENCODED_MAX_LEN) COLLATE 'binary' NOT NULL,\n  DSIP_LOG_LEVEL          INT                                                       NOT NULL DEFAULT 3,\n  DSIP_LOG_FACILITY       INT                                                       NOT NULL DEFAULT 18,\n  DSIP_SSL_KEY            VARCHAR(255)                                              NOT NULL DEFAULT '',\n  DSIP_SSL_CERT           VARCHAR(255)                                              NOT NULL DEFAULT '',\n  DSIP_SSL_CA             VARCHAR(255)                                              NOT NULL DEFAULT '/etc/dsiprouter/certs/ca-list.pem',\n  DSIP_SSL_EMAIL          VARCHAR(255)                                              NOT NULL DEFAULT '',\n  DSIP_CERTS_DIR          VARCHAR(255)                                              NOT NULL DEFAULT '/etc/dsiprouter/certs',\n  VERSION                 VARCHAR(32)                                               NOT NULL,\n  DEBUG                   TINYINT(1)                                                NOT NULL DEFAULT 0,\n  `ROLE`                  VARCHAR(32)                                               NOT NULL DEFAULT '',\n  GUI_INACTIVE_TIMEOUT    INT UNSIGNED                                              NOT NULL DEFAULT 20,\n  KAM_DB_HOST             VARCHAR(255)                                              NOT NULL DEFAULT 'localhost',\n  KAM_DB_DRIVER           VARCHAR(255)                                              NOT NULL DEFAULT '',\n  KAM_DB_TYPE             VARCHAR(255)                                              NOT NULL DEFAULT 'mysql',\n  KAM_DB_PORT             VARCHAR(255)                                              NOT NULL DEFAULT '3306',\n  KAM_DB_NAME             VARCHAR(255)                                              NOT NULL DEFAULT 'kamailio',\n  KAM_DB_USER             VARCHAR(255)                                              NOT NULL DEFAULT 'kamailio',\n  KAM_DB_PASS             VARBINARY(@AESCTR_CREDS_ENCODED_MAX_LEN) COLLATE 'binary' NOT NULL,\n  KAM_KAMCMD_PATH         VARCHAR(255)                                              NOT NULL DEFAULT '/usr/sbin/kamcmd',\n  KAM_CFG_PATH            VARCHAR(255)                                              NOT NULL DEFAULT '/etc/kamailio/kamailio.cfg',\n  KAM_TLSCFG_PATH         VARCHAR(255)                                              NOT NULL DEFAULT '/etc/kamailio/tls.cfg',\n  RTP_CFG_PATH            VARCHAR(255)                                              NOT NULL DEFAULT '/etc/kamailio/kamailio.cfg',\n  FLT_CARRIER             INT                                                       NOT NULL DEFAULT 8,\n  FLT_PBX                 INT                                                       NOT NULL DEFAULT 9,\n  FLT_MSTEAMS             INT                                                       NOT NULL DEFAULT 17,\n  FLT_OUTBOUND            INT                                                       NOT NULL DEFAULT 8000,\n  FLT_INBOUND             INT                                                       NOT NULL DEFAULT 9000,\n  FLT_LCR_MIN             INT                                                       NOT NULL DEFAULT 10000,\n  FLT_FWD_MIN             INT                                                       NOT NULL DEFAULT 20000,\n  DEFAULT_AUTH_DOMAIN     VARCHAR(255)                                              NOT NULL DEFAULT 'sip.dsiprouter.org',\n  TELEBLOCK_GW_ENABLED    TINYINT(1)                                                NOT NULL DEFAULT 0,\n  TELEBLOCK_GW_IP         VARCHAR(255)                                              NOT NULL DEFAULT '62.34.24.22',\n  TELEBLOCK_GW_PORT       VARCHAR(255)                                              NOT NULL DEFAULT '5066',\n  TELEBLOCK_MEDIA_IP      VARCHAR(255)                                              NOT NULL DEFAULT '',\n  TELEBLOCK_MEDIA_PORT    VARCHAR(255)                                              NOT NULL DEFAULT '',\n  FLOWROUTE_ACCESS_KEY    VARCHAR(255)                                              NOT NULL DEFAULT '',\n  FLOWROUTE_SECRET_KEY    VARCHAR(255)                                              NOT NULL DEFAULT '',\n  FLOWROUTE_API_ROOT_URL  VARCHAR(255)                                              NOT NULL DEFAULT 'https://api.flowroute.com/v2',\n  HOMER_ID                BIGINT                                                    NOT NULL,\n  HOMER_HEP_HOST          VARCHAR(255)                                              NOT NULL DEFAULT '',\n  HOMER_HEP_PORT          INT                                                       NOT NULL DEFAULT 9060,\n  NETWORK_MODE            INT                                                       NOT NULL DEFAULT 0,\n  IPV6_ENABLED            TINYINT(1)                                                NOT NULL DEFAULT 0,\n  INTERNAL_IP_ADDR        VARCHAR(255)                                              NOT NULL DEFAULT '',\n  INTERNAL_IP_NET         VARCHAR(255)                                              NOT NULL DEFAULT '',\n  INTERNAL_IP6_ADDR       VARCHAR(255)                                              NOT NULL DEFAULT '',\n  INTERNAL_IP6_NET        VARCHAR(255)                                              NOT NULL DEFAULT '',\n  INTERNAL_FQDN           VARCHAR(255)                                              NOT NULL DEFAULT '',\n  EXTERNAL_IP_ADDR        VARCHAR(255)                                              NOT NULL DEFAULT '',\n  EXTERNAL_IP6_ADDR       VARCHAR(255)                                              NOT NULL DEFAULT '',\n  EXTERNAL_FQDN           VARCHAR(255)                                              NOT NULL DEFAULT '',\n  PUBLIC_IFACE            VARCHAR(255)                                              NOT NULL DEFAULT '',\n  PRIVATE_IFACE           VARCHAR(255)                                              NOT NULL DEFAULT '',\n  UPLOAD_FOLDER           VARCHAR(255)                                              NOT NULL DEFAULT '/tmp',\n  MAIL_SERVER             VARCHAR(255)                                              NOT NULL DEFAULT 'smtp.gmail.com',\n  MAIL_PORT               INT                                                       NOT NULL DEFAULT 587,\n  MAIL_USE_TLS            TINYINT(1)                                                NOT NULL DEFAULT 1,\n  MAIL_USERNAME           VARCHAR(255)                                              NOT NULL DEFAULT '',\n  MAIL_PASSWORD           VARBINARY(@AESCTR_CREDS_ENCODED_MAX_LEN) COLLATE 'binary' NOT NULL,\n  MAIL_ASCII_ATTACHMENTS  TINYINT(1)                                                NOT NULL DEFAULT 0,\n  MAIL_DEFAULT_SENDER     VARCHAR(255)                                              NOT NULL DEFAULT 'DoNotReply@smtp.gmail.com',\n  MAIL_DEFAULT_SUBJECT    VARCHAR(255)                                              NOT NULL DEFAULT 'dSIPRouter System Notification',\n  DSIP_LICENSE_STORE      BLOB                                                      NOT NULL,\n  RTPENGINE_URI           VARCHAR(255)                                              NOT NULL DEFAULT 'udp:localhost:7722',\n  CHECK (`ROLE` IN ('', 'outbound', 'inout')),\n  PRIMARY KEY (DSIP_ID)\n) ENGINE = InnoDB\n  DEFAULT CHARSET = utf8mb4\n  MIN_ROWS = 1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n-- Update dsip_settings table for current dsiprouter instance, then\n-- Sync changes to dsip_settings across same cluster with the following exceptions:\n-- DSIP_ID is unchanged, it is associated to a single node\n-- DSIP_CLUSTER_ID is unchanged\n-- DSIP_CLUSTER_SYNC is unchanged\n-- HOMER_ID is unchanged, it is associated to a single node\n-- NETWORK_MODE is unchanged\n-- IPV6_ENABLED is unchanged\n-- INTERNAL_IP_ADDR is unchanged\n-- INTERNAL_IP_NET is unchanged\n-- INTERNAL_IP6_ADDR is unchanged\n-- INTERNAL_IP6_NET is unchanged\n-- INTERNAL_FQDN is unchanged\n-- EXTERNAL_IP_ADDR is unchanged\n-- EXTERNAL_IP6_ADDR is unchanged\n-- EXTERNAL_FQDN is unchanged\n-- PUBLIC_IFACE is unchanged\n-- PRIVATE_IFACE is unchanged\n-- DSIP_LICENSE_STORE is unchanged, it is associated to a single node\nDROP PROCEDURE IF EXISTS update_dsip_settings;\nDELIMITER //\nCREATE PROCEDURE update_dsip_settings(\n  IN NEW_DSIP_ID VARBINARY(@HASHED_CREDS_ENCODED_MAX_LEN),\n  IN NEW_DSIP_CLUSTER_ID INT UNSIGNED,\n  IN NEW_DSIP_CLUSTER_SYNC TINYINT(1),\n  IN NEW_DSIP_PROTO VARCHAR(16),\n  IN NEW_DSIP_PORT INT,\n  IN NEW_DSIP_USERNAME VARCHAR(255),\n  IN NEW_DSIP_PASSWORD VARBINARY(@HASHED_CREDS_ENCODED_MAX_LEN),\n  IN NEW_DSIP_IPC_PASS VARBINARY(@AESCTR_CREDS_ENCODED_MAX_LEN),\n  IN NEW_DSIP_API_PROTO VARCHAR(16),\n  IN NEW_DSIP_API_PORT INT,\n  IN NEW_DSIP_PRIV_KEY VARCHAR(255),\n  IN NEW_DSIP_PID_FILE VARCHAR(255),\n  IN NEW_DSIP_UNIX_SOCK VARCHAR(255),\n  IN NEW_DSIP_IPC_SOCK VARCHAR(255),\n  IN NEW_DSIP_API_TOKEN VARBINARY(@AESCTR_CREDS_ENCODED_MAX_LEN),\n  IN NEW_DSIP_LOG_LEVEL INT,\n  IN NEW_DSIP_LOG_FACILITY INT,\n  IN NEW_DSIP_SSL_KEY VARCHAR(255),\n  IN NEW_DSIP_SSL_CERT VARCHAR(255),\n  IN NEW_DSIP_SSL_CA VARCHAR(255),\n  IN NEW_DSIP_SSL_EMAIL VARCHAR(255),\n  IN NEW_DSIP_CERTS_DIR VARCHAR(255),\n  IN NEW_VERSION VARCHAR(32),\n  IN NEW_DEBUG TINYINT(1),\n  IN NEW_ROLE VARCHAR(32),\n  IN NEW_GUI_INACTIVE_TIMEOUT INT UNSIGNED,\n  IN NEW_KAM_DB_HOST VARCHAR(255),\n  IN NEW_KAM_DB_DRIVER VARCHAR(255),\n  IN NEW_KAM_DB_TYPE VARCHAR(255),\n  IN NEW_KAM_DB_PORT VARCHAR(255),\n  IN NEW_KAM_DB_NAME VARCHAR(255),\n  IN NEW_KAM_DB_USER VARCHAR(255),\n  IN NEW_KAM_DB_PASS VARBINARY(@AESCTR_CREDS_ENCODED_MAX_LEN),\n  IN NEW_KAM_KAMCMD_PATH VARCHAR(255),\n  IN NEW_KAM_CFG_PATH VARCHAR(255),\n  IN NEW_KAM_TLSCFG_PATH VARCHAR(255),\n  IN NEW_RTP_CFG_PATH VARCHAR(255),\n  IN NEW_FLT_CARRIER INT,\n  IN NEW_FLT_PBX INT,\n  IN NEW_FLT_MSTEAMS INT,\n  IN NEW_FLT_OUTBOUND INT,\n  IN NEW_FLT_INBOUND INT,\n  IN NEW_FLT_LCR_MIN INT,\n  IN NEW_FLT_FWD_MIN INT,\n  IN NEW_DEFAULT_AUTH_DOMAIN VARCHAR(255),\n  IN NEW_TELEBLOCK_GW_ENABLED TINYINT(1),\n  IN NEW_TELEBLOCK_GW_IP VARCHAR(255),\n  IN NEW_TELEBLOCK_GW_PORT VARCHAR(255),\n  IN NEW_TELEBLOCK_MEDIA_IP VARCHAR(255),\n  IN NEW_TELEBLOCK_MEDIA_PORT VARCHAR(255),\n  IN NEW_FLOWROUTE_ACCESS_KEY VARCHAR(255),\n  IN NEW_FLOWROUTE_SECRET_KEY VARCHAR(255),\n  IN NEW_FLOWROUTE_API_ROOT_URL VARCHAR(255),\n  IN NEW_HOMER_ID BIGINT,\n  IN NEW_HOMER_HEP_HOST VARCHAR(255),\n  IN NEW_HOMER_HEP_PORT INT,\n  IN NEW_NETWORK_MODE INT,\n  IN NEW_IPV6_ENABLED TINYINT(1),\n  IN NEW_INTERNAL_IP_ADDR VARCHAR(255),\n  IN NEW_INTERNAL_IP_NET VARCHAR(255),\n  IN NEW_INTERNAL_IP6_ADDR VARCHAR(255),\n  IN NEW_INTERNAL_IP6_NET VARCHAR(255),\n  IN NEW_INTERNAL_FQDN VARCHAR(255),\n  IN NEW_EXTERNAL_IP_ADDR VARCHAR(255),\n  IN NEW_EXTERNAL_IP6_ADDR VARCHAR(255),\n  IN NEW_EXTERNAL_FQDN VARCHAR(255),\n  IN NEW_PUBLIC_IFACE VARCHAR(255),\n  IN NEW_PRIVATE_IFACE VARCHAR(255),\n  IN NEW_UPLOAD_FOLDER VARCHAR(255),\n  IN NEW_MAIL_SERVER VARCHAR(255),\n  IN NEW_MAIL_PORT INT,\n  IN NEW_MAIL_USE_TLS TINYINT(1),\n  IN NEW_MAIL_USERNAME VARCHAR(255),\n  IN NEW_MAIL_PASSWORD VARBINARY(@AESCTR_CREDS_ENCODED_MAX_LEN),\n  IN NEW_MAIL_ASCII_ATTACHMENTS TINYINT(1),\n  IN NEW_MAIL_DEFAULT_SENDER VARCHAR(255),\n  IN NEW_MAIL_DEFAULT_SUBJECT VARCHAR(255),\n  IN NEW_DSIP_LICENSE_STORE BLOB,\n  IN NEW_RTPENGINE_URI VARCHAR(255)\n)\nBEGIN\n  START TRANSACTION;\n\n  REPLACE INTO dsip_settings\n  VALUES (NEW_DSIP_ID,\n          NEW_DSIP_CLUSTER_ID,\n          NEW_DSIP_CLUSTER_SYNC,\n          NEW_DSIP_PROTO,\n          NEW_DSIP_PORT,\n          NEW_DSIP_USERNAME,\n          NEW_DSIP_PASSWORD,\n          NEW_DSIP_IPC_PASS,\n          NEW_DSIP_API_PROTO,\n          NEW_DSIP_API_PORT,\n          NEW_DSIP_PRIV_KEY,\n          NEW_DSIP_PID_FILE,\n          NEW_DSIP_UNIX_SOCK,\n          NEW_DSIP_IPC_SOCK,\n          NEW_DSIP_API_TOKEN,\n          NEW_DSIP_LOG_LEVEL,\n          NEW_DSIP_LOG_FACILITY,\n          NEW_DSIP_SSL_KEY,\n          NEW_DSIP_SSL_CERT,\n          NEW_DSIP_SSL_CA,\n          NEW_DSIP_SSL_EMAIL,\n          NEW_DSIP_CERTS_DIR,\n          NEW_VERSION,\n          NEW_DEBUG,\n          NEW_ROLE,\n          NEW_GUI_INACTIVE_TIMEOUT,\n          NEW_KAM_DB_HOST,\n          NEW_KAM_DB_DRIVER,\n          NEW_KAM_DB_TYPE,\n          NEW_KAM_DB_PORT,\n          NEW_KAM_DB_NAME,\n          NEW_KAM_DB_USER,\n          NEW_KAM_DB_PASS,\n          NEW_KAM_KAMCMD_PATH,\n          NEW_KAM_CFG_PATH,\n          NEW_KAM_TLSCFG_PATH,\n          NEW_RTP_CFG_PATH,\n          NEW_FLT_CARRIER,\n          NEW_FLT_PBX,\n          NEW_FLT_MSTEAMS,\n          NEW_FLT_OUTBOUND,\n          NEW_FLT_INBOUND,\n          NEW_FLT_LCR_MIN,\n          NEW_FLT_FWD_MIN,\n          NEW_DEFAULT_AUTH_DOMAIN,\n          NEW_TELEBLOCK_GW_ENABLED,\n          NEW_TELEBLOCK_GW_IP,\n          NEW_TELEBLOCK_GW_PORT,\n          NEW_TELEBLOCK_MEDIA_IP,\n          NEW_TELEBLOCK_MEDIA_PORT,\n          NEW_FLOWROUTE_ACCESS_KEY,\n          NEW_FLOWROUTE_SECRET_KEY,\n          NEW_FLOWROUTE_API_ROOT_URL,\n          NEW_HOMER_ID,\n          NEW_HOMER_HEP_HOST,\n          NEW_HOMER_HEP_PORT,\n          NEW_NETWORK_MODE,\n          NEW_IPV6_ENABLED,\n          NEW_INTERNAL_IP_ADDR,\n          NEW_INTERNAL_IP_NET,\n          NEW_INTERNAL_IP6_ADDR,\n          NEW_INTERNAL_IP6_NET,\n          NEW_INTERNAL_FQDN,\n          NEW_EXTERNAL_IP_ADDR,\n          NEW_EXTERNAL_IP6_ADDR,\n          NEW_EXTERNAL_FQDN,\n          NEW_PUBLIC_IFACE,\n          NEW_PRIVATE_IFACE,\n          NEW_UPLOAD_FOLDER,\n          NEW_MAIL_SERVER,\n          NEW_MAIL_PORT,\n          NEW_MAIL_USE_TLS,\n          NEW_MAIL_USERNAME,\n          NEW_MAIL_PASSWORD,\n          NEW_MAIL_ASCII_ATTACHMENTS,\n          NEW_MAIL_DEFAULT_SENDER,\n          NEW_MAIL_DEFAULT_SUBJECT,\n          NEW_DSIP_LICENSE_STORE,\n          NEW_RTPENGINE_URI);\n\n  IF NEW_DSIP_CLUSTER_SYNC = 1 THEN\n    UPDATE dsip_settings\n    SET DSIP_PROTO             = NEW_DSIP_PROTO,\n        DSIP_PORT              = NEW_DSIP_PORT,\n        DSIP_USERNAME          = NEW_DSIP_USERNAME,\n        DSIP_PASSWORD          = NEW_DSIP_PASSWORD,\n        DSIP_IPC_PASS          = NEW_DSIP_IPC_PASS,\n        DSIP_API_PROTO         = NEW_DSIP_API_PROTO,\n        DSIP_API_PORT          = NEW_DSIP_API_PORT,\n        DSIP_PRIV_KEY          = NEW_DSIP_PRIV_KEY,\n        DSIP_PID_FILE          = NEW_DSIP_PID_FILE,\n        DSIP_UNIX_SOCK         = NEW_DSIP_UNIX_SOCK,\n        DSIP_IPC_SOCK          = NEW_DSIP_IPC_SOCK,\n        DSIP_API_TOKEN         = NEW_DSIP_API_TOKEN,\n        DSIP_LOG_LEVEL         = NEW_DSIP_LOG_LEVEL,\n        DSIP_LOG_FACILITY      = NEW_DSIP_LOG_FACILITY,\n        DSIP_SSL_KEY           = NEW_DSIP_SSL_KEY,\n        DSIP_SSL_CERT          = NEW_DSIP_SSL_CERT,\n        DSIP_SSL_CA            = NEW_DSIP_SSL_CA,\n        DSIP_SSL_EMAIL         = NEW_DSIP_SSL_EMAIL,\n        DSIP_CERTS_DIR         = NEW_DSIP_CERTS_DIR,\n        VERSION                = NEW_VERSION,\n        DEBUG                  = NEW_DEBUG,\n        `ROLE`                 = NEW_ROLE,\n        GUI_INACTIVE_TIMEOUT   = NEW_GUI_INACTIVE_TIMEOUT,\n        KAM_DB_HOST            = NEW_KAM_DB_HOST,\n        KAM_DB_DRIVER          = NEW_KAM_DB_DRIVER,\n        KAM_DB_TYPE            = NEW_KAM_DB_TYPE,\n        KAM_DB_PORT            = NEW_KAM_DB_PORT,\n        KAM_DB_NAME            = NEW_KAM_DB_NAME,\n        KAM_DB_USER            = NEW_KAM_DB_USER,\n        KAM_DB_PASS            = NEW_KAM_DB_PASS,\n        KAM_KAMCMD_PATH        = NEW_KAM_KAMCMD_PATH,\n        KAM_CFG_PATH           = NEW_KAM_CFG_PATH,\n        KAM_TLSCFG_PATH        = NEW_KAM_TLSCFG_PATH,\n        RTP_CFG_PATH           = NEW_RTP_CFG_PATH,\n        FLT_CARRIER            = NEW_FLT_CARRIER,\n        FLT_PBX                = NEW_FLT_PBX,\n        FLT_MSTEAMS            = NEW_FLT_MSTEAMS,\n        FLT_OUTBOUND           = NEW_FLT_OUTBOUND,\n        FLT_INBOUND            = NEW_FLT_INBOUND,\n        FLT_LCR_MIN            = NEW_FLT_LCR_MIN,\n        FLT_FWD_MIN            = NEW_FLT_FWD_MIN,\n        DEFAULT_AUTH_DOMAIN    = NEW_DEFAULT_AUTH_DOMAIN,\n        TELEBLOCK_GW_ENABLED   = NEW_TELEBLOCK_GW_ENABLED,\n        TELEBLOCK_GW_IP        = NEW_TELEBLOCK_GW_IP,\n        TELEBLOCK_GW_PORT      = NEW_TELEBLOCK_GW_PORT,\n        TELEBLOCK_MEDIA_IP     = NEW_TELEBLOCK_MEDIA_IP,\n        TELEBLOCK_MEDIA_PORT   = NEW_TELEBLOCK_MEDIA_PORT,\n        FLOWROUTE_ACCESS_KEY   = NEW_FLOWROUTE_ACCESS_KEY,\n        FLOWROUTE_SECRET_KEY   = NEW_FLOWROUTE_SECRET_KEY,\n        FLOWROUTE_API_ROOT_URL = NEW_FLOWROUTE_API_ROOT_URL,\n        HOMER_HEP_HOST         = NEW_HOMER_HEP_HOST,\n        HOMER_HEP_PORT         = NEW_HOMER_HEP_PORT,\n        UPLOAD_FOLDER          = NEW_UPLOAD_FOLDER,\n        MAIL_SERVER            = NEW_MAIL_SERVER,\n        MAIL_PORT              = NEW_MAIL_PORT,\n        MAIL_USE_TLS           = NEW_MAIL_USE_TLS,\n        MAIL_USERNAME          = NEW_MAIL_USERNAME,\n        MAIL_PASSWORD          = NEW_MAIL_PASSWORD,\n        MAIL_ASCII_ATTACHMENTS = NEW_MAIL_ASCII_ATTACHMENTS,\n        MAIL_DEFAULT_SENDER    = NEW_MAIL_DEFAULT_SENDER,\n        MAIL_DEFAULT_SUBJECT   = NEW_MAIL_DEFAULT_SUBJECT,\n        RTPENGINE_URI          = NEW_RTPENGINE_URI\n    WHERE DSIP_CLUSTER_ID = NEW_DSIP_CLUSTER_ID\n      AND DSIP_CLUSTER_SYNC = 1\n      AND DSIP_ID != NEW_DSIP_ID;\n  END IF;\n  COMMIT;\nEND //\nDELIMITER ;\n"
  },
  {
    "path": "kamailio/defaults/subscribers.sql",
    "content": "-- update schema for subscribers table\nALTER TABLE subscriber\n  ADD email_address varchar(128) NOT NULL DEFAULT '',\n  ADD rpid varchar(128) NOT NULL DEFAULT '';"
  },
  {
    "path": "kamailio/defaults/uacreg.sql",
    "content": "-- update uacreg schema\nALTER TABLE uacreg\n  MODIFY COLUMN `l_domain` VARCHAR(253) NOT NULL DEFAULT '',\n  MODIFY COLUMN `r_domain` VARCHAR(253) NOT NULL DEFAULT '',\n  MODIFY COLUMN `realm` varchar(253) NOT NULL DEFAULT '',\n  MODIFY COLUMN `auth_proxy` varchar(16000) NOT NULL DEFAULT '';"
  },
  {
    "path": "kamailio/htable-kam57.patch",
    "content": "diff --git a/src/modules/htable/ht_api.c b/src/modules/htable/ht_api.c\nindex 913d038748..2391cb5c5d 100644\n--- a/src/modules/htable/ht_api.c\n+++ b/src/modules/htable/ht_api.c\n@@ -255,7 +255,7 @@ ht_t *ht_get_table(str *name)\n\n int ht_add_table(str *name, int autoexp, str *dbtable, str *dbcols, int size,\n \t\tint dbmode, int itype, int_str *ival, int updateexpire,\n-\t\tint dmqreplicate)\n+\t\tint dmqreplicate, char coldelim, char colnull)\n {\n \tunsigned int htid;\n \tht_t *ht;\n@@ -342,8 +342,8 @@ int ht_add_table(str *name, int autoexp, str *dbtable, str *dbcols, int size,\n \t\t}\n \t\tht->ncols = c + 1;\n \t\tht->pack[0] = 'l';\n-\t\tht->pack[1] = ',';\n-\t\tht->pack[2] = '*';\n+\t\tht->pack[1] = coldelim;\n+\t\tht->pack[2] = colnull;\n \t}\n\n \tht->next = _ht_root;\n@@ -957,6 +958,8 @@ int ht_table_spec(char *spec)\n \tunsigned int dbmode = 0;\n \tunsigned int updateexpire = 1;\n \tunsigned int dmqreplicate = 0;\n+\tchar coldelim = ',';\n+\tchar colnull = '*';\n \tstr in;\n \tstr tok;\n \tparam_t *pit = NULL;\n@@ -1023,13 +1026,34 @@ int ht_table_spec(char *spec)\n\n \t\t\tLM_DBG(\"htable [%.*s] - dmqreplicate [%u]\\n\", name.len, name.s,\n \t\t\t\t\tdmqreplicate);\n+\t\t} else if(pit->name.len == 8\n+\t\t\t\t  && strncmp(pit->name.s, \"coldelim\", 8) == 0) {\n+\t\t\tif(tok.len > 1)\n+\t\t\t\tgoto error;\n+\n+\t\t\tcoldelim = tok.s[0];\n+\t\t\tLM_DBG(\"htable [%.*s] - coldelim [%c]\\n\", name.len, name.s,\n+\t\t\t\t   coldelim);\n+\t\t} else if(pit->name.len == 7\n+\t\t\t\t  && strncmp(pit->name.s, \"colnull\", 7) == 0) {\n+\t\t\tif(tok.len > 1)\n+\t\t\t\tgoto error;\n+\n+\t\t\tif(tok.len == 0) {\n+\t\t\t\tcolnull = '\\0';\n+\t\t\t} else {\n+\t\t\t\tcolnull = tok.s[0];\n+\t\t\t}\n+\n+\t\t\tLM_DBG(\"htable [%.*s] - colnull [%c]\\n\", name.len, name.s,\n+\t\t\t   \t\tcolnull);\n \t\t} else {\n \t\t\tgoto error;\n \t\t}\n \t}\n\n \treturn ht_add_table(&name, autoexpire, &dbtable, &dbcols, size, dbmode,\n-\t\t\titype, &ival, updateexpire, dmqreplicate);\n+\t\t\titype, &ival, updateexpire, dmqreplicate, coldelim, colnull);\n\n error:\n \tLM_ERR(\"invalid htable parameter [%.*s]\\n\", in.len, in.s);\ndiff --git a/src/modules/htable/ht_api.h b/src/modules/htable/ht_api.h\nindex d8bdc2aab2..e24a93b1f1 100644\n--- a/src/modules/htable/ht_api.h\n+++ b/src/modules/htable/ht_api.h\n@@ -88,7 +88,7 @@ typedef struct _ht_pv\n\n int ht_add_table(str *name, int autoexp, str *dbtable, str *dbcols, int size,\n \t\tint dbmode, int itype, int_str *ival, int updateexpire,\n-\t\tint dmqreplicate);\n+\t\tint dmqreplicate, char coldelim, char colnull);\n int ht_init_tables(void);\n int ht_destroy(void);\n int ht_set_cell(ht_t *ht, str *name, int type, int_str *val, int mode);\ndiff --git a/src/modules/htable/ht_db.c b/src/modules/htable/ht_db.c\nindex 631788b1a5..7a22ff6c48 100644\n--- a/src/modules/htable/ht_db.c\n+++ b/src/modules/htable/ht_db.c\n@@ -121,7 +121,9 @@ static int ht_pack_values(\n \tlen = 0;\n \tfor(c = 1; c < cols; c++) {\n \t\tif(VAL_NULL(&RES_ROWS(db_res)[row].values[c])) {\n-\t\t\tlen += 1;\n+\t\t\tif(ht->pack[2] != '\\0') {\n+\t\t\t\tlen += 1;\n+\t\t\t}\n \t\t} else if(RES_ROWS(db_res)[row].values[c].type == DB1_STRING) {\n \t\t\tlen += strlen(RES_ROWS(db_res)[row].values[c].val.string_val);\n \t\t} else if(RES_ROWS(db_res)[row].values[c].type == DB1_STR) {\n@@ -143,8 +145,10 @@ static int ht_pack_values(\n \tp = vbuf;\n \tfor(c = 1; c < cols; c++) {\n \t\tif(VAL_NULL(&RES_ROWS(db_res)[row].values[c])) {\n-\t\t\t*p = ht->pack[2];\n-\t\t\tp++;\n+\t\t\tif(ht->pack[2] != '\\0') {\n+\t\t\t\t*p = ht->pack[2];\n+\t\t\t\tp++;\n+\t\t\t}\n \t\t} else if(RES_ROWS(db_res)[row].values[c].type == DB1_STRING) {\n \t\t\tstrcpy(p, RES_ROWS(db_res)[row].values[c].val.string_val);\n \t\t\tp += strlen(RES_ROWS(db_res)[row].values[c].val.string_val);\n"
  },
  {
    "path": "kamailio/kamdbctl.patch",
    "content": "diff --git a/utils/kamctl/kamdbctl.base b/utils/kamctl/kamdbctl.base\nindex 093334c024..488ebfa821 100644\n--- a/utils/kamctl/kamdbctl.base\n+++ b/utils/kamctl/kamdbctl.base\n@@ -20,6 +20,9 @@ DBROUSER=${DBROUSER:-kamailioro}\n # password for read-only user\n DBROPW=${DBROPW:-kamailioro}\n \n+# address of database server for root coonections\n+DBROOTHOST=${DBROOTHOST:-$DBHOST}\n+\n # user name column\n USERCOL=${USERCOL:-username}\n \ndiff --git a/utils/kamctl/kamdbctl.mysql b/utils/kamctl/kamdbctl.mysql\nindex 81a730bbe6..49915dc795 100644\n--- a/utils/kamctl/kamdbctl.mysql\n+++ b/utils/kamctl/kamdbctl.mysql\n@@ -27,23 +27,22 @@ fi\n # config vars\n #################################################################\n \n-# full privileges MySQL user\n-if [ -z \"$DBROOTUSER\" ]; then\n-\tDBROOTUSER=\"root\"\n-fi\n-\n # Set DBROOTPW in kamctlrc or via next line to set the database\n # root password if you want to run this script without any user prompt.\n # This is unsafe, but useful e.g. for automatic testing.\n #DBROOTPW=\"\"\n \n-\n-if [ -z \"$DBPORT\" ] ; then\n-\tCMD=\"mysql -h $DBHOST -u$DBROOTUSER \"\n-\tDUMP_CMD=\"mysqldump -h $DBHOST -u$DBROOTUSER -c -t \"\n-else\n-\tCMD=\"mysql -h $DBHOST -P $DBPORT -u$DBROOTUSER \"\n-\tDUMP_CMD=\"mysqldump -h $DBHOST -P $DBPORT -u$DBROOTUSER -c -t \"\n+# build the client base commands one param at a time\n+# let the client choose defaults where not specified\n+CMD=\"mysql -h $DBROOTHOST\"\n+DUMP_CMD=\"mysqldump -c -t -h $DBROOTHOST\"\n+if [ -n \"$DBROOTPORT\" ] ; then\n+\tCMD=\"$CMD -P $DBROOTPORT\"\n+\tDUMP_CMD=\"$DUMP_CMD -P $DBROOTPORT\"\n+fi\n+if [ -n \"$DBROOTUSER\" ]; then\n+  \tCMD=\"$CMD -u $DBROOTUSER\"\n+  \tDUMP_CMD=\"mysqldump -u $DBROOTUSER\"\n fi\n \n #################################################################\n"
  },
  {
    "path": "kamailio/modules/dsiprouter/Makefile",
    "content": "#\n# Usrloc module Makefile\n#\n# WARNING: do not run this directly, it should be run by the master Makefile\n\ninclude ../../Makefile.defs\nauto_gen=\nNAME=dsiprouter.so\nLIBS=-lssl -lcrypto\n\nSERLIBPATH=../../lib\nSER_LIBS+=$(SERLIBPATH)/srdb2/srdb2\ninclude ../../Makefile.modules\n"
  },
  {
    "path": "kamailio/modules/dsiprouter/README.md",
    "content": "## Instructions\n\n### assumptions\n\n- You have dSIPRouter and Kamailio installed\n\n\n### clone your kamailio version's branch:\n\n```\nKAM_VERSIONL=$(kamailio -v 2>/dev/null | grep '^version:' | awk '{print $3}' | sed -e  's/\\([0-9]\\.[0-9]\\)\\.[0-9]/\\1/')\nrm -rf /tmp/kamailio 2>/dev/null\ngit clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git /tmp/kamailio\n```\n\n### copy to src dir and compile:\n\n```\nDSIP_PROJECT_DIR=/opt/dsiprouter\ncp -rf ${DSIP_PROJECT_DIR}/kamailio/modules/dsiprouter/ /tmp/kamailio/src/modules/\ncd /tmp/kamailio/src/modules/dsiprouter\nmake\n```\n\n### copy to deployment location:\n\n```\nMPATH=$(grep mpath /etc/kamailio/kamailio.cfg | awk 'NR==2' | awk '{print $3}')\ncp /tmp/kamailio/src/modules/dsiprouter/*.so $MPATH\n```\n\n### load module in kamailio (/etc/kamailio/kamailio.cfg):\n\n```\nloadmodule \"dsiprouter.so\"\n```\n\n### restart kamailio\n\n```\nsystemctl restart kamailio\n```\n\n## Notes\n\n- Anytime Kamailio is upgraded (even patch releases) you must recompile this module.\n"
  },
  {
    "path": "kamailio/modules/dsiprouter/mod_dsiprouter.c",
    "content": "/*\n * mod_dsiprouter.c\n */\n/*!\n * \\file\n * \\brief dSipRouter Module Interface\n * \\ingroup dsiprouter\n * Module: \\ref dsiprouter\n */\n\n/**\n * @defgroup dsiprouter dsiprouter :: Kamailio dsiprouter module\n * @brief Kamailio dsiprouter module\n */\n\n#include <string.h>\n#include <stdio.h>\n\n#include \"../../core/ver_defs.h\"\n#include \"../../core/sr_module.h\"\n#include \"../../core/mod_fix.h\"\n#include \"../../core/rpc.h\"\n#include \"../../core/rpc_lookup.h\"\n\n#include \"mod_funcs.h\"\n\nMODULE_VERSION\n\n/* module params */\nstatic char *license_file = \"/etc/dsiprouter/license.txt\";\nstatic char *signature_file = \"/etc/dsiprouter/license-sig.b64\";\nstatic char *uuid_file = \"/etc/dsiprouter/uuid.txt\";\n\n/* Module management function prototypes */\nstatic int mod_init(void);\nstatic int mod_child_init(int rank);\nstatic void mod_destroy(void);\nstatic int fixup_get_params(void **param, int param_no);\nstatic int fixup_get_params_free(void **param, int param_no);\nstatic int mod_health_check();\nstatic void rpc_health_check(rpc_t *rpc, void *ctx);\n\n/* Exported functions */\nstatic cmd_export_t cmds[] = {\n\t{\"health_check\", (cmd_function) mod_health_check, 1,\n\tfixup_get_params, fixup_get_params_free, ANY_ROUTE},\n\t{0,0,                                             0,0,0}\n};\n\n/* Exported rpc functions */\nstatic const char *rpc_health_check_doc[2] = {\"Check health of dsiprouter system.\", 0};\nstatic rpc_export_t rpc_cmds[] = {\n\t\t{\"dsiprouter.health_check\", rpc_health_check, rpc_health_check_doc,0},\n\t\t{0,0,0,0}\n};\n\n/* Exported parameters */\nstatic param_export_t params[] = {\n\t\t{\"license_file\", PARAM_STRING, &license_file},\n\t\t{\"signature_file\", PARAM_STRING, &signature_file},\n\t\t{\"uuid_file\", PARAM_STRING, &uuid_file},\n\t\t{0,0,0}\n};\n\n/* Module interface */\nmodule_exports_t exports = {\n\t\t\"dsiprouter\", /* module name */\n\t\tDEFAULT_DLFLAGS, \t/* dlopen flags */\n\t\tcmds,      \t\t\t/* exported functions */\n\t\tparams,    \t\t\t/* exported parameters */\n\t\t0,   \t\t/* exported rpc functions */\n\t\t0,        \t/* exported pseudo-variables */\n\t\t0,        /* exported response function */\n\t\tmod_init,        \t/* exported initialization function */\n\t\tmod_child_init,\t\t/* exported child initialization function */\n\t\tmod_destroy,   \t\t/* exported destroy function */\n};\n\n/* Module initialization function - The main initialization function will be called\n   before any other function exported by the module. The function will be called only once,\n   before the main process forks. This function is good for initialization that is common\n   for all the children (processes). The function should return 0 if everything went OK\n   and a negative error code otherwise. Server will abort if the function returns a negative value.\n*/\nstatic int mod_init(void) {\n\tLM_DBG(\"mod_dsiprouter initializing\\n\");\n\tif (rpc_register_array(rpc_cmds) != 0) {\n\t\tLM_ERR(\"failed to register RPC commands\\n\");\n\t\treturn -1;\n\t}\n\treturn 0;\n}\n\nstatic int mod_child_init(int rank) {\n\treturn 0;\n}\n\nstatic void mod_destroy(void) {\n\tLM_DBG(\"mod_dsiprouter unloading\\n\");\n}\n\nstatic int fixup_get_params(void **param, int param_no) {\n\tif (param_no == 1) {\n\t\treturn fixup_pvar_null(param, 1);\n\t}\n\tLM_ERR(\"invalid parameter number <%d>\\n\", param_no);\n\treturn -1;\n}\n\nstatic int fixup_get_params_free(void **param, int param_no) {\n\tif (param_no == 1) {\n\t\treturn fixup_free_pvar_null(param, 1);\n\t}\n\tLM_ERR(\"invalid parameter number <%d>\\n\", param_no);\n\treturn -1;\n}\n\nstatic int mod_health_check() {\n\treturn validate_license(license_file, signature_file, uuid_file);\n}\n\nstatic void rpc_health_check(rpc_t *rpc, void *ctx) {\n\tif (!validate_license(license_file, signature_file, uuid_file)) {\n\t\trpc->fault(ctx, 500, \"Health Check Failed\");\n\t}\n\telse {\n\t\trpc->rpl_printf(ctx, \"Health Check Succeeded\");\n\t}\n}\n"
  },
  {
    "path": "kamailio/modules/dsiprouter/mod_funcs.c",
    "content": "#include <stdio.h>\n#include <string.h>\n#include <stdlib.h>\n#include <stdbool.h>\n#include <time.h>\n#include <openssl/pem.h>\n\nstatic char licensing_public_key[] = \"-----BEGIN PUBLIC KEY-----\\n\"\n\t\"MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0zcK4H4q9NWC4MkW7y2G\\n\"\n\t\"V/Tm91U5pnL+VkzwlrXSI/Eh45pGeNfosSVN2NGQciEjeDrcbPdP4QbWguHIDDmi\\n\"\n\t\"CJ0vFAMyHchNIJa5nt0QAW3V7nQ217PYLr0A3KVkVqGwR5+Z1i/1xEIuXy4ZHUqd\\n\"\n\t\"pJlYfkmJIcGgGGpUDoZhEB1zLySutIxArmuMqj6DNt9fYfsMCYTBjmVY2IJfgNha\\n\"\n\t\"zrLrQY+SNYjad1A0XuegZy48fKM9hqXR55ZO1yVZ3a7Mea9xwSsXcuAu3ZRL0kWt\\n\"\n\t\"p/yNWqAco26fJ00veqVA+rOT0qhW6VmRn9eE4pJoOhkUXYnw2xY5yo0oROAnuQ18\\n\"\n\t\"kZUzkfcHIVWjLqfK0+rW4Bmbx0jjYKZRo5kQKwWBghc+ASf9m5LARtj4qx9ihicl\\n\"\n\t\"gUdhdEQr4sVSYPoqSj5BTH/oaC04qw2bwx/TKFM2+YZ6O6fee85Su4pYTRznzGL0\\n\"\n\t\"7B4xReWpLfylAKkex+lmkVfeJ+O5ZwB/Id77oZhrghpi9ylMn+slopBnOyJlvz2t\\n\"\n\t\"2z6DVi1Ryn1p59t1b4VTyMTot3QaMGD3y8KRDvooDfY5jDtANirG0W9ugXlBOCyT\\n\"\n\t\"3ML7CaMgTcI24R33lVF/jtNfOMScKj+J9/d0qY6LIYf4U55oda4RUk+a++PW3fCm\\n\"\n\t\"eOUVXKmEsIkzo5YsAokiMeUCAwEAAQ==\\n\"\n\t\"-----END PUBLIC KEY-----\\n\";\n\n/**\n * Read file into an allocated buffer\n * @param path \tfile path\n * @return \t\tstring or NULL\n */\nchar *readFile(char *path) {\n\tchar *source = NULL;\n\tFILE *fp = fopen(path, \"rb\");\n\tif (fp != NULL) {\n\t\t/* Go to the end of the file. */\n\t\tif (fseek(fp, 0L, SEEK_END) == 0) {\n\t\t\t/* Get the size of the file. */\n\t\t\tsize_t bufsize = ftell(fp);\n\t\t\tif (bufsize == -1) {\n\t\t\t\tfprintf(stderr, \"Error reading file %s\\n\", path);\n\t\t\t\treturn NULL;\n\t\t\t}\n\n\t\t\t/* Allocate our buffer to that size. */\n\t\t\tsource = malloc(sizeof(char) * (bufsize + 1));\n\t\t\tif (source == NULL) {\n\t\t\t\tfprintf(stderr, \"Error reading file %s\\n\", path);\n\t\t\t\treturn NULL;\n\t\t\t}\n\n\t\t\t/* Go back to the start of the file. */\n\t\t\tif (fseek(fp, 0L, SEEK_SET) != 0) {\n\t\t\t\tfprintf(stderr, \"Error reading file %s\\n\", path);\n\t\t\t\tfree(source);\n\t\t\t\treturn NULL;\n\t\t\t}\n\n\t\t\t/* Read the entire file into memory. */\n\t\t\tsize_t fileLen = fread(source, sizeof(char), bufsize, fp);\n\t\t\tif (ferror(fp) != 0) {\n\t\t\t\tfprintf(stderr, \"Error reading file %s\\n\", path);\n\t\t\t\tfree(source);\n\t\t\t\treturn NULL;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tsource[fileLen++] = '\\0'; /* Just to be safe. */\n\t\t\t}\n\t\t}\n\t\tfclose(fp);\n\t}\n\treturn source;\n}\n\n/**\n Function for tokenizing strings (extends strtok function)\n * @param str \t\tstring to tokenize (in)\n * @param delims\tsubstring delimiters (in)\n * @param len \t\tlength of the returned string array (out)\n * @return \t\t\ta ptr to an array of ptrs to strings\n * @note\t\t\tconsecutive delims will return empty string in array\n * @note\t\t\tthe return array is terminated with a null ptr '\\0'\n */\nchar **strsplit(char *str, const char delims[], size_t *len) {\n\tchar *save, *tok;        /* holds tok val btwn calls */\n\tchar **result = NULL;    /* set result to NULL */\n\tchar *tmp = strdup(str); /* leaves original str intact */\n\tsize_t delims_size = strlen(delims);\n\tsize_t count = 0;        /* number of main strings */\n\tsize_t sub_count = 0;    /* number of substrings */\n\tint i = 0;\n\n\t/* get number of delims in str */\n\tdo {\n\t\ttmp += delims_size;\n\t\tcount++;\n\t} while ((tmp = strstr(tmp, delims)) != NULL);\n\tcount++; /* add one for trailing token */\n\ttmp = strdup(str);\n\n\tsave = malloc(sizeof(char) * strlen(str));\n\tresult = malloc(sizeof(char *) * count);\n\n\tif (result && save) {\n\t\twhile ((tok = strstr(tmp, delims)) != NULL) {\n\t\t\tstrncpy(save, tmp, (tok - tmp));\n\t\t\tsave[(tok - tmp)] = '\\0';\n//            printf(\"Token extracted: <<%s>>\\n\", save);\n\t\t\tresult[i++] = strdup(save);\n\t\t\ttok += delims_size;\n\t\t\ttmp = tok;\n\t\t} /* grab trailing token */\n\t\tif (tmp && tmp[0] != '\\0') {\n\t\t\tresult[i++] = strdup(tmp);\n//            printf(\"Token extracted: <<%s>>\\n\", tmp);\n\t\t} /* set last ptr to NULL */\n\t\tresult[i] = '\\0';\n\t\t*len = count; /* pass num toks */\n\t}\n\telse { /* set len to 0 on error */\n\t\t*len = 0;\n\t}\n\tif (save) free(save);\n\treturn result;\n}\n\n/**\n * Decodes a base64 encoded string\n * @param b64message\n * @param out_length\n * @return\n */\nunsigned char *b64decode(char *b64message, size_t *out_length) {\n\tBIO *b64_bio, *mem_bio;\n\tsize_t b64_len = strlen(b64message);\n\tunsigned char *base64_decoded = calloc((b64_len * 3) / 4 + 1, sizeof(char));\n\tb64_bio = BIO_new(BIO_f_base64());\n\tmem_bio = BIO_new(BIO_s_mem());\n\tBIO_write(mem_bio, b64message, b64_len);\n\tBIO_push(b64_bio, mem_bio);\n\tif (b64message[b64_len-1] != '\\n') {\n\t\tBIO_set_flags(b64_bio, BIO_FLAGS_BASE64_NO_NL);\n\t}\n\tint decoded_byte_index = 0;\n\twhile (0 < BIO_read(b64_bio, base64_decoded + decoded_byte_index, 1)) {\n\t\tdecoded_byte_index++;\n\t}\n\t*out_length = decoded_byte_index;\n\tBIO_free_all(b64_bio);\n\treturn base64_decoded;\n}\n\n/**\n * Create an RSA key info struct\n * @param key\n * @param public\n * @return\n */\nRSA *createRSA(unsigned char *key, int public) {\n\tRSA *rsa = NULL;\n\tBIO *keybio = NULL;\n\n\tkeybio = BIO_new_mem_buf(key, -1);\n\tif (keybio == NULL) {\n\t\tfprintf(stderr, \"Failed to create key BIO\\n\");\n\t\treturn NULL;\n\t}\n\tif (public) {\n\t\trsa = PEM_read_bio_RSA_PUBKEY(keybio, &rsa, NULL, NULL);\n\t}\n\telse {\n\t\trsa = PEM_read_bio_RSAPrivateKey(keybio, &rsa, NULL, NULL);\n\t}\n\tif (rsa == NULL) {\n\t\tfprintf(stderr, \"Failed to create RSA\\n\");\n\t\tfree(keybio);\n\t\treturn NULL;\n\t}\n\n\tif (keybio) { BIO_free(keybio); }\n\treturn rsa;\n}\n\n/**\n * Verify a binary signature using RSA\n * @param msg\n * @param msglen\n * @param sig\n * @param siglen\n * @param pubkey\n * @return\n */\nint verifyRSA(const unsigned char *msg, size_t msglen, unsigned char *sig, size_t siglen, unsigned char *pubkey) {\n\tunsigned char hash[SHA512_DIGEST_LENGTH];\n\n\tRSA *rsa = createRSA(pubkey, true);\n\tif (rsa == NULL) {\n\t\tgoto verifyRSA_failure;\n\t}\n\n\tif (!SHA512(msg, msglen, hash)) {\n\t\tgoto verifyRSA_failure;\n\t}\n\n\tif (!RSA_verify(NID_sha512, hash, sizeof(hash), sig, (unsigned int) siglen, rsa)) {\n\t\tgoto verifyRSA_failure;\n\t}\n\n\tif (rsa) { RSA_free(rsa); }\n\treturn true;\nverifyRSA_failure:\n\tif (rsa) { RSA_free(rsa); }\n\treturn false;\n}\n\n/**\n * Validate a dsiprouter license\n * current license format:\n * dsiprouter_unique_id,license_type,expiration_date\n * @param license_file \t\tpath to dsip license\n * @param signature_file\tpath to license signature\n * @param uuid_file\t\t\tpath to dsip uuid\n * @return \t\t\t\t\ttrue or false\n */\nint validate_license(char *license_file, char *signature_file, char *uuid_file) {\n\tchar *license = NULL, *dsip_uuid = NULL, *signature_b64 = NULL;\n\tunsigned char* signature = NULL;\n\tchar **fields = NULL;\n\tsize_t sig_len, fields_len = 0;\n\tint status = false;\n\n\t// get data from files\n\tlicense = readFile(license_file);\n\tdsip_uuid = readFile(uuid_file);\n\tsignature_b64 = readFile(signature_file);\n\tif (license == NULL || dsip_uuid == NULL || signature_b64 == NULL) {\n\t\tgoto validate_license_ret;\n\t}\n\n\t// validate license hasn't been changed using signature\n\tsignature = b64decode(signature_b64, &sig_len);\n\tif (signature == NULL) {\n\t\tgoto validate_license_ret;\n\t}\n\tif (!verifyRSA(license, strlen(license), signature, sig_len, licensing_public_key)) {\n\t\tgoto validate_license_ret;\n\t}\n\n\t// parse fields from license\n\tfields = strsplit(license, \",\", &fields_len);\n\tif (fields == NULL) {\n\t\tgoto validate_license_ret;\n\t}\n\n\t// validate this license is valid for this instance\n\tif (strcmp(fields[0], dsip_uuid) != 0) {\n\t\tgoto validate_license_ret;\n\t}\n\n\t// validate license type\n\tif (strcmp(fields[1], \"enterprise\") != 0) {\n\t\tgoto validate_license_ret;\n\t}\n\n\t// validate expiration date\n\ttime_t now = time(NULL);\n\ttime_t expires = (time_t) atoll(fields[2]);\n\tif (difftime(expires, now) <= 0) {\n\t\tgoto validate_license_ret;\n\t}\n\n\t// passed all checks\n\tstatus = true;\n\nvalidate_license_ret:\n\tif (license) { free(license); }\n\tif (signature_b64) { free(signature_b64); }\n\tif (signature) { free(signature); }\n\tif (dsip_uuid) { free(dsip_uuid); }\n\tif (fields) { free(fields); }\n\treturn status;\n}\n\n/* paths in real application should be:\n * license_file:\t/etc/dsiprouter/license.txt\n * signature_file:\t/etc/dsiprouter/license-sig.b64\n * uuid_file: \t\t/etc/dsiprouter/uuid.txt */\nint main() {\n\tif (validate_license(\"resources/license-good.txt\", \"resources/license-good-sig.b64\", \"resources/dsip-uuid.txt\")) {\n\t\tprintf(\"license-good.txt:\tvalid\\n\");\n\t}\n\telse {\n\t\tprintf(\"license-good.txt:\tinvalid\\n\");\n\t}\n\tif (validate_license(\"resources/license-bad.txt\", \"resources/license-bad-sig.b64\", \"resources/dsip-uuid.txt\")) {\n\t\tprintf(\"license-bad.txt:\tvalid\\n\");\n\t}\n\telse {\n\t\tprintf(\"license-bad.txt:\tinvalid\\n\");\n\t}\n\treturn EXIT_SUCCESS;\n}"
  },
  {
    "path": "kamailio/modules/dsiprouter/mod_funcs.h",
    "content": "#ifndef DSIPROUTER_LICENSING_MOD_FUNCS_H\n#define DSIPROUTER_LICENSING_MOD_FUNCS_H\n\n#include <stddef.h>\n#include <openssl/pem.h>\n\nchar *readFile(char *path);\nchar **strsplit(char *str, const char delims[], size_t *len);\nunsigned char *b64decode(char *b64message, size_t *out_length);\nRSA *createRSA(unsigned char *key, int public);\nint verifyRSA(const unsigned char *msg, size_t msglen, unsigned char *sig, size_t siglen, unsigned char *pubkey);\nint validate_license(char *license_file, char *signature_file, char *uuid_file);\n\n#endif //DSIPROUTER_LICENSING_MOD_FUNCS_H\n"
  },
  {
    "path": "kamailio/rhel/8.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local KAM_MINOR_VERSION=$(perl -pe 's%^([0-9])\\.([0-9]).*$%\\1.\\2%' <<<\"$KAM_VERSION\")\n    local RHEL_BASE_VER=$(rpm -E %{rhel})\n    local NPROC=$(nproc)\n    local OS_ARCH=$(uname -m)\n\n    # Install Dependencies\n    dnf groupinstall --setopt=group_package_types=mandatory,default,optional -y 'Development Tools'\n    dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-${DISTRO_MAJOR_VER}.noarch.rpm\n    dnf install -y psmisc curl wget sed gawk perl firewalld openssl-devel logrotate rsyslog python3 libuuid-devel \\\n        libtool jansson-devel libcurl-devel libatomic python3-virtualenv policycoreutils-python-utils\n\n    # we need a newer version of certbot than the distro repos offer\n    dnf remove -y *certbot*\n    python3 -m venv /opt/certbot/\n    /opt/certbot/bin/pip install --upgrade pip\n    /opt/certbot/bin/pip install certbot\n    ln -sf /opt/certbot/bin/certbot /usr/bin/certbot\n\n    dnf install -y kernel-modules-extra-$(uname -r) || {\n        printwarn 'could not install kernel modules for current kernel'\n        echo 'upgrading kernel and installing new modules'\n        printwarn 'you will need to reboot the machine for changes to take effect'\n        dnf install -y kernel-modules-extra\n    }\n\n    if (( $? == 0 )); then\n        echo 'sctp' >/etc/modules-load.d/sctp.conf\n        sed -i -re 's%^blacklist sctp%#blacklist sctp%g' /etc/modprobe.d/*\n        modprobe sctp\n    else\n        printwarn 'Could not install kernel modules for SCTP support. Continuing installation...'\n    fi\n\n    # create kamailio user and group\n    mkdir -p /var/run/kamailio\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel kamailio &>/dev/null; groupdel kamailio &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    # Add the Kamailio repos to yum\n    (cat << EOF\n[kamailio]\nname=Kamailio\nbaseurl=https://rpm.kamailio.org/centos/${RHEL_BASE_VER}/${KAM_MINOR_VERSION}/${KAM_VERSION}/\\$basearch/\nenabled=1\nmetadata_expire=30d\ngpgcheck=1\nrepo_gpgcheck=0\ngpgkey=https://rpm.kamailio.org/rpm-pub.key\ntype=rpm\nEOF\n    ) > /etc/yum.repos.d/kamailio.repo\n\n    dnf clean -y metadata\n    dnf makecache -y\n    dnf install -y kamailio kamailio-ldap kamailio-mysql kamailio-sipdump kamailio-websocket kamailio-postgresql kamailio-debuginfo \\\n        kamailio-xmpp kamailio-unixodbc kamailio-utils kamailio-tls kamailio-presence kamailio-outbound kamailio-gzcompress \\\n        kamailio-http_async_client kamailio-dmq_userloc kamailio-jansson kamailio-json kamailio-sctp\n\n    # workaround for kamailio rpm transaction failures\n    if (( $? != 0 )); then\n        rpm --import $(grep 'gpgkey' /etc/yum.repos.d/kamailio.repo | cut -d '=' -f 2)\n        REPOS='kamailio kamailio-ldap kamailio-mysql kamailio-postgresql kamailio-debuginfo kamailio-xmpp kamailio-unixodbc kamailio-utils kamailio-tls kamailio-presence kamailio-outbound kamailio-gzcompress'\n        for REPO in $REPOS; do\n            yum install -y $(grep 'baseurl' /etc/yum.repos.d/kamailio.repo | cut -d '=' -f 2)$(uname -m)/$(repoquery -i ${REPO} | head -4 | tail -n 3 | tr -d '[:blank:]' | cut -d ':' -f 2 | perl -pe 'chomp if eof' | tr '\\n' '-').$(uname -m).rpm\n        done\n    fi\n\n    # get info about the kamailio install for later use in script\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n    # create kamailio tmp files\n    echo \"d /run/kamailio 0750 kamailio kamailio\" > /etc/tmpfiles.d/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR} ${BACKUPS_DIR}/kamailio\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${BACKUPS_DIR}/kamailio/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    (cat << EOF\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n#STORE_PLAINTEXT_PW=0\nEOF\n    ) > ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create\n\n    # give kamailio permissions in SELINUX\n    semanage port -a -t sip_port_t -p udp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIPS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIPS_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_WSS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_WSS_PORT}\n    semanage port -a -t sip_port_t -p udp ${KAM_DMQ_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_DMQ_PORT}\n\n    # Start firewalld\n    systemctl start firewalld\n    systemctl enable firewalld\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Make sure MariaDB and Local DNS start before Kamailio\n    if ! grep -q v 'mariadb.service dnsmasq.service' /lib/systemd/system/kamailio.service 2>/dev/null; then\n        sed -i -r -e 's/(After=.*)/\\1 mariadb.service dnsmasq.service/' /lib/systemd/system/kamailio.service\n    fi\n    if ! grep -q v \"${DSIP_PROJECT_DIR}/dsiprouter.sh updatednsconfig\" /lib/systemd/system/kamailio.service 2>/dev/null; then\n        sed -i -r -e \"0,\\|^ExecStart.*|{s||ExecStartPre=-${DSIP_PROJECT_DIR}/dsiprouter.sh updatednsconfig\\n&|}\" /lib/systemd/system/kamailio.service\n    fi\n    systemctl daemon-reload\n\n    # Enable Kamailio for system startup\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-bundle.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libks\n    if [[ ! -d ${SRC_DIR}/libks ]]; then\n        git clone --single-branch -c advice.detachedHead=false https://github.com/signalwire/libks -b v1.8.3 ${SRC_DIR}/libks\n    fi\n    (\n        cd ${SRC_DIR}/libks &&\n        cmake -DCMAKE_BUILD_TYPE=Release . &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libks'\n        return 1\n    }\n\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    (\n        cd ${SRC_DIR}/libstirshaken &&\n        ./bootstrap.sh &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 &&\n        make -j $NPROC &&\n        make -j $NPROC install &&\n        ldconfig\n    ) || {\n        printerr 'Failed to compile and install libstirshaken'\n        return 1\n    }\n\n    ## compile and install STIR/SHAKEN module\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/stirshaken &&\n        make -j $NPROC\n    ) &&\n    cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/ || {\n        printerr 'Failed to compile and install STIR/SHAKEN module'\n        return 1\n    }\n\n    # patch uac module to support reload_delta\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/uac &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/uac.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/uac/uac.so ${KAM_MODULES_DIR}/\n    ) || {\n        printerr 'Failed to patch uac module'\n        return 1\n    }\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop servers\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR} ${SYSTEM_KAMAILIO_CONFIG_DIR}.bak.$(date +%Y%m%d_%H%M%S)\n\n    # Uninstall Kamailio modules\n    yum remove -y kamailio\\*\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "kamailio/rhel/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local KAM_MINOR_VERSION=$(perl -pe 's%^([0-9])\\.([0-9]).*$%\\1.\\2%' <<<\"$KAM_VERSION\")\n    local RHEL_BASE_VER=$(rpm -E %{rhel})\n    local NPROC=$(nproc)\n\n    # Install Dependencies\n    {\n        dnf config-manager -y --set-enabled codeready-builder-for-rhel-9-$(uname -m)-rpms ||\n        dnf config-manager -y --set-enabled codeready-builder-for-rhel-9-rhui-rpms\n    } &&\n    dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm &&\n    dnf groupinstall -y 'core' &&\n    dnf groupinstall -y 'base' &&\n    dnf groupinstall -y 'Development Tools' &&\n    dnf install -y git curl perl firewalld logrotate rsyslog certbot cmake libuuid-devel \\\n        libcurl-devel libjwt-devel libatomic openssl-devel policycoreutils-python-utils \\\n        libks-devel\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    dnf install -y kernel-modules-extra-$(uname -r) || {\n        printwarn 'could not install kernel modules for current kernel'\n        echo 'upgrading kernel and installing new modules'\n        printwarn 'you will need to reboot the machine for changes to take effect'\n        dnf install -y kernel-modules-extra\n    }\n\n    if (( $? == 0 )); then\n        echo 'sctp' >/etc/modules-load.d/sctp.conf\n        sed -i -re 's%^blacklist sctp%#blacklist sctp%g' /etc/modprobe.d/*\n        modprobe sctp\n    else\n        printwarn 'Could not install kernel modules for SCTP support. Continuing installation...'\n    fi\n\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel kamailio &>/dev/null; groupdel kamailio &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n\n    # TODO: fix upstream kamailio.repo file\n    #dnf config-manager -y --add-repo https://rpm.kamailio.org/centos/kamailio.repo &&\n    #dnf config-manager --disable 'kamailio*' &&\n    #dnf config-manager --enable \"kamailio-$KAM_VERSION_DOTTED\" &&\n\n    # Add the Kamailio repos to yum\n    (cat << EOF\n[kamailio]\nname=Kamailio\nbaseurl=https://rpm.kamailio.org/centos/${RHEL_BASE_VER}/${KAM_MINOR_VERSION}/${KAM_VERSION}/\\$basearch/\nenabled=1\nmetadata_expire=30d\ngpgcheck=1\nrepo_gpgcheck=0\ngpgkey=https://rpm.kamailio.org/rpm-pub.key\ntype=rpm\nEOF\n    ) > /etc/yum.repos.d/kamailio.repo\n\n    dnf clean -y metadata\n    dnf makecache -y\n    dnf install -y kamailio kamailio-ldap kamailio-mysql kamailio-sipdump kamailio-websocket kamailio-postgresql kamailio-debuginfo \\\n        kamailio-xmpp kamailio-unixodbc kamailio-utils kamailio-tls kamailio-presence kamailio-outbound kamailio-gzcompress \\\n        kamailio-http_async_client kamailio-dmq_userloc kamailio-jansson kamailio-json kamailio-uuid kamailio-sctp\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing kamailio packages'\n        return 1\n    fi\n\n    # get info about the kamailio install for later use in script\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    # make sure run dir exists\n    mkdir -p /var/run/kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n\n    touch /etc/tmpfiles.d/kamailio.conf\n    echo \"d /run/kamailio 0750 kamailio users\" > /etc/tmpfiles.d/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR} ${BACKUPS_DIR}/kamailio\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${BACKUPS_DIR}/kamailio/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    cat <<EOF >${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n#STORE_PLAINTEXT_PW=0\nEOF\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create\n\n    # give kamailio permissions in SELINUX\n    semanage port -a -t sip_port_t -p udp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIPS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIPS_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_WSS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_WSS_PORT}\n    semanage port -a -t sip_port_t -p udp ${KAM_DMQ_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_DMQ_PORT}\n\n    # Start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Configure Kamailio systemd service\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio-v2.service /lib/systemd/system/kamailio.service\n    chmod 644 /lib/systemd/system/kamailio.service\n    systemctl daemon-reload\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-bundle.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    (\n        cd ${SRC_DIR}/libstirshaken &&\n        ./bootstrap.sh &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 &&\n        make -j $NPROC CFLAGS='-Wno-deprecated-declarations' &&\n        make -j $NPROC install &&\n        ldconfig\n    ) || {\n        printerr 'Failed to compile and install libstirshaken'\n        return 1\n    }\n\n    ## compile and install STIR/SHAKEN module\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/stirshaken &&\n        make -j $NPROC\n    ) &&\n    cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/ || {\n        printerr 'Failed to compile and install STIR/SHAKEN module'\n        return 1\n    }\n\n    # patch uac module to support reload_delta\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/uac &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/uac.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/uac/uac.so ${KAM_MODULES_DIR}/\n    ) || {\n        printerr 'Failed to patch uac module'\n        return 1\n    }\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop servers\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR} ${SYSTEM_KAMAILIO_CONFIG_DIR}.bak.$(date +%Y%m%d_%H%M%S)\n\n    # Uninstall Kamailio modules\n    dnf remove -y kamailio\\*\n\n    # remove our selinux changes\n    semanage port -D -t sip_port_t -p udp\n    semanage port -D -t sip_port_t -p tcp\n    semanage port -D -t rabbitmq_port_t -p udp\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "kamailio/rocky/8.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local KAM_MINOR_VERSION=$(perl -pe 's%^([0-9])\\.([0-9]).*$%\\1.\\2%' <<<\"$KAM_VERSION\")\n    local RHEL_BASE_VER=$(rpm -E %{rhel})\n    local NPROC=$(nproc)\n\n    # Install Dependencies\n    dnf config-manager --enable -y powertools &&\n    dnf install -y epel-release &&\n    dnf groupinstall --setopt=group_package_types=mandatory,default,optional -y 'core' &&\n    dnf groupinstall --setopt=group_package_types=mandatory,default,optional -y 'base' &&\n    dnf groupinstall --setopt=group_package_types=mandatory,default,optional -y 'Development Tools' &&\n    dnf install -y psmisc curl wget sed gawk vim perl firewalld logrotate rsyslog \\\n        uuid openssl-devel libuuid-devel libjwt-devel libatomic bzip2-devel libffi-devel libcurl-devel \\\n        python3.11 python3.11-pip policycoreutils-python-utils\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        exit 1\n    fi\n\n    # we need a newer version of certbot than the distro repos offer\n    dnf remove -y *certbot*\n    python3 -m venv --upgrade-deps /opt/certbot/\n    /opt/certbot/bin/pip install certbot\n    ln -sf /opt/certbot/bin/certbot /usr/bin/certbot\n\n    yum install -y kernel-modules-extra-$(uname -r) || {\n        printwarn 'could not install kernel modules for current kernel'\n        echo 'upgrading kernel and installing new modules'\n        printwarn 'you will need to reboot the machine for changes to take effect'\n        yum install -y kernel-modules-extra\n    }\n\n    if (( $? == 0 )); then\n        echo 'sctp' >/etc/modules-load.d/sctp.conf\n        sed -i -re 's%^blacklist sctp%#blacklist sctp%g' /etc/modprobe.d/*\n        modprobe sctp\n    else\n        printwarn 'Could not install kernel modules for SCTP support. Continuing installation...'\n    fi\n\n    # create kamailio user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    # Add the Kamailio repos to yum\n    (cat << EOF\n[kamailio]\nname=Kamailio\nbaseurl=https://rpm.kamailio.org/centos/${RHEL_BASE_VER}/${KAM_MINOR_VERSION}/${KAM_VERSION}/\\$basearch/\nenabled=1\nmetadata_expire=30d\ngpgcheck=1\nrepo_gpgcheck=0\ngpgkey=https://rpm.kamailio.org/rpm-pub.key\ntype=rpm\nEOF\n    ) > /etc/yum.repos.d/kamailio.repo\n\n    dnf clean -y metadata\n    dnf makecache -y\n    dnf install -y kamailio kamailio-ldap kamailio-mysql kamailio-sipdump kamailio-websocket \\\n        kamailio-postgresql kamailio-debuginfo kamailio-xmpp kamailio-unixodbc kamailio-utils kamailio-tls \\\n        kamailio-presence kamailio-outbound kamailio-gzcompress kamailio-http_async_client kamailio-dmq_userloc \\\n        kamailio-sctp\n\n    # workaround for kamailio rpm transaction failures\n    if (( $? != 0 )); then\n        rpm --import $(grep 'gpgkey' /etc/yum.repos.d/kamailio.repo | cut -d '=' -f 2)\n        REPOS='kamailio kamailio-ldap kamailio-mysql kamailio-postgresql kamailio-debuginfo kamailio-xmpp kamailio-unixodbc kamailio-utils kamailio-tls kamailio-presence kamailio-outbound kamailio-gzcompress'\n        for REPO in $REPOS; do\n            yum install -y $(grep 'baseurl' /etc/yum.repos.d/kamailio.repo | cut -d '=' -f 2)$(uname -m)/$(repoquery -i ${REPO} | head -4 | tail -n 3 | tr -d '[:blank:]' | cut -d ':' -f 2 | perl -pe 'chomp if eof' | tr '\\n' '-').$(uname -m).rpm\n        done\n    fi\n\n    # get info about the kamailio install for later use in script\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    touch /etc/tmpfiles.d/kamailio.conf\n    echo \"d /run/kamailio 0750 kamailio users\" > /etc/tmpfiles.d/kamailio.conf\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR}\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    (cat << EOF\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n# STORE_PLAINTEXT_PW=0\nEOF\n    ) > ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create\n\n    # give kamailio permissions in SELINUX\n    semanage port -a -t sip_port_t -p udp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIPS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIPS_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_WSS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_WSS_PORT}\n    semanage port -a -t sip_port_t -p udp ${KAM_DMQ_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_DMQ_PORT}\n\n    # Start firewalld\n    systemctl start firewalld\n    systemctl enable firewalld\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Make sure MariaDB and Local DNS start before Kamailio\n    if ! grep -q v 'mariadb.service dnsmasq.service' /lib/systemd/system/kamailio.service 2>/dev/null; then\n        sed -i -r -e 's/(After=.*)/\\1 mariadb.service dnsmasq.service/' /lib/systemd/system/kamailio.service\n    fi\n    if ! grep -q v \"${DSIP_PROJECT_DIR}/dsiprouter.sh updatednsconfig\" /lib/systemd/system/kamailio.service 2>/dev/null; then\n        sed -i -r -e \"0,\\|^ExecStart.*|{s||ExecStartPre=-${DSIP_PROJECT_DIR}/dsiprouter.sh updatednsconfig\\n&|}\" /lib/systemd/system/kamailio.service\n    fi\n    systemctl daemon-reload\n\n    # Enable Kamailio for system startup\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-bundle.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libks\n    if [[ ! -d ${SRC_DIR}/libks ]]; then\n        git clone --single-branch -c advice.detachedHead=false https://github.com/signalwire/libks -b v1.8.3 ${SRC_DIR}/libks\n    fi\n    (\n        cd ${SRC_DIR}/libks &&\n        cmake -DCMAKE_BUILD_TYPE=Release . &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libks'\n        return 1\n    }\n\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    (\n        cd ${SRC_DIR}/libstirshaken &&\n        ./bootstrap.sh &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 &&\n        make -j $NPROC &&\n        make -j $NPROC install &&\n        ldconfig\n    ) || {\n        printerr 'Failed to compile and install libstirshaken'\n        return 1\n    }\n\n    ## compile and install STIR/SHAKEN module\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/stirshaken &&\n        make -j $NPROC\n    ) &&\n    cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/ || {\n        printerr 'Failed to compile and install STIR/SHAKEN module'\n        return 1\n    }\n\n    # patch uac module to support reload_delta\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/uac &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/uac.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/uac/uac.so ${KAM_MODULES_DIR}/\n    ) || {\n        printerr 'Failed to patch uac module'\n        return 1\n    }\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop servers\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR} ${SYSTEM_KAMAILIO_CONFIG_DIR}.bak.$(date +%Y%m%d_%H%M%S)\n\n    # Uninstall Kamailio modules\n    yum remove -y kamailio\\*\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "kamailio/rocky/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local KAM_MINOR_VERSION=$(perl -pe 's%^([0-9])\\.([0-9]).*$%\\1.\\2%' <<<\"$KAM_VERSION\")\n    local RHEL_BASE_VER=$(rpm -E %{rhel})\n    local NPROC=$(nproc)\n\n    # Install Dependencies\n    dnf install -y epel-release &&\n    {\n        # TODO: fix upstream kamailio.repo file\n        #dnf config-manager -y --add-repo https://rpm.kamailio.org/centos/kamailio.repo &&\n        #dnf config-manager --disable 'kamailio*' &&\n        #dnf config-manager --enable \"kamailio-$KAM_VERSION_DOTTED\" &&\n\n        # Add the Kamailio repos to yum\n        (cat <<EOF\n[kamailio]\nname=Kamailio\nbaseurl=https://rpm.kamailio.org/centos/${RHEL_BASE_VER}/${KAM_MINOR_VERSION}/${KAM_VERSION}/\\$basearch/\nenabled=1\nmetadata_expire=30d\ngpgcheck=1\nrepo_gpgcheck=0\ngpgkey=https://rpm.kamailio.org/rpm-pub.key\ntype=rpm\nEOF\n        ) >/etc/yum.repos.d/kamailio.repo &&\n        dnf makecache -y\n    } &&\n    dnf groupinstall -y 'core' &&\n    dnf groupinstall -y 'base' &&\n    dnf groupinstall -y 'Development Tools' &&\n    dnf install -y git curl perl firewalld logrotate rsyslog certbot cmake libuuid-devel \\\n        libcurl-devel libjwt-devel libatomic openssl-devel policycoreutils-python-utils \\\n        libks-devel\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        return 1\n    fi\n\n    dnf install -y kernel-modules-extra-$(uname -r) || {\n        printwarn 'could not install kernel modules for current kernel'\n        echo 'upgrading kernel and installing new modules'\n        printwarn 'you will need to reboot the machine for changes to take effect'\n        dnf install -y kernel-modules-extra\n    }\n\n    if (( $? == 0 )); then\n        echo 'sctp' >/etc/modules-load.d/sctp.conf\n        sed -i -re 's%^blacklist sctp%#blacklist sctp%g' /etc/modprobe.d/*\n        modprobe sctp\n    else\n        printwarn 'Could not install kernel modules for SCTP support. Continuing installation...'\n    fi\n\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel kamailio &>/dev/null; groupdel kamailio &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n\n    dnf install -y kamailio kamailio-ldap kamailio-mysql kamailio-sipdump kamailio-websocket kamailio-postgresql kamailio-debuginfo \\\n        kamailio-xmpp kamailio-unixodbc kamailio-utils kamailio-tls kamailio-presence kamailio-outbound kamailio-gzcompress \\\n        kamailio-http_async_client kamailio-dmq_userloc kamailio-jansson kamailio-json kamailio-uuid kamailio-sctp\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing kamailio packages'\n        return 1\n    fi\n\n    # get info about the kamailio install for later use in script\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    # make sure run dir exists\n    mkdir -p /var/run/kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n\n    touch /etc/tmpfiles.d/kamailio.conf\n    echo \"d /run/kamailio 0750 kamailio users\" > /etc/tmpfiles.d/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR} ${BACKUPS_DIR}/kamailio\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${BACKUPS_DIR}/kamailio/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    cat <<EOF >${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n#STORE_PLAINTEXT_PW=0\nEOF\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create\n\n    # give kamailio permissions in SELINUX\n    semanage port -a -t sip_port_t -p udp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIP_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIP_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_SIPS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_SIPS_PORT}\n    semanage port -a -t sip_port_t -p tcp ${KAM_WSS_PORT} || semanage port -m -t sip_port_t -p tcp ${KAM_WSS_PORT}\n    semanage port -a -t sip_port_t -p udp ${KAM_DMQ_PORT} || semanage port -m -t sip_port_t -p udp ${KAM_DMQ_PORT}\n\n    # Start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Configure Kamailio systemd service\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio-v2.service /lib/systemd/system/kamailio.service\n    chmod 644 /lib/systemd/system/kamailio.service\n    systemctl daemon-reload\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-bundle.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    (\n        cd ${SRC_DIR}/libstirshaken &&\n        ./bootstrap.sh &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 &&\n        make -j $NPROC CFLAGS='-Wno-deprecated-declarations' &&\n        make -j $NPROC install &&\n        ldconfig\n    ) || {\n        printerr 'Failed to compile and install libstirshaken'\n        return 1\n    }\n\n    ## compile and install STIR/SHAKEN module\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/stirshaken &&\n        make -j $NPROC\n    ) &&\n    cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/ || {\n        printerr 'Failed to compile and install STIR/SHAKEN module'\n        return 1\n    }\n\n    # patch uac module to support reload_delta\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/uac &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/uac.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/uac/uac.so ${KAM_MODULES_DIR}/\n    ) || {\n        printerr 'Failed to patch uac module'\n        return 1\n    }\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop servers\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR} ${SYSTEM_KAMAILIO_CONFIG_DIR}.bak.$(date +%Y%m%d_%H%M%S)\n\n    # Uninstall Kamailio modules\n    dnf remove -y kamailio\\*\n\n    # remove our selinux changes\n    semanage port -D -t sip_port_t -p udp\n    semanage port -D -t sip_port_t -p tcp\n    semanage port -D -t rabbitmq_port_t -p udp\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "kamailio/stir_shaken.patch",
    "content": "diff --git a/include/stir_shaken.h b/include/stir_shaken.h\nindex d408be5..4f5e08d 100644\n--- a/include/stir_shaken.h\n+++ b/include/stir_shaken.h\n@@ -19,6 +19,7 @@ extern \"C\" {\n \n #include <pthread.h>\n \n+#include <openssl/opensslv.h>\n #include <openssl/crypto.h>\n #include <openssl/pem.h>\n #include <openssl/rand.h>\ndiff --git a/src/stir_shaken.c b/src/stir_shaken.c\nindex afe28e2..a755d6b 100644\n--- a/src/stir_shaken.c\n+++ b/src/stir_shaken.c\n@@ -723,7 +723,13 @@ stir_shaken_status_t stir_shaken_is_key_trusted(stir_shaken_context_t *ss, EVP_P\n \t}\n \n \t// Let SSL confirm\n-\tif (!EVP_PKEY_cmp(pkey, candidate_pkey)) {\n+\tif (!\n+#if OPENSSL_VERSION_NUMBER >= 0x30000000L\n+\t\tEVP_PKEY_eq(pkey, candidate_pkey)\n+#else\n+\t\tEVP_PKEY_cmp(pkey, candidate_pkey)\n+#endif\n+\t) {\n \t\treturn STIR_SHAKEN_STATUS_FALSE;\n \t}\n \ndiff --git a/test/stir_shaken_test_12.c b/test/stir_shaken_test_12.c\nindex 43caa1e..265aa1f 100644\n--- a/test/stir_shaken_test_12.c\n+++ b/test/stir_shaken_test_12.c\n@@ -101,7 +101,13 @@ stir_shaken_status_t stir_shaken_unit_test_x509_cert_path_verification(void)\n \tsnprintf(ca.tn_auth_list_uri, STIR_SHAKEN_BUFLEN, \"http://ca.com/api\");\n \t//sp.cert.x = stir_shaken_generate_x509_cert_from_csr(&ss, sp.code, sp.csr.req, ca.keys.private_key, ca.issuer_c, ca.issuer_cn, sp.serial, sp.expiry_days);\n \tpkey = X509_REQ_get_pubkey(sp.csr.req);\n-\tstir_shaken_assert(1 == EVP_PKEY_cmp(pkey, sp.keys.public_key), \"Public key in CSR different than SP's\");\n+\tstir_shaken_assert(1 == \n+#if OPENSSL_VERSION_NUMBER >= 0x30000000L\n+                EVP_PKEY_eq(pkey, sp.keys.public_key),\n+#else\n+                EVP_PKEY_cmp(pkey, sp.keys.public_key),\n+#endif\n+\t\t\"Public key in CSR different than SP's\");\n \t//sp.cert.x = stir_shaken_generate_x509_end_entity_cert(&ss, ca.cert.x, ca.keys.private_key, sp.keys.public_key, ca.issuer_c, ca.issuer_cn, sp.subject_c, sp.subject_cn, ca.serial_sp, ca.expiry_days_sp, ca.number_start_sp, ca.number_end_sp);\n \tsp.cert.x = stir_shaken_generate_x509_end_entity_cert_from_csr(&ss, ca.cert.x, ca.keys.private_key, ca.issuer_c, ca.issuer_cn, sp.csr.req, ca.serial, ca.expiry_days, ca.tn_auth_list_uri);\n \tPRINT_SHAKEN_ERROR_IF_SET\ndiff --git a/test/stir_shaken_test_17.c b/test/stir_shaken_test_17.c\nindex aa862b3..7fde1be 100644\n--- a/test/stir_shaken_test_17.c\n+++ b/test/stir_shaken_test_17.c\n@@ -153,7 +153,13 @@ stir_shaken_status_t stir_shaken_unit_test_vs_verify(void)\n \tsnprintf(ca.tn_auth_list_uri, STIR_SHAKEN_BUFLEN, \"http://ca.com/api\");\n \t//sp.cert.x = stir_shaken_generate_x509_cert_from_csr(&ss, sp.code, sp.csr.req, ca.keys.private_key, ca.issuer_c, ca.issuer_cn, sp.serial, sp.expiry_days);\n \tpkey = X509_REQ_get_pubkey(sp.csr.req);\n-\tstir_shaken_assert(1 == EVP_PKEY_cmp(pkey, sp.keys.public_key), \"Public key in CSR different than SP's\");\n+\tstir_shaken_assert(1 == \n+#if OPENSSL_VERSION_NUMBER >= 0x30000000L\n+                EVP_PKEY_eq(pkey, sp.keys.public_key),\n+#else\n+                EVP_PKEY_cmp(pkey, sp.keys.public_key),\n+#endif\n+\t\t\"Public key in CSR different than SP's\");\n \t//sp.cert.x = stir_shaken_generate_x509_end_entity_cert(&ss, ca.cert.x, ca.keys.private_key, sp.keys.public_key, ca.issuer_c, ca.issuer_cn, sp.subject_c, sp.subject_cn, ca.serial_sp, ca.expiry_days_sp, ca.number_start_sp, ca.number_end_sp);\n \tsp.cert.x = stir_shaken_generate_x509_end_entity_cert_from_csr(&ss, ca.cert.x, ca.keys.private_key, ca.issuer_c, ca.issuer_cn, sp.csr.req, ca.serial, ca.expiry_days, ca.tn_auth_list_uri);\n \tPRINT_SHAKEN_ERROR_IF_SET\n"
  },
  {
    "path": "kamailio/systemd/kamailio-v1.service",
    "content": "[Unit]\nDescription=Kamailio - the Open Source SIP Server\nRequires=basic.target network.target\nAfter=network.target network-online.target systemd-journald.socket basic.target\nAfter=rsyslog.service dnsmasq.service mariadb.service rtpengine.service\nWants=mariadb.service\nDefaultDependencies=no\n\n[Service]\nType=forking\nUser=kamailio\nGroup=kamailio\nPermissionsStartOnly=true\nEnvironment='RUNDIR=/run/kamailio'\nEnvironmentFile=/etc/default/kamailio.conf\n# PIDFile requires a full absolute path\nPIDFile=/run/kamailio/kamailio.pid\n# Exec* requires a full absolute path\nExecStartPre=/usr/bin/dsiprouter chown -certs -kamailio\nExecStart=/usr/sbin/kamailio -P $PIDFILE -f $CFGFILE -m $SHM_MEMORY -M $PKG_MEMORY --atexit=no\nRestart=on-failure\n# necessary for chown of control files e.g. for jsonrpcs and ctl modules\nAmbientCapabilities=CAP_CHOWN\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "kamailio/systemd/kamailio-v2.service",
    "content": "[Unit]\nDescription=Kamailio - the Open Source SIP Server\nRequires=basic.target network.target\nAfter=network.target network-online.target systemd-journald.socket basic.target\nAfter=rsyslog.service dnsmasq.service mariadb.service rtpengine.service\nWants=mariadb.service\nDefaultDependencies=no\n\n[Service]\nType=forking\nUser=kamailio\nGroup=kamailio\nEnvironment='RUNDIR=/run/kamailio'\nEnvironmentFile=/etc/default/kamailio.conf\nEnvironmentFile=-/etc/default/kamailio.d/*.conf\n# PIDFile requires a full absolute path\nPIDFile=/run/kamailio/kamailio.pid\n# Exec* requires a full absolute path\nExecStartPre=!-/usr/bin/dsiprouter chown -certs -kamailio\nExecStart=/usr/sbin/kamailio -P $PIDFILE -f $CFGFILE -m $SHM_MEMORY -M $PKG_MEMORY --atexit=no\nRestart=on-failure\n# /run/kamailio in tmpfs\nRuntimeDirectory=kamailio\nRuntimeDirectoryMode=0770\n# necessary for chown of control files e.g. for jsonrpcs and ctl modules\nAmbientCapabilities=CAP_CHOWN\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "kamailio/systemd/kamailio.conf",
    "content": "RUN_KAMAILIO=yes\nUSER=kamailio\nGROUP=kamailio\nSHM_MEMORY=512\nPKG_MEMORY=64\nPIDFILE=/run/kamailio/kamailio.pid\nCFGFILE=/etc/kamailio/kamailio.cfg\n#DUMP_CORE=yes"
  },
  {
    "path": "kamailio/uac.patch",
    "content": "diff --git a/src/modules/uac/uac_reg.c b/src/modules/uac/uac_reg.c\n--- a/src/modules/uac/uac_reg.c\t(revision aa335e6138414f764dcf7287db271207f76bc6d8)\n+++ b/src/modules/uac/uac_reg.c\t(date 1713982080938)\n@@ -63,6 +63,7 @@\n #define UAC_REG_DB_COLS_NUM 15\n \n int _uac_reg_gc_interval = 150;\n+int _uac_reg_reload_delta = 30;\n \n typedef struct _reg_uac\n {\n@@ -393,11 +394,11 @@\n \ttn = time(NULL);\n \n \tlock_get(_reg_htable_gc_lock);\n-\tif(_reg_htable_gc->stime > tn - _uac_reg_gc_interval) {\n+\tif(_reg_htable_gc->stime > tn - _uac_reg_reload_delta) {\n \t\tlock_release(_reg_htable_gc_lock);\n \t\tLM_ERR(\"shifting in-memory table is not possible in less than %d \"\n \t\t\t   \"secs\\n\",\n-\t\t\t\t_uac_reg_gc_interval);\n+\t\t\t   _uac_reg_reload_delta);\n \t\treturn -1;\n \t}\n \tuac_reg_reset_ht_gc();\ndiff --git a/src/modules/uac/uac.c b/src/modules/uac/uac.c\n--- a/src/modules/uac/uac.c\t(revision aa335e6138414f764dcf7287db271207f76bc6d8)\n+++ b/src/modules/uac/uac.c\t(date 1713982956895)\n@@ -115,6 +115,7 @@\n extern int reg_timer_interval;\n extern int _uac_reg_gc_interval;\n extern int _uac_reg_use_domain;\n+extern int _uac_reg_reload_delta;\n \n static pv_export_t mod_pvs[] = {\n \t\t{{\"uac_req\", sizeof(\"uac_req\") - 1}, PVT_OTHER, pv_get_uac_req,\n@@ -187,7 +188,9 @@\n \t\t{\"reg_hash_size\", INT_PARAM, &reg_htable_size},\n \t\t{\"reg_use_domain\", PARAM_INT, &_uac_reg_use_domain},\n \t\t{\"default_socket\", PARAM_STR, &uac_default_socket},\n-\t\t{\"event_callback\", PARAM_STR, &uac_event_callback}, {0, 0, 0}};\n+\t\t{\"event_callback\", PARAM_STR, &uac_event_callback},\n+\t\t{\"reload_delta\", INT_PARAM, &_uac_reg_reload_delta},\n+\t\t{0, 0, 0}};\n \n \n struct module_exports exports = {\"uac\", /* module name */\n"
  },
  {
    "path": "kamailio/ubuntu/20.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local KAM_SOURCES_LIST=\"/etc/apt/sources.list.d/kamailio.list\"\n    local KAM_PREFS_CONF=\"/etc/apt/preferences.d/kamailio.pref\"\n    local NPROC=$(nproc)\n\n    # nf_tables is the default fw on ubuntu but it has too many bugs at this time\n    # instead we will use legacy iptables until these issues are ironed out\n    update-alternatives --set iptables /usr/sbin/iptables-legacy\n    update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy\n\n    # Install Dependencies\n    apt-get install -y curl wget sed gawk vim perl uuid-dev libssl-dev logrotate rsyslog firewalld \\\n        python3 libcurl4-openssl-dev libjansson-dev cmake python3-venv\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        exit 1\n    fi\n\n    ## compile and install openssl v1.1.1 (workaround for amazon linux repo conflicts)\n    ## we must overwrite system packages (openssl/openssl-devel) otherwise python's openssl package is not supported\n    if [[ \"$(openssl version 2>/dev/null | awk '{print $2}')\" != \"1.1.1w\" ]]; then\n        if [[ ! -d ${SRC_DIR}/openssl ]]; then\n            ( cd ${SRC_DIR} &&\n            curl -sL https://www.openssl.org/source/openssl-1.1.1w.tar.gz 2>/dev/null |\n            tar -xzf - --transform 's%openssl-1.1.1w%openssl%'; )\n        fi\n        (\n            cd ${SRC_DIR}/openssl &&\n            ./Configure --prefix=/usr linux-$(uname -m) &&\n            make -j $NPROC &&\n            make -j $NPROC install\n        ) || {\n            printerr 'Failed to compile openssl'\n            return 1\n        }\n    fi\n\n    # we need a newer version of certbot than the distro repos offer\n    apt-get remove -y *certbot*\n    python3 -m venv /opt/certbot/\n    /opt/certbot/bin/pip install --upgrade pip\n    /opt/certbot/bin/pip install certbot\n    ln -sf /opt/certbot/bin/certbot /usr/bin/certbot\n\n    # create kamailio user and group\n    mkdir -p /var/run/kamailio\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel kamailio &>/dev/null; groupdel kamailio &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    # add repo sources to apt\n    mkdir -p /etc/apt/sources.list.d\n    (cat << EOF\n# kamailio repo's\ndeb https://deb-archive.kamailio.org/repos/kamailio-${KAM_VERSION} focal main\n#deb-src https://deb-archive.kamailio.org/repos/kamailio-${KAM_VERSION} focal main\nEOF\n    ) > ${KAM_SOURCES_LIST}\n\n    # give higher precedence to packages from kamailio repo\n    mkdir -p /etc/apt/preferences.d\n    (cat << 'EOF'\nPackage: *\nPin: origin deb-archive.kamailio.org\nPin-Priority: 1000\nEOF\n    ) > ${KAM_PREFS_CONF}\n\n    # Add Key for Kamailio Repo\n    wget -O- https://deb-archive.kamailio.org/kamailiodebkey.gpg | apt-key add -\n\n    # Update repo sources cache\n    apt-get update -y\n\n    # Install Kamailio packages\n    apt-get install -y --allow-downgrades kamailio kamailio-mysql-modules kamailio-extra-modules kamailio-tls-modules kamailio-websocket-modules \\\n        kamailio-presence-modules kamailio-json-modules\n\n    # get info about the kamailio install for later use in script\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n    # create kamailio tmp files\n    echo \"d /run/kamailio 0750 kamailio kamailio\" > /etc/tmpfiles.d/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR} ${BACKUPS_DIR}/kamailio\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${BACKUPS_DIR}/kamailio/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    (cat << EOF\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nEXTRA_MODULES=\"imc cpl siptrace domainpolicy carrierroute drouting userblocklist htable purple uac pipelimit mtree sca mohqueue rtpproxy rtpengine secfilter\"\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n#STORE_PLAINTEXT_PW=0\nEOF\n    ) > ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Make sure MariaDB and Local DNS start before Kamailio\n    if ! grep -q -v 'mariadb.service dnsmasq.service' /lib/systemd/system/kamailio.service 2>/dev/null; then\n        sed -i -r -e 's/(After=.*)/\\1 mariadb.service dnsmasq.service/' /lib/systemd/system/kamailio.service\n    fi\n    if ! grep -q -v \"${DSIP_PROJECT_DIR}/dsiprouter.sh updatednsconfig\" /lib/systemd/system/kamailio.service 2>/dev/null; then\n        sed -i -r -e \"0,\\|^ExecStart.*|{s||ExecStartPre=-${DSIP_PROJECT_DIR}/dsiprouter.sh updatednsconfig\\n&|}\" /lib/systemd/system/kamailio.service\n    fi\n    systemctl daemon-reload\n\n    # Enable Kamailio for system startup\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-certificates.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libjwt (version in repos is too old)\n    if [[ ! -d ${SRC_DIR}/libjwt ]]; then\n        git clone --depth 1 -c advice.detachedHead=false -b v2.1.1 https://github.com/devopsec/libjwt.git ${SRC_DIR}/libjwt\n    fi\n\n    (\n        cd ${SRC_DIR}/libjwt &&\n        autoreconf -i &&\n        ./configure --prefix=/usr &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libjwt'\n        return 1\n    }\n\n    ## compile and install libks\n    if [[ ! -d ${SRC_DIR}/libks ]]; then\n        git clone --single-branch -c advice.detachedHead=false https://github.com/signalwire/libks -b v1.8.3 ${SRC_DIR}/libks\n    fi\n    (\n        cd ${SRC_DIR}/libks &&\n        cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release . &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libks'\n        return 1\n    }\n\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    ( cd ${SRC_DIR}/libstirshaken && ./bootstrap.sh && ./configure --prefix=/usr &&\n        make && make install && ldconfig; exit $?;\n    ) || { printerr 'Failed to compile and install libstirshaken'; return 1; }\n\n    ## compile and install STIR/SHAKEN module\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/stirshaken &&\n        make -j $NPROC\n    ) &&\n    cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/ || {\n        printerr 'Failed to compile and install STIR/SHAKEN module'\n        return 1\n    }\n\n    # patch uac module to support reload_delta\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/uac &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/uac.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/uac/uac.so ${KAM_MODULES_DIR}/\n    ) || {\n        printerr 'Failed to patch uac module'\n        return 1\n    }\n\n    return 0\n}\n\nfunction uninstall() {\n    # Stop and disable services\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    cp -rf ${SYSTEM_KAMAILIO_CONFIG_DIR}/. ${BACKUPS_DIR}/kamailio/\n    rm -rf ${SYSTEM_KAMAILIO_CONFIG_DIR}\n\n    # Uninstall Stirshaken Required Packages\n    ( cd /libjwt; make uninstall; exit $?; ) && rm -rf /libjwt\n    ( cd /libks; make uninstall; exit $?; ) && rm -rf /libks\n    ( cd /libstirshaken; make uninstall;exit $?; ) && rm -rf /libstirshaken\n    rm -rf /kamailio\n\n    # Uninstall Kamailio modules\n    apt-get -y remove --purge kamailio\\*\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "kamailio/ubuntu/22.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local KAM_SOURCES_LIST=\"/etc/apt/sources.list.d/kamailio.list\"\n    local KAM_PREFS_CONF=\"/etc/apt/preferences.d/kamailio.pref\"\n    local NPROC=$(nproc)\n\n    # nf_tables is the default fw on ubuntu but it has too many bugs at this time\n    # instead we will use legacy iptables until these issues are ironed out\n    update-alternatives --set iptables /usr/sbin/iptables-legacy\n    update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy\n\n    # Install Dependencies\n    apt-get install -y curl wget sed gawk vim perl uuid-dev libssl-dev logrotate rsyslog \\\n        libcurl4-openssl-dev libjansson-dev cmake firewalld python3 python3-venv\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        exit 1\n    fi\n\n    ## compile and install openssl v1.1.1 (workaround for amazon linux repo conflicts)\n    ## we must overwrite system packages (openssl/openssl-devel) otherwise python's openssl package is not supported\n    if [[ \"$(openssl version 2>/dev/null | awk '{print $2}')\" != \"1.1.1w\" ]]; then\n        if [[ ! -d ${SRC_DIR}/openssl ]]; then\n            ( cd ${SRC_DIR} &&\n            curl -sL https://www.openssl.org/source/openssl-1.1.1w.tar.gz 2>/dev/null |\n            tar -xzf - --transform 's%openssl-1.1.1w%openssl%'; )\n        fi\n        (\n            cd ${SRC_DIR}/openssl &&\n            ./Configure --prefix=/usr linux-$(uname -m) &&\n            make -j $NPROC &&\n            make -j $NPROC install\n        ) || {\n            printerr 'Failed to compile openssl'\n            return 1\n        }\n    fi\n\n    # we need a newer version of certbot than the distro repos offer\n    apt-get remove -y *certbot*\n    python3 -m venv /opt/certbot/\n    /opt/certbot/bin/pip install --upgrade pip\n    /opt/certbot/bin/pip install certbot\n    ln -sf /opt/certbot/bin/certbot /usr/bin/certbot\n\n    # create kamailio user and group\n    mkdir -p /var/run/kamailio\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel kamailio &>/dev/null; groupdel kamailio &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    # add repo sources to apt\n    mkdir -p /etc/apt/sources.list.d\n    (cat << EOF\n# kamailio repo's\ndeb https://deb-archive.kamailio.org/repos/kamailio-${KAM_VERSION} jammy main\n#deb-src https://deb-archive.kamailio.org/repos/kamailio-${KAM_VERSION} jammy main\nEOF\n    ) > ${KAM_SOURCES_LIST}\n\n    # give higher precedence to packages from kamailio repo\n    mkdir -p /etc/apt/preferences.d\n    (cat << 'EOF'\nPackage: *\nPin: origin deb-archive.kamailio.org\nPin-Priority: 1000\nEOF\n    ) > ${KAM_PREFS_CONF}\n\n    # Add Key for Kamailio Repo\n    wget -O- https://deb-archive.kamailio.org/kamailiodebkey.gpg | apt-key add -\n\n    # Update repo sources cache\n    apt-get update -y\n\n    # Install Kamailio packages\n    apt-get install -y --allow-downgrades kamailio kamailio-mysql-modules kamailio-extra-modules kamailio-tls-modules \\\n        kamailio-websocket-modules kamailio-presence-modules kamailio-json-modules kamailio-sctp-modules\n\n    # get info about the kamailio install for later use in script\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n    # create kamailio tmp files\n    echo \"d /run/kamailio 0750 kamailio kamailio\" > /etc/tmpfiles.d/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR} ${BACKUPS_DIR}/kamailio\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${BACKUPS_DIR}/kamailio/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    (cat << EOF\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nEXTRA_MODULES=\"imc cpl siptrace domainpolicy carrierroute drouting userblocklist htable purple uac pipelimit mtree sca mohqueue rtpproxy rtpengine secfilter\"\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n#STORE_PLAINTEXT_PW=0\nEOF\n    ) > ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Make sure MariaDB and Local DNS start before Kamailio\n    if ! grep -q -v 'mariadb.service dnsmasq.service' /lib/systemd/system/kamailio.service 2>/dev/null; then\n        sed -i -r -e 's/(After=.*)/\\1 mariadb.service dnsmasq.service/' /lib/systemd/system/kamailio.service\n    fi\n    if ! grep -q -v \"${DSIP_PROJECT_DIR}/dsiprouter.sh updatednsconfig\" /lib/systemd/system/kamailio.service 2>/dev/null; then\n        sed -i -r -e \"0,\\|^ExecStart.*|{s||ExecStartPre=-${DSIP_PROJECT_DIR}/dsiprouter.sh updatednsconfig\\n&|}\" /lib/systemd/system/kamailio.service\n    fi\n    systemctl daemon-reload\n\n    # Enable Kamailio for system startup\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-certificates.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libjwt (version in repos is too old)\n    if [[ ! -d ${SRC_DIR}/libjwt ]]; then\n        git clone --depth 1 -c advice.detachedHead=false -b v2.1.1 https://github.com/devopsec/libjwt.git ${SRC_DIR}/libjwt\n    fi\n    (\n        cd ${SRC_DIR}/libjwt &&\n        autoreconf -i &&\n        ./configure --prefix=/usr &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libjwt'\n        return 1\n    }\n\n    ## compile and install libks\n    if [[ ! -d ${SRC_DIR}/libks ]]; then\n        git clone --single-branch -c advice.detachedHead=false https://github.com/signalwire/libks -b v1.8.3 ${SRC_DIR}/libks\n    fi\n    (\n        cd ${SRC_DIR}/libks &&\n        cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release . &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libks'\n        return 1\n    }\n\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    (\n        # TODO: commit updates to upstream to fix EVP_PKEY_cmp being deprecated\n        cd ${SRC_DIR}/libstirshaken &&\n        ./bootstrap.sh &&\n        ./configure --prefix=/usr CFLAGS='-Wno-deprecated-declarations' LDFLAGS='-L/usr/lib' &&\n        make -j $NPROC &&\n        make -j $NPROC install &&\n        ldconfig\n    ) || {\n        printerr 'Failed to compile and install libstirshaken'\n        return 1\n    }\n\n    ## compile and install STIR/SHAKEN module\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/stirshaken &&\n        make -j $NPROC\n    ) &&\n    cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/ || {\n        printerr 'Failed to compile and install STIR/SHAKEN module'\n        return 1\n    }\n\n    # patch uac module to support reload_delta\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/uac &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/uac.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/uac/uac.so ${KAM_MODULES_DIR}/\n    ) || {\n        printerr 'Failed to patch uac module'\n        return 1\n    }\n\n    return 0\n}\n\nfunction uninstall() {\n    # Stop and disable services\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    cp -rf ${SYSTEM_KAMAILIO_CONFIG_DIR}/. ${BACKUPS_DIR}/kamailio/\n    rm -rf ${SYSTEM_KAMAILIO_CONFIG_DIR}\n\n    # Uninstall Stirshaken Required Packages\n    ( cd ${SRC_DIR}/libjwt; make uninstall; exit $?; ) && rm -rf ${SRC_DIR}/libjwt\n    ( cd ${SRC_DIR}/libks; make uninstall; exit $?; ) && rm -rf ${SRC_DIR}/libks\n    ( cd ${SRC_DIR}/libstirshaken; make uninstall;exit $?; ) && rm -rf ${SRC_DIR}/libstirshaken\n    rm -rf ${SRC_DIR}/kamailio\n\n    # Uninstall Kamailio modules\n    apt-get -y remove --purge kamailio\\*\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "kamailio/ubuntu/24.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    local KAM_DEBMINOR_VERSION=$(perl -pe 's%^([0-9])\\.([0-9]).*$%\\1\\2%' <<<\"$KAM_VERSION\")\n    local KAM_SOURCES_LIST=\"/etc/apt/sources.list.d/kamailio.list\"\n    local KAM_PREFS_CONF=\"/etc/apt/preferences.d/kamailio.pref\"\n    local NPROC=$(nproc)\n\n    # Install Dependencies\n    apt install -y curl wget sed gawk vim perl uuid-dev libssl-dev logrotate rsyslog \\\n        libcurl4-openssl-dev libjansson-dev cmake firewalld python3 python3-venv\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing required packages'\n        exit 1\n    fi\n\n    # we need a newer version of certbot than the distro repos offer\n    apt remove -y *certbot*\n    python3 -m venv /opt/certbot/\n    /opt/certbot/bin/pip install --upgrade pip\n    /opt/certbot/bin/pip install certbot\n    ln -sf /opt/certbot/bin/certbot /usr/bin/certbot\n\n    # create kamailio user and group\n    mkdir -p /var/run/kamailio\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel kamailio &>/dev/null; groupdel kamailio &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Kamailio SIP Proxy\" kamailio\n    chown -R kamailio:kamailio /var/run/kamailio\n\n    # add repo sources to apt\n    mkdir -p /etc/apt/sources.list.d\n    # TODO: noble not available in the archive repos\n    (cat << EOF\n# kamailio repo's\ndeb https://deb.kamailio.org/kamailio${KAM_DEBMINOR_VERSION} noble main\n#deb-src https://deb-archive.kamailio.org/kamailio${KAM_DEBMINOR_VERSION} noble main\nEOF\n    ) > ${KAM_SOURCES_LIST}\n\n    # give higher precedence to packages from kamailio repo\n    mkdir -p /etc/apt/preferences.d\n    (cat << 'EOF'\nPackage: *\nPin: origin deb.kamailio.org\nPin-Priority: 1000\nEOF\n    ) > ${KAM_PREFS_CONF}\n\n    # Add Key for Kamailio Repo\n    curl -s https://deb.kamailio.org/kamailiodebkey.gpg | gpg --dearmor >/etc/apt/trusted.gpg.d/kamailiodebkey.gpg\n\n    # Update repo sources cache\n    apt update -y\n\n    # Install Kamailio packages\n    apt install -y --allow-downgrades kamailio kamailio-mysql-modules kamailio-extra-modules \\\n        kamailio-tls-modules kamailio-websocket-modules kamailio-presence-modules \\\n        kamailio-json-modules kamailio-sctp-modules\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing kamailio packages'\n        exit 1\n    fi\n\n    # get info about the kamailio install for later use in script\n    KAM_MODULES_DIR=$(find /usr/lib{32,64,}/{i386*/*,i386*/kamailio/*,x86_64*/*,x86_64*/kamailio/*,*} -name drouting.so -printf '%h' -quit 2>/dev/null)\n\n    # create kamailio defaults config\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio.conf /etc/default/kamailio.conf\n    # create kamailio tmp files\n    echo \"d /run/kamailio 0750 kamailio kamailio\" > /etc/tmpfiles.d/kamailio.conf\n\n    # Configure Kamailio and Required Database Modules\n    mkdir -p ${SYSTEM_KAMAILIO_CONFIG_DIR} ${BACKUPS_DIR}/kamailio\n    mv -f ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc ${BACKUPS_DIR}/kamailio/kamctlrc.$(date +%Y%m%d_%H%M%S)\n    if [[ -z \"${ROOT_DB_PASS-unset}\" ]]; then\n        local ROOTPW_SETTING=\"DBROOTPWSKIP=yes\"\n    else\n        local ROOTPW_SETTING=\"DBROOTPW=\\\"${ROOT_DB_PASS}\\\"\"\n    fi\n\n    # TODO: we should set STORE_PLAINTEXT_PW to 0, this is not default but would need tested\n    (cat << EOF\nDBENGINE=MYSQL\nDBHOST=\"${KAM_DB_HOST}\"\nDBPORT=\"${KAM_DB_PORT}\"\nDBNAME=\"${KAM_DB_NAME}\"\nDBROUSER=\"${KAM_DB_USER}\"\nDBROPW=\"${KAM_DB_PASS}\"\nDBRWUSER=\"${KAM_DB_USER}\"\nDBRWPW=\"${KAM_DB_PASS}\"\nDBROOTUSER=\"${ROOT_DB_USER}\"\n${ROOTPW_SETTING}\nCHARSET=utf8\nEXTRA_MODULES=\"imc cpl siptrace domainpolicy carrierroute drouting userblocklist htable purple uac pipelimit mtree sca mohqueue rtpproxy rtpengine secfilter\"\nINSTALL_EXTRA_TABLES=yes\nINSTALL_PRESENCE_TABLES=yes\nINSTALL_DBUID_TABLES=yes\n#STORE_PLAINTEXT_PW=0\nEOF\n    ) > ${SYSTEM_KAMAILIO_CONFIG_DIR}/kamctlrc\n\n   # in mariadb ver >= 10.6.1 --port= now defaults to transport=tcp\n    # we want socket connections for root as default so apply our patch to kamdbctl\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd /usr/lib/x86_64-linux-gnu/kamailio/kamctl &&\n        patch -p3 -N <${DSIP_PROJECT_DIR}/kamailio/kamdbctl.patch\n    )\n    if (( $? > 1 )); then\n        printerr 'Failed patching kamdbctl'\n        return 1\n    fi\n\n    # Execute 'kamdbctl create' to create the Kamailio database schema\n    kamdbctl create || {\n        printerr 'Failed creating kamailio database'\n        return 1\n    }\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup firewall rules\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --add-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --zone=public --add-port=22/tcp --permanent\n    firewall-cmd --reload\n\n    # Configure Kamailio systemd service\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/kamailio-v2.service /lib/systemd/system/kamailio.service\n    chmod 644 /lib/systemd/system/kamailio.service\n    systemctl daemon-reload\n    systemctl enable kamailio\n\n    # Enable Kamailio for system startup\n    systemctl enable kamailio\n\n    # Configure rsyslog defaults\n    if ! grep -q 'dSIPRouter rsyslog.conf' /etc/rsyslog.conf 2>/dev/null; then\n        cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rsyslog.conf /etc/rsyslog.conf\n    fi\n\n    # Setup kamailio Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/kamailio.conf /etc/rsyslog.d/kamailio.conf\n    touch /var/log/kamailio.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/kamailio /etc/logrotate.d/kamailio\n\n    # Setup Kamailio to use the CA cert's that are shipped with the OS\n    mkdir -p ${DSIP_SYSTEM_CONFIG_DIR}/certs/stirshaken\n    ln -s /etc/ssl/certs/ca-certificates.crt ${DSIP_SSL_CA}\n    updateCACertsDir\n\n    # setup STIR/SHAKEN module for kamailio\n    ## compile and install libjwt (version in repos is too old)\n    if [[ ! -d ${SRC_DIR}/libjwt ]]; then\n        git clone --depth 1 -c advice.detachedHead=false -b v2.1.1 https://github.com/devopsec/libjwt.git ${SRC_DIR}/libjwt\n    fi\n    (\n        cd ${SRC_DIR}/libjwt &&\n        autoreconf -i &&\n        ./configure --prefix=/usr &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libjwt'\n        return 1\n    }\n\n    ## compile and install libks\n    if [[ ! -d ${SRC_DIR}/libks ]]; then\n        git clone --single-branch -c advice.detachedHead=false https://github.com/signalwire/libks -b v1.8.3 ${SRC_DIR}/libks\n    fi\n    (\n        cd ${SRC_DIR}/libks &&\n        cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release . &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libks'\n        return 1\n    }\n\n    ## compile and install libstirshaken\n    if [[ ! -d ${SRC_DIR}/libstirshaken ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/signalwire/libstirshaken ${SRC_DIR}/libstirshaken\n    fi\n    (\n        # TODO: commit updates to upstream to fix EVP_PKEY_cmp being deprecated\n        cd ${SRC_DIR}/libstirshaken &&\n        ./bootstrap.sh &&\n        ./configure --prefix=/usr CFLAGS='-Wno-deprecated-declarations' LDFLAGS='-L/usr/lib' &&\n        make -j $NPROC &&\n        make -j $NPROC install &&\n        ldconfig\n    ) || {\n        printerr 'Failed to compile and install libstirshaken'\n        return 1\n    }\n\n    ## compile and install STIR/SHAKEN module\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/kamailio ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/kamailio)\" != \"${KAM_VERSION}\" ]]; then\n            rm -rf ${SRC_DIR}/kamailio\n            git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${KAM_VERSION} https://github.com/kamailio/kamailio.git ${SRC_DIR}/kamailio\n    fi\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/stirshaken &&\n        make -j $NPROC\n    ) &&\n    cp -f ${SRC_DIR}/kamailio/src/modules/stirshaken/stirshaken.so ${KAM_MODULES_DIR}/ || {\n        printerr 'Failed to compile and install STIR/SHAKEN module'\n        return 1\n    }\n\n    # patch uac module to support reload_delta\n    # TODO: commit upstream (https://github.com/kamailio/kamailio.git)\n    (\n        cd ${SRC_DIR}/kamailio/src/modules/uac &&\n        patch -p4 -N <${DSIP_PROJECT_DIR}/kamailio/uac.patch\n        (( $? > 1 )) && exit 1\n        make -j $NPROC &&\n        cp -f ${SRC_DIR}/kamailio/src/modules/uac/uac.so ${KAM_MODULES_DIR}/\n    ) || {\n        printerr 'Failed to patch uac module'\n        return 1\n    }\n\n    return 0\n}\n\nfunction uninstall() {\n    # Stop and disable services\n    systemctl stop kamailio\n    systemctl disable kamailio\n\n    # Backup kamailio configuration directory\n    cp -rf ${SYSTEM_KAMAILIO_CONFIG_DIR}/. ${BACKUPS_DIR}/kamailio/\n    rm -rf ${SYSTEM_KAMAILIO_CONFIG_DIR}\n\n    # Uninstall Stirshaken Required Packages\n    ( cd ${SRC_DIR}/libjwt; make uninstall; exit $?; ) && rm -rf ${SRC_DIR}/libjwt\n    ( cd ${SRC_DIR}/libks; make uninstall; exit $?; ) && rm -rf ${SRC_DIR}/libks\n    ( cd ${SRC_DIR}/libstirshaken; make uninstall;exit $?; ) && rm -rf ${SRC_DIR}/libstirshaken\n    rm -rf ${SRC_DIR}/kamailio\n\n    # Uninstall Kamailio modules\n    apt-get -y remove --purge kamailio\\*\n\n    # Remove firewall rules that was created by us:\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/udp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIP_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_SIPS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_WSS_PORT}/tcp --permanent\n    firewall-cmd --zone=public --remove-port=${KAM_DMQ_PORT}/udp --permanent\n    firewall-cmd --reload\n\n    # Remove kamailio Logging\n    rm -f /etc/rsyslog.d/kamailio.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/kamailio\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "mysql/almalinux/8.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create mysql user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel mysql &>/dev/null; groupdel mysql &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Mysql Database Server\" mysql\n\n    # install mysql packages\n    yum install -y mariadb mariadb-server\n\n    # Setup mysql config locations in a reliable manner\n    # Setup mysql config locations in a reliable manner\n    ln -s /usr/share/mariadb /usr/share/mysql\n    ln -s /var/log/mariadb /var/log/mysql\n    rm -f ~/.my.cnf 2>/dev/null\n    mkdir -p /var/run/mariadb\n    chown -R mysql:mysql /var/run/mariadb /var/lib/mysql /var/log/mysql /usr/share/mysql\n\n    # allow symlinks in mariadb service\n    sed -i 's/symbolic-links=0/#symbolic-links=0/' /etc/my.cnf\n\n    # add in the original aliases (from debian repo) to mariadb.service\n    #perl -0777 -i -pe 's|(\\[Install\\]\\s+WantedBy.*?\\n+)|\\1Alias=mysql.service\\nAlias=mysqld.service\\n\\n|gms' /lib/systemd/system/mariadb.service\n\n    # if db is remote don't run local service\n    reconfigureMysqlSystemdService\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop and disable services\n    systemctl stop mariadb\n    systemctl disable mariadb\n\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # remove mysql unit files we created\n    rm -rf /etc/systemd/system/mariadb.service.d/\n    rm -f /etc/systemd/system/mariadb.service 2>/dev/null\n    systemctl daemon-reload\n\n    # Uninstall mysql / Mariadb packages\n    yum remove -y mysql\\*\n    yum remove -y mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "mysql/almalinux/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create mysql user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel mysql &>/dev/null; groupdel mysql &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Mysql Database Server\" mysql\n\n    # install mysql packages\n    dnf install -y mariadb mariadb-server &&\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing mariadb packages'\n        return 1\n    fi\n\n    # Setup mysql config locations in a reliable manner\n    rm -f ~/.my.cnf 2>/dev/null\n    ln -snf /usr/share/mariadb /usr/share/mysql\n    ln -snf /var/log/mariadb /var/log/mysql\n    mkdir -p /var/run/mariadb /var/lib/mysql\n    chown -R mysql:mysql /var/run/mariadb/ /var/lib/mysql/ /var/log/mysql/ /usr/share/mysql/ /var/lib/mysql\n\n    # allow symlinks in mariadb service\n    sed -i 's/symbolic-links=0/#symbolic-links=0/' /etc/my.cnf\n\n    # if db is remote don't run local service\n    reconfigureMysqlSystemdService\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # remove mysql unit files we created\n    rm -rf /etc/systemd/system/mariadb.service.d/\n    rm -f /etc/systemd/system/mariadb.service 2>/dev/null\n    systemctl daemon-reload\n\n    # Uninstall mysql / Mariadb packages\n    dnf remove -y mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "mysql/amzn/2.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create mysql user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel mysql &>/dev/null; groupdel mysql &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Mysql Database Server\" mysql\n\n    # install mysql packages\n    amazon-linux-extras enable mariadb10.5 >/dev/null\n    yum makecache -y\n    yum install -y mariadb mariadb-server\n\n    # Setup mysql config locations in a reliable manner\n    rm -f ~/.my.cnf 2>/dev/null\n    ln -snf /usr/share/mariadb /usr/share/mysql\n    ln -snf /var/log/mariadb /var/log/mysql\n    mkdir -p /var/run/mariadb\n    chown -R mysql:mysql /var/run/mariadb/ /var/lib/mysql/ /var/log/mysql/ /usr/share/mysql/\n\n    # allow symlinks in mariadb service\n    sed -i 's/symbolic-links=0/#symbolic-links=0/' /etc/my.cnf\n\n    # setup aliases and if db is remote replace with dummy service file\n    reconfigureMysqlSystemdService\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop servers\n    systemctl stop mariadb\n    systemctl disable mariadb\n\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # remove mysql unit files we created\n    rm -rf /etc/systemd/system/mariadb.service.d/\n    rm -f /etc/systemd/system/mariadb.service 2>/dev/null\n    systemctl daemon-reload\n\n    # Uninstall mysql / Mariadb packages\n    yum remove -y mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "mysql/centos/7.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create mysql user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel mysql &>/dev/null; groupdel mysql &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Mysql Database Server\" mysql\n\n    # install dependencies\n    yum install -y curl\n\n    # setup the mariadb community repos for newer packages\n    (\n        cd /tmp &&\n        curl -sLO https://r.mariadb.com/downloads/mariadb_repo_setup &&\n        chmod +x mariadb_repo_setup &&\n        ./mariadb_repo_setup --mariadb-server-version=\"mariadb-10.4\"\n    ) || {\n        printerr 'Failed setting up mariadb package repos'\n        return 1\n    }\n\n    # install mysql packages\n    yum install -y mariadb mariadb-server\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing mariadb packages'\n        return 1\n    fi\n\n    # Setup mysql config locations in a reliable manner\n    rm -f ~/.my.cnf 2>/dev/null\n    ln -snf /usr/share/mariadb /usr/share/mysql\n    ln -snf /var/log/mariadb /var/log/mysql\n    mkdir -p /var/run/mariadb /var/lib/mysql\n    chown -R mysql:mysql /var/run/mariadb/ /var/lib/mysql/ /var/log/mysql/ /usr/share/mysql/ /var/lib/mysql\n\n    # allow symlinks in mariadb service\n    sed -i 's/symbolic-links=0/#symbolic-links=0/' /etc/my.cnf\n\n    # if db is remote don't run local service\n    reconfigureMysqlSystemdService\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop servers\n    systemctl stop mariadb\n    systemctl disable mariadb\n\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # remove mysql unit files we created\n    rm -f /lib/systemd/system/mysql.service /lib/systemd/system/mysqld.service\n\n    # Uninstall mysql / Mariadb packages\n    yum remove -y mysql\\*\n    yum remove -y mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "mysql/centos/8.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create mysql user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel mysql &>/dev/null; groupdel mysql &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Mysql Database Server\" mysql\n\n    # install mysql packages\n    dnf install -y mariadb mariadb-server\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing mariadb packages'\n        return 1\n    fi\n\n    # Setup mysql config locations in a reliable manner\n    rm -f ~/.my.cnf 2>/dev/null\n    ln -snf /usr/share/mariadb /usr/share/mysql\n    ln -snf /var/log/mariadb /var/log/mysql\n    mkdir -p /var/run/mariadb /var/lib/mysql\n    chown -R mysql:mysql /var/run/mariadb/ /var/lib/mysql/ /var/log/mysql/ /usr/share/mysql/ /var/lib/mysql\n\n    # allow symlinks in mariadb service\n    sed -i 's/symbolic-links=0/#symbolic-links=0/' /etc/my.cnf\n\n    # if db is remote don't run local service\n    reconfigureMysqlSystemdService\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # remove mysql unit files we created\n    rm -rf /etc/systemd/system/mariadb.service.d/\n    rm -f /etc/systemd/system/mariadb.service 2>/dev/null\n    systemctl daemon-reload\n\n    # Uninstall mysql / Mariadb packages\n    dnf remove -y mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "mysql/centos/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create mysql user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel mysql &>/dev/null; groupdel mysql &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Mysql Database Server\" mysql\n\n    # install mysql packages\n    dnf install -y mariadb mariadb-server &&\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing mariadb packages'\n        return 1\n    fi\n\n    # Setup mysql config locations in a reliable manner\n    rm -f ~/.my.cnf 2>/dev/null\n    ln -snf /usr/share/mariadb /usr/share/mysql\n    ln -snf /var/log/mariadb /var/log/mysql\n    mkdir -p /var/run/mariadb /var/lib/mysql\n    chown -R mysql:mysql /var/run/mariadb/ /var/lib/mysql/ /var/log/mysql/ /usr/share/mysql/ /var/lib/mysql\n\n    # allow symlinks in mariadb service\n    sed -i 's/symbolic-links=0/#symbolic-links=0/' /etc/my.cnf\n\n    # if db is remote don't run local service\n    reconfigureMysqlSystemdService\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # remove mysql unit files we created\n    rm -rf /etc/systemd/system/mariadb.service.d/\n    rm -f /etc/systemd/system/mariadb.service 2>/dev/null\n    systemctl daemon-reload\n\n    # Uninstall mysql / Mariadb packages\n    dnf remove -y mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "mysql/debian/10.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    # create mysql user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel mysql &>/dev/null; groupdel mysql &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Mysql Database Server\" mysql\n\n    # install mysql packages\n    apt-get install -y -t bullseye default-mysql-server mariadb-server\n\n    # if db is remote don't run local service\n    reconfigureMysqlSystemdService\n\n    # Make sure no extra configs present on fresh install\n    rm -f ~/.my.cnf\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop and disable services\n    systemctl stop mariadb\n    systemctl disable mariadb\n\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # Uninstall mysql / mariadb packages\n    apt-get -y remove --purge mysql\\*\n    apt-get -y remove --purge mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "mysql/debian/11.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    # create mysql user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel mysql &>/dev/null; groupdel mysql &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Mysql Database Server\" mysql\n\n    # install mysql packages\n    apt-get install -y --allow-unauthenticated default-mysql-server ||\n        apt-get install -y --allow-unauthenticated mariadb-server\n\n    # if db is remote don't run local service\n    reconfigureMysqlSystemdService\n\n    # Make sure no extra configs present on fresh install\n    rm -f ~/.my.cnf\n\n    # ensure data directory exists\n    mkdir -p /var/lib/mysql\n    chown mysql:mysql /var/lib/mysql\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop and disable services\n    systemctl stop mariadb\n    systemctl disable mariadb\n\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # Uninstall mysql / mariadb packages\n    apt-get -y remove --purge mysql\\*\n    apt-get -y remove --purge mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "mysql/debian/12.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    # create mysql user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel mysql &>/dev/null; groupdel mysql &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Mysql Database Server\" mysql\n\n    # install mysql packages\n    apt-get install -y mariadb-server\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing mariadb packages'\n        return 1\n    fi\n\n    # if db is remote don't run local service\n    reconfigureMysqlSystemdService\n\n    # Make sure no extra configs present on fresh install\n    rm -f ~/.my.cnf\n\n    # ensure data directory exists\n    mkdir -p /var/lib/mysql\n    chown mysql:mysql /var/lib/mysql\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop and disable services\n    systemctl stop mariadb\n    systemctl disable mariadb\n\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # Uninstall mariadb packages\n    apt-get -y remove --purge mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "mysql/debian/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    # install mysql packages\n    apt-get install -y --allow-unauthenticated default-mysql-server ||\n        apt-get install -y --allow-unauthenticated mariadb-server\n\n    # if db is remote don't run local service\n    reconfigureMysqlSystemdService\n\n    # Enable mysql on boot\n    systemctl enable mariadb\n\n    # Make sure no extra configs present on fresh install\n    rm -f ~/.my.cnf\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop and disable services\n    systemctl stop mariadb\n    systemctl disable mariadb\n\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # Uninstall mysql / mariadb packages\n    apt-get -y remove --purge mysql\\*\n    apt-get -y remove --purge mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "mysql/rhel/8.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create mysql user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel mysql &>/dev/null; groupdel mysql &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Mysql Database Server\" mysql\n\n    # install mysql packages\n    dnf install -y mariadb mariadb-server\n\n    # Setup mysql config locations in a reliable manner\n    rm -f ~/.my.cnf 2>/dev/null\n    ln -snf /var/log/mariadb /var/log/mysql\n    mkdir -p /var/run/mariadb /usr/share/mysql\n    chown -R mysql:mysql /var/run/mariadb/ /var/lib/mysql/ /var/log/mysql/ /usr/share/mysql/\n\n    # allow symlinks in mariadb service\n    sed -i 's/symbolic-links=0/#symbolic-links=0/' /etc/my.cnf\n\n    # add in the original aliases (from debian repo) to mariadb.service\n    #perl -0777 -i -pe 's|(\\[Install\\]\\s+WantedBy.*?\\n+)|\\1Alias=mysql.service\\nAlias=mysqld.service\\n\\n|gms' /lib/systemd/system/mariadb.service\n\n    # if db is remote don't run local service\n    reconfigureMysqlSystemdService\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop and disable services\n    systemctl stop mariadb\n    systemctl disable mariadb\n\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # remove mysql unit files we created\n    rm -rf /etc/systemd/system/mariadb.service.d/\n    rm -f /etc/systemd/system/mariadb.service 2>/dev/null\n    systemctl daemon-reload\n\n    # Uninstall mysql / Mariadb packages\n    yum remove -y mysql\\*\n    yum remove -y mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "mysql/rhel/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create mysql user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel mysql &>/dev/null; groupdel mysql &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Mysql Database Server\" mysql\n\n    # install mysql packages\n    dnf install -y mariadb mariadb-server &&\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing mariadb packages'\n        return 1\n    fi\n\n    # Setup mysql config locations in a reliable manner\n    rm -f ~/.my.cnf 2>/dev/null\n    ln -snf /usr/share/mariadb /usr/share/mysql\n    ln -snf /var/log/mariadb /var/log/mysql\n    mkdir -p /var/run/mariadb /var/lib/mysql\n    chown -R mysql:mysql /var/run/mariadb/ /var/lib/mysql/ /var/log/mysql/ /usr/share/mysql/ /var/lib/mysql\n\n    # allow symlinks in mariadb service\n    sed -i 's/symbolic-links=0/#symbolic-links=0/' /etc/my.cnf\n\n    # if db is remote don't run local service\n    reconfigureMysqlSystemdService\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # remove mysql unit files we created\n    rm -rf /etc/systemd/system/mariadb.service.d/\n    rm -f /etc/systemd/system/mariadb.service 2>/dev/null\n    systemctl daemon-reload\n\n    # Uninstall mysql / Mariadb packages\n    dnf remove -y mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "mysql/rocky/8.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create mysql user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel mysql &>/dev/null; groupdel mysql &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Mysql Database Server\" mysql\n\n    # install mysql packages\n    yum install -y mariadb mariadb-server\n\n    # Setup mysql config locations in a reliable manner\n    # Setup mysql config locations in a reliable manner\n    ln -s /usr/share/mariadb /usr/share/mysql\n    ln -s /var/log/mariadb /var/log/mysql\n    rm -f ~/.my.cnf 2>/dev/null\n    mkdir -p /var/run/mariadb\n    chown -R mysql:mysql /var/run/mariadb /var/lib/mysql /var/log/mysql /usr/share/mysql\n\n    # allow symlinks in mariadb service\n    sed -i 's/symbolic-links=0/#symbolic-links=0/' /etc/my.cnf\n\n    # add in the original aliases (from debian repo) to mariadb.service\n    #perl -0777 -i -pe 's|(\\[Install\\]\\s+WantedBy.*?\\n+)|\\1Alias=mysql.service\\nAlias=mysqld.service\\n\\n|gms' /lib/systemd/system/mariadb.service\n\n    # if db is remote don't run local service\n    reconfigureMysqlSystemdService\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop and disable services\n    systemctl stop mariadb\n    systemctl disable mariadb\n\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # remove mysql unit files we created\n    rm -rf /etc/systemd/system/mariadb.service.d/\n    rm -f /etc/systemd/system/mariadb.service 2>/dev/null\n    systemctl daemon-reload\n\n    # Uninstall mysql / Mariadb packages\n    yum remove -y mysql\\*\n    yum remove -y mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "mysql/rocky/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create mysql user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel mysql &>/dev/null; groupdel mysql &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Mysql Database Server\" mysql\n\n    # install mysql packages\n    dnf install -y mariadb mariadb-server &&\n\n    if (( $? != 0 )); then\n        printerr 'Failed installing mariadb packages'\n        return 1\n    fi\n\n    # Setup mysql config locations in a reliable manner\n    rm -f ~/.my.cnf 2>/dev/null\n    ln -snf /usr/share/mariadb /usr/share/mysql\n    ln -snf /var/log/mariadb /var/log/mysql\n    mkdir -p /var/run/mariadb /var/lib/mysql\n    chown -R mysql:mysql /var/run/mariadb/ /var/lib/mysql/ /var/log/mysql/ /usr/share/mysql/ /var/lib/mysql\n\n    # allow symlinks in mariadb service\n    sed -i 's/symbolic-links=0/#symbolic-links=0/' /etc/my.cnf\n\n    # if db is remote don't run local service\n    reconfigureMysqlSystemdService\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # remove mysql unit files we created\n    rm -rf /etc/systemd/system/mariadb.service.d/\n    rm -f /etc/systemd/system/mariadb.service 2>/dev/null\n    systemctl daemon-reload\n\n    # Uninstall mysql / Mariadb packages\n    dnf remove -y mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "mysql/systemd/dummy.service",
    "content": "[Unit]\nDescription=MySQL Dummy Service\n\n[Service]\nType=oneshot\nExecStart=/bin/true\nRemainAfterExit=true\nTimeoutSec=0\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "mysql/systemd/override.conf",
    "content": "[Install]\nAlias=\nAlias=mysql.service\nAlias=mysqld.service\n"
  },
  {
    "path": "mysql/systemd/override.sh",
    "content": "#!/usr/bin/env bash\n\nDSIP_PROJECT_DIR=${DSIP_PROJECT_DIR:-/opt/dsiprouter}\n\n# alias mariadb.service to mysql.service and mysqld.service using systemd drop-in replacements\n# allowing us to use same service name (mysql.service) across platforms\nmkdir -p /etc/systemd/system/mariadb.service.d\ncp -f ${DSIP_PROJECT_DIR}/mysql/systemd/override.conf /etc/systemd/system/mariadb.service.d/override.conf\nchmod 644 /etc/systemd/system/mariadb.service.d/override.conf\n"
  },
  {
    "path": "mysql/ubuntu/20.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    # create mysql user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel mysql &>/dev/null; groupdel mysql &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Mysql Database Server\" mysql\n\n    # install mysql packages\n    apt-get install -y mariadb-server mariadb-client libmariadbd-dev\n\n    # Setup mysql config locations in a reliable manner\n    rm -f ~/.my.cnf 2>/dev/null\n    mkdir -p /var/run/mariadb\n    chown -R mysql:mysql /var/run/mariadb /var/lib/mysql /var/log/mysql /usr/share/mysql\n    #( cd /lib/systemd/system/; ln -s mariadb.service mysqld.service; ln -s mariadb.service mysql.service; )\n\n    # setup aliases and if db is remote replace with dummy service file\n    reconfigureMysqlSystemdService\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop and disable services\n    systemctl stop mariadb\n    systemctl disable mariadb\n\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # remove mysql unit files we created\n    rm -rf /etc/systemd/system/mariadb.service.d/\n    rm -f /etc/systemd/system/mariadb.service 2>/dev/null\n    systemctl daemon-reload\n\n    # Uninstall mysql / mariadb packages\n    apt-get -y remove --purge mysql\\*\n    apt-get -y remove --purge mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "mysql/ubuntu/22.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    # create mysql user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel mysql &>/dev/null; groupdel mysql &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Mysql Database Server\" mysql\n\n    # install mysql packages\n    apt-get install -y mariadb-server mariadb-client\n\n    # Make sure no extra configs present on fresh install\n    rm -f ~/.my.cnf\n\n    # setup aliases and if db is remote replace with dummy service file\n    reconfigureMysqlSystemdService\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop and disable services\n    systemctl stop mariadb\n    systemctl disable mariadb\n\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # remove mysql unit files we created\n    rm -rf /etc/systemd/system/mariadb.service.d/\n    rm -f /etc/systemd/system/mariadb.service 2>/dev/null\n    systemctl daemon-reload\n\n    # Uninstall mysql / mariadb packages\n    apt-get -y remove --purge mysql\\*\n    apt-get -y remove --purge mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "mysql/ubuntu/24.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n    # create mysql user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel mysql &>/dev/null; groupdel mysql &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"Mysql Database Server\" mysql\n\n    # install mysql packages\n    apt-get install -y mariadb-server mariadb-client\n\n    # Make sure no extra configs present on fresh install\n    rm -f ~/.my.cnf\n\n    # setup aliases and if db is remote replace with dummy service file\n    reconfigureMysqlSystemdService\n\n    # TODO: selinux/apparmor permissions for mysql\n    #       firewall rules (cluster install needs remote access)\n    #       configure galera replication (cluster install)\n    #       configure group replication (cluster install)\n\n    # TODO: configure mysql to redirect error_log to syslog (as our other services do)\n    #       https://mariadb.com/kb/en/systemd/#configuring-mariadb-to-write-the-error-log-to-syslog\n\n    # TODO: configure logrotate to rotate syslog logs from mysql\n\n    return 0\n}\n\nfunction uninstall {\n    # Stop and disable services\n    systemctl stop mariadb\n    systemctl disable mariadb\n\n    # Backup mysql / mariadb\n    mv -f /var/lib/mysql /var/lib/mysql.bak.$(date +%Y%m%d_%H%M%S)\n\n    # remove mysql unit files we created\n    rm -rf /etc/systemd/system/mariadb.service.d/\n    rm -f /etc/systemd/system/mariadb.service 2>/dev/null\n    systemctl daemon-reload\n\n    # Uninstall mysql / mariadb packages\n    apt-get -y remove --purge mysql\\*\n    apt-get -y remove --purge mariadb\\*\n    rm -rf /etc/my.cnf*; rm -f /etc/my.cnf*; rm -f ~/*my.cnf\n\n    # TODO: remove selinux/apparmor rules\n\n    # TODO: remove mysql firewall rules\n\n    # TODO: remove mysql syslog config\n\n    # TODO: remove mysql logrotate config\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "nginx/almalinux/8.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel nginx &>/dev/null; groupdel nginx &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    dnf remove -y rs-epel-release*\n    dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-${DISTRO_MAJOR_VER}.noarch.rpm\n\n    dnf install -y nginx\n\n    if (( $? != 0 )); then\n        return 1\n    fi\n\n    # setup runtime directorys for nginx\n    mkdir -p /run/nginx\n    chown -R nginx:nginx /run/nginx\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    # configure nginx systemd service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v2.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v2.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\n\nfunction uninstall() {\n    # stop nginx and remove nginx package\n    systemctl stop nginx\n    systemctl disable nginx\n    dnf remove -y nginx\n\n    # remove nginx systemd service\n    rm -f /usr/sbin/nginx-stop\n    rm -f /lib/systemd/system/nginx.service\n    rm -f /lib/systemd/system/nginx-watcher.service\n    rm -f /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "nginx/almalinux/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel nginx &>/dev/null; groupdel nginx &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    dnf install -y nginx\n\n    if (( $? != 0 )); then\n        printerr 'failed installing nginx packages'\n        return 1\n    fi\n\n    # setup runtime directorys for nginx\n    mkdir -p /run/nginx\n    chown -R nginx:nginx /run/nginx\n\n    # give nginx permissions in SELINUX\n    semanage port -a -t http_port_t -p tcp ${DSIP_PORT} ||\n    semanage port -m -t http_port_t -p tcp ${DSIP_PORT}\n    # NOTE: /var/run is required here due to the aliasing in the fcontexts\n    #semanage fcontext -a -t httpd_var_run_t '/var/run/dsiprouter/dsiprouter\\.sock'\n    # TODO: this is a workaround, this the \"wrong\" way to do it\n    # we need to figure out why the fcontexts are not applying by default to new files\n    # and possibly (preferably) create our own type with those specific permissions\n    # for example a new type dsiprouter_run_t labeled on '/var/run/dsiprouter/.+'\n    (\n        if semodule -l | grep -q 'dsiprouter'; then\n            semodule -r dsiprouter\n        fi\n        cd /tmp &&\n        checkmodule -M -m -o dsiprouter.mod ${DSIP_PROJECT_DIR}/nginx/selinux/centos.te &&\n        semodule_package -o dsiprouter.pp -m dsiprouter.mod &&\n        semodule -i dsiprouter.pp\n    )\n    if (( $? != 0 )); then\n        printerr 'failed updating selinux permissions'\n        return 1\n    fi\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    # configure nginx systemd service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v2.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v2.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\nfunction uninstall() {\n    # stop nginx and remove nginx package\n    systemctl stop nginx\n    systemctl disable nginx\n    dnf remove -y nginx\n\n    # remove nginx systemd service\n    rm -f /usr/sbin/nginx-stop\n    rm -f /lib/systemd/system/nginx.service\n    rm -f /lib/systemd/system/nginx-watcher.service\n    rm -f /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n\n    # remove SELINUX permissions\n    semanage port -d -t http_port_t -p tcp ${DSIP_PORT}\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "nginx/amzn/2.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel nginx &>/dev/null; groupdel nginx &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    # Install nginx\n    amazon-linux-extras enable -y nginx1 >/dev/null\n    yum clean -y metadata\n    yum install -y nginx\n\n    if (( $? != 0 )); then\n        return 1\n    fi\n\n    # setup runtime directorys for nginx\n    mkdir -p /run/nginx\n    chown -R nginx:nginx /run/nginx\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    # configure nginx systemd service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v1.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v1.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\n\nfunction uninstall() {\n    # stop nginx and remove nginx package\n    systemctl stop nginx\n    systemctl disable nginx\n    yum remove -y nginx\n\n    # remove nginx systemd service\n    rm -f /usr/sbin/nginx-stop\n    rm -f /lib/systemd/system/nginx.service\n    rm -f /lib/systemd/system/nginx-watcher.service\n    rm -f /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "nginx/centos/7.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install {\n\n    # Get the default version of python enabled\n    VER=`python -V 2>&1`\n    VER=`echo $VER | cut -d \" \" -f 2`\n    # Uninstall 3.6 and install a specific version of 3.6 if already installed\n    if [[ \"$VER\" =~ 3.6 ]]; then\n        yum remove -y rs-epel-release\n        yum remove -y python36  python36-libs python36-devel python36-pip\n        yum install -y https://centos7.iuscommunity.org/ius-release.rpm\n        yum install -y python36u python36u-libs python36u-devel python36u-pip\n    elif [[ \"$VER\" =~ 3 ]]; then\n        yum remove -y rs-epel-release\n        yum remove -y python3* python3*-libs python3*-devel python3*-pip\n        yum install -y https://centos7.iuscommunity.org/ius-release.rpm\n        yum install -y python36u python36u-libs python36u-devel python36u-pip\n    else\n        yum install -y https://centos7.iuscommunity.org/ius-release.rpm\n        yum install -y python36u python36u-libs python36u-devel python36u-pip\n    fi\n\n   # Install dependencies for dSIPRouter\n    yum install -y yum-utils\n    yum --setopt=group_package_types=mandatory,default,optional groupinstall -y \"Development Tools\"\n    yum install -y firewalld nginx\n    yum install -y python36 python36-libs python36-devel python36-pip MySQL-python\n    yum install -y logrotate rsyslog perl libev-devel util-linux postgresql-devel mariadb-devel\n\n    # create dsiprouter and nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock\n    useradd --system --user-group --shell /bin/false --comment \"dSIPRouter SIP Provider Platform\" dsiprouter\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    # make sure the nginx user has access to dsiprouter directories\n    usermod -a -G dsiprouter nginx\n    # make dsiprouter user has access to kamailio files\n    usermod -a -G kamailio dsiprouter\n\n    # setup runtime directorys for dsiprouter and nginx\n    mkdir -p ${DSIP_RUN_DIR} /run/nginx\n    chown -R dsiprouter:dsiprouter ${DSIP_RUN_DIR}\n    chown -R nginx:nginx /run/nginx\n\n    # give nginx permissions in SELINUX\n    semanage port -a -t http_port_t -p tcp ${DSIP_PORT} ||\n    semanage port -m -t http_port_t -p tcp ${DSIP_PORT}\n    # NOTE: /var/run is required here due to the aliasing in the fcontexts\n    #semanage fcontext -a -t httpd_var_run_t '/var/run/dsiprouter/dsiprouter\\.sock'\n    # TODO: this is a workaround, this the \"wrong\" way to do it\n    # we need to figure out why the fcontexts are not applying by default to new files\n    # and possibly (preferably) create our own type with those specific permissions\n    # for example a new type dsiprouter_run_t labeled on '/var/run/dsiprouter/.+'\n    (\n        if semodule -l | grep -q 'dsiprouter'; then\n            semodule -r dsiprouter\n        fi\n        cd /tmp &&\n        checkmodule -M -m -o dsiprouter.mod ${DSIP_PROJECT_DIR}/nginx/selinux/centos.te &&\n        semodule_package -o dsiprouter.pp -m dsiprouter.mod &&\n        semodule -i dsiprouter.pp\n    )\n    if (( $? != 0 )); then\n        printerr 'failed updating selinux permissions'\n        return 1\n    fi\n\n    # reset python cmd in case it was just installed\n    setPythonCmd\n\n   # Enable and start firewalld\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    if (( $? != 0 )); then\n        # fix for bug: https://bugzilla.redhat.com/show_bug.cgi?id=1575845\n        systemctl restart dbus\n        systemctl restart firewalld\n        # fix for ensuing bug: https://bugzilla.redhat.com/show_bug.cgi?id=1372925\n        systemctl restart systemd-logind\n    fi\n\n    # Setup Firewall for DSIP_PORT\n    firewall-offline-cmd --zone=public --add-port=${DSIP_PORT}/tcp\n\n    cat ${DSIP_PROJECT_DIR}/gui/requirements.txt | xargs -n 1 ${PYTHON_CMD} -m pip install\n    if [ $? -eq 1 ]; then\n        printerr \"dSIPRouter install failed: Couldn't install required libraries\"\n        exit 1\n    fi\n\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v1.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v1.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\n\nfunction uninstall {\n    # Uninstall dependencies for dSIPRouter\n    PIP_CMD=\"pip\"\n\n    cat ${DSIP_PROJECT_DIR}/gui/requirements.txt | xargs -n 1 $PYTHON_CMD -m ${PIP_CMD} uninstall --yes\n    if [ $? -eq 1 ]; then\n        printerr \"dSIPRouter uninstall failed or the libraries are already uninstalled\"\n        exit 1\n    else\n        printdbg \"DSIPRouter uninstall was successful\"\n        exit 0\n    fi\n\n    yum remove -y python36u\\*\n    yum remove -y ius-release\n    yum remove -y nginx\n    yum groupremove -y \"Development Tools\"\n\n    # Remove the repos\n    rm -f /etc/yum.repos.d/ius*\n    rm -f /etc/pki/rpm-gpg/IUS-COMMUNITY-GPG-KEY\n    yum clean all\n\n    # Remove Firewall for DSIP_PORT\n    firewall-cmd --zone=public --remove-port=${DSIP_PORT}/tcp --permanent\n    firewall-cmd --reload\n\n    # Remove dSIPRouter Logging\n    rm -f /etc/rsyslog.d/dsiprouter.conf\n\n    # Remove logrotate settings\n    rm -f /etc/logrotate.d/dsiprouter\n\n    # Remove dSIProuter as a service\n    systemctl disable dsiprouter.service\n    rm -f /lib/systemd/system/dsiprouter.service\n    systemctl daemon-reload\n}\n\n\ncase \"$1\" in\n    uninstall|remove)\n        uninstall\n        ;;\n    install)\n        install\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "nginx/centos/8.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel nginx &>/dev/null; groupdel nginx &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    dnf install -y nginx\n\n    if (( $? != 0 )); then\n        return 1\n    fi\n\n    # setup runtime directorys for nginx\n    mkdir -p /run/nginx\n    chown -R nginx:nginx /run/nginx\n\n    # give nginx permissions in SELINUX\n    semanage port -a -t http_port_t -p tcp ${DSIP_PORT} ||\n    semanage port -m -t http_port_t -p tcp ${DSIP_PORT}\n    # NOTE: /var/run is required here due to the aliasing in the fcontexts\n    #semanage fcontext -a -t httpd_var_run_t '/var/run/dsiprouter/dsiprouter\\.sock'\n    # TODO: this is a workaround, this the \"wrong\" way to do it\n    # we need to figure out why the fcontexts are not applying by default to new files\n    # and possibly (preferably) create our own type with those specific permissions\n    # for example a new type dsiprouter_run_t labeled on '/var/run/dsiprouter/.+'\n    (\n        if semodule -l | grep -q 'dsiprouter'; then\n            semodule -r dsiprouter\n        fi\n        cd /tmp &&\n        checkmodule -M -m -o dsiprouter.mod ${DSIP_PROJECT_DIR}/nginx/selinux/centos.te &&\n        semodule_package -o dsiprouter.pp -m dsiprouter.mod &&\n        semodule -i dsiprouter.pp\n    )\n    if (( $? != 0 )); then\n        printerr 'failed updating selinux permissions'\n        return 1\n    fi\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    # configure nginx systemd service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v2.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v2.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\nfunction uninstall() {\n    # stop nginx and remove nginx package\n    systemctl stop nginx\n    systemctl disable nginx\n    dnf remove -y nginx\n\n    # remove nginx systemd service\n    rm -f /usr/sbin/nginx-stop\n    rm -f /lib/systemd/system/nginx.service\n    rm -f /lib/systemd/system/nginx-watcher.service\n    rm -f /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n\n    # remove SELINUX permissions\n    semanage port -d -t http_port_t -p tcp ${DSIP_PORT}\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "nginx/centos/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel nginx &>/dev/null; groupdel nginx &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    dnf install -y nginx\n\n    if (( $? != 0 )); then\n        printerr 'failed installing nginx packages'\n        return 1\n    fi\n\n    # setup runtime directorys for nginx\n    mkdir -p /run/nginx\n    chown -R nginx:nginx /run/nginx\n\n    # give nginx permissions in SELINUX\n    semanage port -a -t http_port_t -p tcp ${DSIP_PORT} ||\n    semanage port -m -t http_port_t -p tcp ${DSIP_PORT}\n    # NOTE: /var/run is required here due to the aliasing in the fcontexts\n    #semanage fcontext -a -t httpd_var_run_t '/var/run/dsiprouter/dsiprouter\\.sock'\n    # TODO: this is a workaround, this the \"wrong\" way to do it\n    # we need to figure out why the fcontexts are not applying by default to new files\n    # and possibly (preferably) create our own type with those specific permissions\n    # for example a new type dsiprouter_run_t labeled on '/var/run/dsiprouter/.+'\n    (\n        if semodule -l | grep -q 'dsiprouter'; then\n            semodule -r dsiprouter\n        fi\n        cd /tmp &&\n        checkmodule -M -m -o dsiprouter.mod ${DSIP_PROJECT_DIR}/nginx/selinux/centos.te &&\n        semodule_package -o dsiprouter.pp -m dsiprouter.mod &&\n        semodule -i dsiprouter.pp\n    )\n    if (( $? != 0 )); then\n        printerr 'failed updating selinux permissions'\n        return 1\n    fi\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    # configure nginx systemd service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v2.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v2.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\nfunction uninstall() {\n    # stop nginx and remove nginx package\n    systemctl stop nginx\n    systemctl disable nginx\n    dnf remove -y nginx\n\n    # remove nginx systemd service\n    rm -f /usr/sbin/nginx-stop\n    rm -f /lib/systemd/system/nginx.service\n    rm -f /lib/systemd/system/nginx-watcher.service\n    rm -f /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n\n    # remove SELINUX permissions\n    semanage port -d -t http_port_t -p tcp ${DSIP_PORT}\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "nginx/configs/dsiprouter.conf",
    "content": "# setup the dsiprouter server group (using unix sockets)\n# if multiple instances are running they can be configured here\nupstream dsiprouter {\n   server unix:DSIP_UNIX_SOCK;\n}\n\n# handle the https requests\nserver {\n    # by default we listen on all interfaces\n    listen DSIP_PORT ssl http2 so_keepalive=on;\n    listen [::]:DSIP_PORT ssl http2 so_keepalive=on;\n    server_name _;\n\n    ssl_certificate DSIP_SSL_CERT;\n    ssl_certificate_key DSIP_SSL_KEY;\n\n    # reverse proxy for dsiprouter\n    location / {\n        proxy_pass http://dsiprouter;\n        proxy_http_version 1.1;\n        proxy_cache_bypass $http_upgrade;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection \"upgrade\";\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n        proxy_set_header X-Forwarded-Host $host;\n        proxy_set_header X-Forwarded-Port $server_port;\n    }\n\n    location /stirshaken_certs {\n         alias /etc/dsiprouter/certs/stirshaken;\n    }\n\n\n    # redirect http to https\n    error_page 497 https://$host:DSIP_PORT$request_uri;\n}"
  },
  {
    "path": "nginx/configs/nginx.conf",
    "content": "user nginx;\nworker_processes auto;\nerror_log /var/log/nginx/error.log warn;\npid /run/nginx/nginx.pid;\ndaemon on;\nmaster_process on;\n\n# Load dynamic modules (ref: /usr/share/doc/nginx/README.dynamic)\ninclude /usr/share/nginx/modules/*.conf;\n\nevents {\n    worker_connections 1024;\n}\n\nhttp {\n    log_format  main  '$remote_addr - $remote_user [$time_local] \"$request\" '\n                      '$status $body_bytes_sent \"$http_referer\" '\n                      '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n\n    include /etc/nginx/mime.types;\n    default_type application/octet-stream;\n\n    sendfile on;\n    tcp_nopush on;\n    tcp_nodelay on;\n    keepalive_timeout 60;\n    access_log off;\n\n    sendfile_max_chunk 512k;\n\n    # TODO: needs optimizing\n    # https://medium.com/@mvuksano/how-to-properly-configure-your-nginx-for-tls-564651438fe0\n    ssl_session_cache   shared:SSL:10m;\n    ssl_session_timeout 10m;\n    ssl_protocols TLS_PROTOCOLS;\n    ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;\n    ssl_prefer_server_ciphers on;\n\n    # Load modular configuration files from the /etc/nginx/conf.d directory.\n    # See http://nginx.org/en/docs/ngx_core_module.html#include\n    # for more information.\n    include /etc/nginx/conf.d/*.conf;\n\n    # Include sites enabled in /etc/nginx/sites-enabled\n    include /etc/nginx/sites-enabled/*.conf;\n}\n"
  },
  {
    "path": "nginx/debian/10.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel nginx &>/dev/null; groupdel nginx &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    # Install dependencies for dSIPRouter\n    apt-get install -y nginx\n\n    if (( $? != 0 )); then\n        return 1\n    fi\n\n    # setup runtime directorys for nginx\n    mkdir -p /run/nginx\n    chown -R nginx:nginx /run/nginx\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    # configure nginx systemd service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v2.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v2.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\nfunction uninstall() {\n    # stop nginx and remove nginx package\n    systemctl stop nginx\n    systemctl disable nginx\n    apt-get remove -y nginx\n\n    # remove nginx systemd service\n    rm -f /usr/sbin/nginx-stop\n    rm -f /lib/systemd/system/nginx.service\n    rm -f /lib/systemd/system/nginx-watcher.service\n    rm -f /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "nginx/debian/11.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel nginx &>/dev/null; groupdel nginx &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    # Install dependencies for dSIPRouter\n    apt-get install -y nginx\n\n    if (( $? != 0 )); then\n        return 1\n    fi\n\n    # setup runtime directorys for nginx\n    mkdir -p /run/nginx\n    chown -R nginx:nginx /run/nginx\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    # configure nginx systemd service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v2.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v2.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\nfunction uninstall() {\n    # stop nginx and remove nginx package\n    systemctl stop nginx\n    systemctl disable nginx\n    apt-get remove -y nginx\n\n    # remove nginx systemd service\n    rm -f /usr/sbin/nginx-stop\n    rm -f /lib/systemd/system/nginx.service\n    rm -f /lib/systemd/system/nginx-watcher.service\n    rm -f /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "nginx/debian/12.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel nginx &>/dev/null; groupdel nginx &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    apt-get install -y nginx\n\n    if (( $? != 0 )); then\n        return 1\n    fi\n\n    # setup runtime directorys for nginx\n    mkdir -p /run/nginx\n    chown -R nginx:nginx /run/nginx\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    # configure nginx systemd service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v2.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v2.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\nfunction uninstall() {\n    # stop nginx and remove nginx package\n    systemctl stop nginx\n    systemctl disable nginx\n    apt-get remove -y nginx\n\n    # remove nginx systemd service\n    rm -f /usr/sbin/nginx-stop\n    rm -f /lib/systemd/system/nginx.service\n    rm -f /lib/systemd/system/nginx-watcher.service\n    rm -f /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "nginx/debian/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel nginx &>/dev/null; groupdel nginx &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    # Install dependencies for dSIPRouter\n    apt-get install -y nginx\n\n    if (( $? != 0 )); then\n        return 1\n    fi\n\n    # setup runtime directorys for nginx\n    mkdir -p /run/nginx\n    chown -R nginx:nginx /run/nginx\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    # configure nginx systemd service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v1.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v1.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\nfunction uninstall() {\n    # stop nginx and remove nginx package\n    systemctl stop nginx\n    systemctl disable nginx\n    apt-get remove -y nginx\n\n    # remove nginx systemd service\n    rm -f /usr/sbin/nginx-stop\n    rm -f /lib/systemd/system/nginx.service\n    rm -f /lib/systemd/system/nginx-watcher.service\n    rm -f /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "nginx/rhel/8.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel nginx &>/dev/null; groupdel nginx &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    dnf remove -y rs-epel-release*\n    dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-${DISTRO_MAJOR_VER}.noarch.rpm\n\n    dnf install -y nginx\n\n    if (( $? != 0 )); then\n        return 1\n    fi\n\n    # setup runtime directorys for nginx\n    mkdir -p /run/nginx\n    chown -R nginx:nginx /run/nginx\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    # configure nginx systemd service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v1.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v1.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\n\nfunction uninstall() {\n    # stop nginx and remove nginx package\n    systemctl stop nginx\n    systemctl disable nginx\n    dnf remove -y nginx\n\n    # remove nginx systemd service\n    rm -f /usr/sbin/nginx-stop\n    rm -f /lib/systemd/system/nginx.service\n    rm -f /lib/systemd/system/nginx-watcher.service\n    rm -f /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "nginx/rhel/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel nginx &>/dev/null; groupdel nginx &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    dnf install -y nginx\n\n    if (( $? != 0 )); then\n        printerr 'failed installing nginx packages'\n        return 1\n    fi\n\n    # setup runtime directorys for nginx\n    mkdir -p /run/nginx\n    chown -R nginx:nginx /run/nginx\n\n    # give nginx permissions in SELINUX\n    semanage port -a -t http_port_t -p tcp ${DSIP_PORT} ||\n    semanage port -m -t http_port_t -p tcp ${DSIP_PORT}\n    # NOTE: /var/run is required here due to the aliasing in the fcontexts\n    #semanage fcontext -a -t httpd_var_run_t '/var/run/dsiprouter/dsiprouter\\.sock'\n    # TODO: this is a workaround, this the \"wrong\" way to do it\n    # we need to figure out why the fcontexts are not applying by default to new files\n    # and possibly (preferably) create our own type with those specific permissions\n    # for example a new type dsiprouter_run_t labeled on '/var/run/dsiprouter/.+'\n    (\n        if semodule -l | grep -q 'dsiprouter'; then\n            semodule -r dsiprouter\n        fi\n        cd /tmp &&\n        checkmodule -M -m -o dsiprouter.mod ${DSIP_PROJECT_DIR}/nginx/selinux/centos.te &&\n        semodule_package -o dsiprouter.pp -m dsiprouter.mod &&\n        semodule -i dsiprouter.pp\n    )\n    if (( $? != 0 )); then\n        printerr 'failed updating selinux permissions'\n        return 1\n    fi\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    # configure nginx systemd service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v2.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v2.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\nfunction uninstall() {\n    # stop nginx and remove nginx package\n    systemctl stop nginx\n    systemctl disable nginx\n    dnf remove -y nginx\n\n    # remove nginx systemd service\n    rm -f /usr/sbin/nginx-stop\n    rm -f /lib/systemd/system/nginx.service\n    rm -f /lib/systemd/system/nginx-watcher.service\n    rm -f /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n\n    # remove SELINUX permissions\n    semanage port -d -t http_port_t -p tcp ${DSIP_PORT}\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "nginx/rocky/8.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel nginx &>/dev/null; groupdel nginx &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    dnf remove -y rs-epel-release*\n    dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-${DISTRO_MAJOR_VER}.noarch.rpm\n\n    dnf install -y nginx\n\n    if (( $? != 0 )); then\n        return 1\n    fi\n\n    # setup runtime directorys for nginx\n    mkdir -p /run/nginx\n    chown -R nginx:nginx /run/nginx\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    # configure nginx systemd service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v2.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v2.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\n\nfunction uninstall() {\n    # stop nginx and remove nginx package\n    systemctl stop nginx\n    systemctl disable nginx\n    dnf remove -y nginx\n\n    # remove nginx systemd service\n    rm -f /usr/sbin/nginx-stop\n    rm -f /lib/systemd/system/nginx.service\n    rm -f /lib/systemd/system/nginx-watcher.service\n    rm -f /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "nginx/rocky/9.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel nginx &>/dev/null; groupdel nginx &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    dnf install -y nginx\n\n    if (( $? != 0 )); then\n        printerr 'failed installing nginx packages'\n        return 1\n    fi\n\n    # setup runtime directorys for nginx\n    mkdir -p /run/nginx\n    chown -R nginx:nginx /run/nginx\n\n    # give nginx permissions in SELINUX\n    semanage port -a -t http_port_t -p tcp ${DSIP_PORT} ||\n    semanage port -m -t http_port_t -p tcp ${DSIP_PORT}\n    # NOTE: /var/run is required here due to the aliasing in the fcontexts\n    #semanage fcontext -a -t httpd_var_run_t '/var/run/dsiprouter/dsiprouter\\.sock'\n    # TODO: this is a workaround, this the \"wrong\" way to do it\n    # we need to figure out why the fcontexts are not applying by default to new files\n    # and possibly (preferably) create our own type with those specific permissions\n    # for example a new type dsiprouter_run_t labeled on '/var/run/dsiprouter/.+'\n    (\n        if semodule -l | grep -q 'dsiprouter'; then\n            semodule -r dsiprouter\n        fi\n        cd /tmp &&\n        checkmodule -M -m -o dsiprouter.mod ${DSIP_PROJECT_DIR}/nginx/selinux/centos.te &&\n        semodule_package -o dsiprouter.pp -m dsiprouter.mod &&\n        semodule -i dsiprouter.pp\n    )\n    if (( $? != 0 )); then\n        printerr 'failed updating selinux permissions'\n        return 1\n    fi\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    # configure nginx systemd service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v2.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v2.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\nfunction uninstall() {\n    # stop nginx and remove nginx package\n    systemctl stop nginx\n    systemctl disable nginx\n    dnf remove -y nginx\n\n    # remove nginx systemd service\n    rm -f /usr/sbin/nginx-stop\n    rm -f /lib/systemd/system/nginx.service\n    rm -f /lib/systemd/system/nginx-watcher.service\n    rm -f /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n\n    # remove SELINUX permissions\n    semanage port -d -t http_port_t -p tcp ${DSIP_PORT}\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "nginx/selinux/centos.te",
    "content": "module dsiprouter 1.0;\n\nrequire {\n\ttype unconfined_t;\n\ttype unconfined_service_t;\n\ttype var_run_t;\n\ttype httpd_t;\n\tclass sock_file write;\n\tclass unix_stream_socket connectto;\n}\n\nallow httpd_t unconfined_t:unix_stream_socket connectto;\nallow httpd_t unconfined_service_t:unix_stream_socket connectto;\nallow httpd_t var_run_t:sock_file write;\n"
  },
  {
    "path": "nginx/systemd/nginx-stop.sh",
    "content": "#!/usr/bin/env bash\n\nTIMEOUT=5\nMAINPID=\"$1\"\nPIDFILE=\"$2\"\n\nif [[ -z \"$(ps -p $MAINPID -o pid= 2>/dev/null)\" ]]; then\n    rm -f ${PIDFILE} 2>/dev/null\n    exit 0\nfi\n\nkill -s SIGSTOP $MAINPID\nfor (( ROUND=0; ROUND<$TIMEOUT; ROUND++ )); do\n    if [[ -z \"$(ps -p $MAINPID -o pid= 2>/dev/null)\" ]]; then\n        rm -f ${PIDFILE} 2>/dev/null\n        exit 0\n    fi\n    sleep 1\ndone\n\nkill -s SIGQUIT $MAINPID\nfor (( ROUND=0; ROUND<$TIMEOUT; ROUND++ )); do\n    if [[ -z \"$(ps -p $MAINPID -o pid= 2>/dev/null)\" ]]; then\n        rm -f ${PIDFILE} 2>/dev/null\n        exit 0\n    fi\n    sleep 1\ndone\n\nexit 1\n\n"
  },
  {
    "path": "nginx/systemd/nginx-v1.service",
    "content": "# ExecStop sends SIGQUIT to the main process\n# If, after 3s nginx is still running, sends SIGTERM to main process\n# If, after 6s nginx is still running, sends SIGKILL to all processes\n[Unit]\nDescription=A high performance web server and a reverse proxy server\nDocumentation=man:nginx(8)\nRequires=basic.target network.target\nWants=nginx-watcher.path\nAfter=network.target network-online.target basic.target nss-lookup.target remote-fs.target\nDefaultDependencies=no\n\n[Service]\nType=forking\nEnvironment='RUNDIR=/run/nginx'\nPIDFile=/run/nginx/nginx.pid\nExecStartPre=/usr/bin/dsiprouter chown -certs -nginx\nExecStartPre=/usr/sbin/nginx -t\nExecStart=/usr/sbin/nginx\nExecReload=/usr/sbin/nginx -s reload\nExecStop=/bin/kill -s SIGQUIT ${MAINPID}\nTimeoutStopSec=3\nKillMode=mixed\nRestart=on-failure\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "nginx/systemd/nginx-v2.service",
    "content": "# ExecStop sends SIGQUIT to the main process\n# If, after 3s nginx is still running, sends SIGTERM to main process\n# If, after 6s nginx is still running, sends SIGKILL to all processes\n[Unit]\nDescription=A high performance web server and a reverse proxy server\nDocumentation=man:nginx(8)\nRequires=basic.target network.target\nWants=nginx-watcher.path\nAfter=network.target network-online.target basic.target nss-lookup.target remote-fs.target\nDefaultDependencies=no\n\n[Service]\nType=forking\nEnvironment='RUNDIR=/run/nginx'\nPIDFile=/run/nginx/nginx.pid\nExecStartPre=!-/usr/bin/dsiprouter chown -certs -nginx\nExecStartPre=/usr/sbin/nginx -t\nExecStart=/usr/sbin/nginx\nExecReload=/usr/sbin/nginx -s reload\nExecStop=/bin/kill -s SIGQUIT ${MAINPID}\nTimeoutStopSec=3\nKillMode=mixed\nRestart=on-failure\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "nginx/systemd/nginx-watcher-v1.service",
    "content": "[Unit]\nDescription=Nginx Service Reloader\n\n[Service]\nType=oneshot\nExecStart=/usr/sbin/nginx -s reload\nStartLimitInterval=5\nStartLimitBurst=3\n\n[Install]\nWantedBy=multi-user.target"
  },
  {
    "path": "nginx/systemd/nginx-watcher-v2.service",
    "content": "[Unit]\nDescription=Nginx Service Reloader\nStartLimitIntervalSec=5\nStartLimitBurst=3\n\n[Service]\nType=oneshot\nExecStart=/usr/sbin/nginx -s reload\n\n[Install]\nWantedBy=multi-user.target"
  },
  {
    "path": "nginx/systemd/nginx-watcher.path",
    "content": "[Unit]\nDescription=Nginx Service Reloader\nPartOf=nginx.service\n\n[Path]\nUnit=nginx-watcher.service\nPathChanged=/etc/dsiprouter/certs/\n\n[Install]\nWantedBy=multi-user.target"
  },
  {
    "path": "nginx/ubuntu/20.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel nginx &>/dev/null; groupdel nginx &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    # Install dependencies for dSIPRouter\n    apt-get install -y nginx\n\n    if (( $? != 0 )); then\n        return 1\n    fi\n\n    # setup runtime directorys for nginx\n    mkdir -p /run/nginx\n    chown -R nginx:nginx /run/nginx\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    # configure nginx systemd service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v2.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v2.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\nfunction uninstall() {\n    # stop nginx and remove nginx package\n    systemctl stop nginx\n    systemctl disable nginx\n    apt-get remove -y nginx\n\n    # remove nginx systemd service\n    rm -f /usr/sbin/nginx-stop\n    rm -f /lib/systemd/system/nginx.service\n    rm -f /lib/systemd/system/nginx-watcher.service\n    rm -f /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "nginx/ubuntu/22.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel nginx &>/dev/null; groupdel nginx &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    # Install dependencies for dSIPRouter\n    apt-get install -y nginx\n\n    if (( $? != 0 )); then\n        return 1\n    fi\n\n    # setup runtime directorys for nginx\n    mkdir -p /run/nginx\n    chown -R nginx:nginx /run/nginx\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    # configure nginx systemd service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v2.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v2.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\nfunction uninstall() {\n    # stop nginx and remove nginx package\n    systemctl stop nginx\n    systemctl disable nginx\n    apt-get remove -y nginx\n\n    # remove nginx systemd service\n    rm -f /usr/sbin/nginx-stop\n    rm -f /lib/systemd/system/nginx.service\n    rm -f /lib/systemd/system/nginx-watcher.service\n    rm -f /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "nginx/ubuntu/24.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction install() {\n    # create nginx user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel nginx &>/dev/null; groupdel nginx &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"nginx HTTP Service Provider\" nginx\n\n    # Install dependencies for dSIPRouter\n    apt-get install -y nginx\n\n    if (( $? != 0 )); then\n        return 1\n    fi\n\n    # setup runtime directorys for nginx\n    mkdir -p /run/nginx\n    chown -R nginx:nginx /run/nginx\n\n    # Configure nginx\n    # determine available TLS protocols (try using highest available)\n    OPENSSL_VER=$(openssl version 2>/dev/null | awk '{print $2}' | perl -pe 's%([0-9])\\.([0-9]).([0-9]).*%\\1\\2\\3%')\n    if (( ${OPENSSL_VER} < 101 )); then\n        TLS_PROTOCOLS=\"TLSv1\"\n    elif (( ${OPENSSL_VER} < 111 )); then\n        TLS_PROTOCOLS=\"TLSv1.1 TLSv1.2\"\n    else\n        TLS_PROTOCOLS=\"TLSv1.2 TLSv1.3\"\n    fi\n    mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/nginx.conf.d/\n    # remove the defaults\n    rm -f /etc/nginx/sites-enabled/* /etc/nginx/sites-available/* /etc/nginx/nginx.conf.d/*\n    # setup our own nginx configs\n    perl -e \"\\$tls_protocols='${TLS_PROTOCOLS}';\" \\\n        -pe 's%TLS_PROTOCOLS%${tls_protocols}%g;' \\\n        ${DSIP_PROJECT_DIR}/nginx/configs/nginx.conf >/etc/nginx/nginx.conf\n\n    # configure nginx systemd service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-stop.sh /usr/sbin/nginx-stop\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-v2.service /lib/systemd/system/nginx.service\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher-v2.service /lib/systemd/system/nginx-watcher.service\n    perl -p \\\n        -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n        ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/lib/systemd/system/nginx-watcher.path\n    chmod 644 /lib/systemd/system/nginx.service\n    chmod 644 /lib/systemd/system/nginx-watcher.service\n    chmod 644 /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n    systemctl enable nginx\n\n    return 0\n}\n\nfunction uninstall() {\n    # stop nginx and remove nginx package\n    systemctl stop nginx\n    systemctl disable nginx\n    apt-get remove -y nginx\n\n    # remove nginx systemd service\n    rm -f /usr/sbin/nginx-stop\n    rm -f /lib/systemd/system/nginx.service\n    rm -f /lib/systemd/system/nginx-watcher.service\n    rm -f /lib/systemd/system/nginx-watcher.path\n    systemctl daemon-reload\n\n    return 0\n}\n\ncase \"$1\" in\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    install)\n        install && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"usage $0 [install | uninstall]\"\n        ;;\nesac\n"
  },
  {
    "path": "resources/apt/debian/10/official-releases.list",
    "content": "#-------------------------------------------------------------------\n# packages from debian 10 (buster) release\n#-------------------------------------------------------------------\ndeb https://deb.debian.org/debian/ buster main contrib non-free\n#deb-src https://deb.debian.org/debian/ buster main contrib non-free\n\ndeb https://deb.debian.org/debian/ buster-updates main contrib non-free\n#deb-src https://deb.debian.org/debian/ buster-updates main contrib non-free\n\ndeb https://deb.debian.org/debian/ buster-backports main contrib non-free\n#deb-src https://deb.debian.org/debian/ buster-backports main contrib non-free\n\ndeb https://deb.debian.org/debian-security/ buster/updates main\n#deb-src https://deb.debian.org/debian-security/ buster/updates main\n\n#-------------------------------------------------------------------\n# packages from debian 11 (bullseye) release\n#-------------------------------------------------------------------\ndeb https://deb.debian.org/debian/ bullseye main contrib non-free\n#deb-src https://deb.debian.org/debian/ bullseye main contrib non-free\n\ndeb https://deb.debian.org/debian/ bullseye-updates main contrib non-free\n#deb-src https://deb.debian.org/debian/ bullseye-updates main contrib non-free\n\ndeb https://deb.debian.org/debian/ bullseye-backports main\n#deb-src https://deb.debian.org/debian/ bullseye-backports main\n\ndeb https://deb.debian.org/debian-security/ bullseye-security main\n#deb-src https://deb.debian.org/debian-security/ bullseye-security main\n"
  },
  {
    "path": "resources/apt/debian/10/official-releases.pref",
    "content": "## default priory assignments\n# priority 1\n# versions coming from archives which in their Release files are marked as \"NotAutomatic: yes\",\n# but not as \"ButAutomaticUpgrades: yes\" like the Debian experimental archive.\n#\n# priority 100\n# a version that is already installed (if any) and to the versions coming from archives which,\n# in their Release files are marked as \"NotAutomatic: yes\" and \"ButAutomaticUpgrades: yes\",\n# like the Debian backports archive since squeeze-backports.\n#\n# priority 500\n# versions that do not belong to the target release.\n#\n# priority 990\n# versions that belong to the target release.\n#\n## interpretation of priority (P)\n# P >= 1000\n# causes a version to be installed even if this constitutes a downgrade of the package\n#\n# 990 <= P < 1000\n# causes a version to be installed even if it does not come from the target release, unless the installed version is more recent\n#\n# 500 <= P < 990\n# causes a version to be installed unless there is a version available belonging to the target release or the installed version is more recent\n#\n# 100 <= P < 500\n# causes a version to be installed unless there is a version available belonging to some other distribution or the installed version is more recent\n#\n# 0 < P < 100\n# causes a version to be installed only if there is no installed version of the package\n#\n# P < 0\n# prevents the version from being installed\n#\n# P = 0\n# has undefined behaviour (do not use it)\n#\n\n#-------------------------------------------------------------------\n# priority for debian 10 (buster) release packages\n#-------------------------------------------------------------------\nPackage: *\nPin: release o=Debian,n=buster\nPin-Priority: 990\n\nPackage: *\nPin: release o=Debian,n=buster-updates\nPin-Priority: 990\n\nPackage: *\nPin: release o=Debian,n=buster-backports\nPin-Priority: 990\n\n#-------------------------------------------------------------------\n# priority for debian 11 (bullseye) release packages\n#-------------------------------------------------------------------\nPackage: *\nPin: release o=Debian,n=bullseye\nPin-Priority: 500\n\nPackage: *\nPin: release o=Debian,n=bullseye-updates\nPin-Priority: 500\n\nPackage: *\nPin: release o=Debian,n=bullseye-security\nPin-Priority: 500\n\nPackage: *\nPin: release o=Debian,n=bullseye-backports\nPin-Priority: 500\n"
  },
  {
    "path": "resources/apt/debian/11/official-releases.list",
    "content": "#-------------------------------------------------------------------\n# packages from debian 11 (bullseye) release\n#-------------------------------------------------------------------\ndeb https://deb.debian.org/debian/ bullseye main contrib non-free\n#deb-src https://deb.debian.org/debian/ bullseye main contrib non-free\n\ndeb https://deb.debian.org/debian/ bullseye-updates main contrib non-free\n#deb-src https://deb.debian.org/debian/ bullseye-updates main contrib non-free\n\ndeb https://deb.debian.org/debian/ bullseye-backports main\n#deb-src https://deb.debian.org/debian/ bullseye-backports main\n\ndeb https://deb.debian.org/debian-security/ bullseye-security main\n#deb-src https://deb.debian.org/debian-security/ bullseye-security main\n"
  },
  {
    "path": "resources/apt/debian/11/official-releases.pref",
    "content": "## default priory assignments\n# priority 1\n# versions coming from archives which in their Release files are marked as \"NotAutomatic: yes\",\n# but not as \"ButAutomaticUpgrades: yes\" like the Debian experimental archive.\n#\n# priority 100\n# a version that is already installed (if any) and to the versions coming from archives which,\n# in their Release files are marked as \"NotAutomatic: yes\" and \"ButAutomaticUpgrades: yes\",\n# like the Debian backports archive since squeeze-backports.\n#\n# priority 500\n# versions that do not belong to the target release.\n#\n# priority 990\n# versions that belong to the target release.\n#\n## interpretation of priority (P)\n# P >= 1000\n# causes a version to be installed even if this constitutes a downgrade of the package\n#\n# 990 <= P < 1000\n# causes a version to be installed even if it does not come from the target release, unless the installed version is more recent\n#\n# 500 <= P < 990\n# causes a version to be installed unless there is a version available belonging to the target release or the installed version is more recent\n#\n# 100 <= P < 500\n# causes a version to be installed unless there is a version available belonging to some other distribution or the installed version is more recent\n#\n# 0 < P < 100\n# causes a version to be installed only if there is no installed version of the package\n#\n# P < 0\n# prevents the version from being installed\n#\n# P = 0\n# has undefined behaviour (do not use it)\n#\n\n#-------------------------------------------------------------------\n# priority for debian 11 (bullseye) release packages\n#-------------------------------------------------------------------\nPackage: *\nPin: release o=Debian,n=bullseye\nPin-Priority: 990\n\nPackage: *\nPin: release o=Debian,n=bullseye-updates\nPin-Priority: 990\n\nPackage: *\nPin: release o=Debian,n=bullseye-security\nPin-Priority: 990\n\nPackage: *\nPin: release o=Debian,n=bullseye-backports\nPin-Priority: 990\n"
  },
  {
    "path": "resources/apt/debian/12/official-releases.list",
    "content": "#-------------------------------------------------------------------\n# packages from debian 12 (bookworm) release\n#-------------------------------------------------------------------\ndeb https://deb.debian.org/debian/ bookworm main contrib non-free\n#deb-src https://deb.debian.org/debian/ bookworm main contrib non-free\n\ndeb https://deb.debian.org/debian/ bookworm-updates main contrib non-free\n#deb-src https://deb.debian.org/debian/ bookworm-updates main contrib non-free\n\ndeb https://deb.debian.org/debian/ bookworm-backports main\n#deb-src https://deb.debian.org/debian/ bookworm-backports main\n\ndeb https://deb.debian.org/debian-security/ bookworm-security main\n#deb-src https://deb.debian.org/debian-security/ bookworm-security main\n"
  },
  {
    "path": "resources/apt/debian/12/official-releases.pref",
    "content": "## default priory assignments\n# priority 1\n# versions coming from archives which in their Release files are marked as \"NotAutomatic: yes\",\n# but not as \"ButAutomaticUpgrades: yes\" like the Debian experimental archive.\n#\n# priority 100\n# a version that is already installed (if any) and to the versions coming from archives which,\n# in their Release files are marked as \"NotAutomatic: yes\" and \"ButAutomaticUpgrades: yes\",\n# like the Debian backports archive since squeeze-backports.\n#\n# priority 500\n# versions that do not belong to the target release.\n#\n# priority 990\n# versions that belong to the target release.\n#\n## interpretation of priority (P)\n# P >= 1000\n# causes a version to be installed even if this constitutes a downgrade of the package\n#\n# 990 <= P < 1000\n# causes a version to be installed even if it does not come from the target release, unless the installed version is more recent\n#\n# 500 <= P < 990\n# causes a version to be installed unless there is a version available belonging to the target release or the installed version is more recent\n#\n# 100 <= P < 500\n# causes a version to be installed unless there is a version available belonging to some other distribution or the installed version is more recent\n#\n# 0 < P < 100\n# causes a version to be installed only if there is no installed version of the package\n#\n# P < 0\n# prevents the version from being installed\n#\n# P = 0\n# has undefined behaviour (do not use it)\n#\n\n#-------------------------------------------------------------------\n# priority for debian 12 (bookworm) release packages\n#-------------------------------------------------------------------\nPackage: *\nPin: release o=Debian,n=bookworm\nPin-Priority: 990\n\nPackage: *\nPin: release o=Debian,n=bookworm-updates\nPin-Priority: 990\n\nPackage: *\nPin: release o=Debian,n=bookworm-security\nPin-Priority: 990\n\nPackage: *\nPin: release o=Debian,n=bookworm-backports\nPin-Priority: 990\n"
  },
  {
    "path": "resources/apt/debian/9/official-releases.list",
    "content": "#-------------------------------------------------------------------\n# packages from debian 9 (stretch) release\n#-------------------------------------------------------------------\ndeb http://deb.debian.org/debian/ stretch main contrib non-free\n#deb-src http://deb.debian.org/debian/ stretch main contrib non-free\n\ndeb http://deb.debian.org/debian/ stretch-updates main contrib non-free\n#deb-src http://deb.debian.org/debian/ stretch-updates main contrib non-free\n\ndeb http://deb.debian.org/debian-security stretch/updates main\n#deb-src http://deb.debian.org/debian-security stretch/updates main\n\ndeb http://deb.debian.org/debian stretch-backports main\n#deb-src http://deb.debian.org/debian stretch-backports main\n\n#-------------------------------------------------------------------\n# packages from debian 10 (buster) release\n#-------------------------------------------------------------------\ndeb https://deb.debian.org/debian/ buster main contrib non-free\n#deb-src https://deb.debian.org/debian/ buster main contrib non-free\n\ndeb https://deb.debian.org/debian/ buster-updates main contrib non-free\n#deb-src https://deb.debian.org/debian/ buster-updates main contrib non-free\n\ndeb https://deb.debian.org/debian/ buster-backports main contrib non-free\n#deb-src https://deb.debian.org/debian/ buster-backports main contrib non-free\n\ndeb https://deb.debian.org/debian-security/ buster/updates main\n#deb-src https://deb.debian.org/debian-security/ buster/updates main\n\n#-------------------------------------------------------------------\n# packages from debian 11 (bullseye) release\n#-------------------------------------------------------------------\ndeb https://deb.debian.org/debian/ bullseye main contrib non-free\n#deb-src https://deb.debian.org/debian/ bullseye main contrib non-free\n\ndeb https://deb.debian.org/debian/ bullseye-updates main contrib non-free\n#deb-src https://deb.debian.org/debian/ bullseye-updates main contrib non-free\n\ndeb https://deb.debian.org/debian/ bullseye-backports main\n#deb-src https://deb.debian.org/debian/ bullseye-backports main\n\ndeb https://deb.debian.org/debian-security/ bullseye-security main\n#deb-src https://deb.debian.org/debian-security/ bullseye-security main\n"
  },
  {
    "path": "resources/apt/debian/9/official-releases.pref",
    "content": "## default priory assignments\n# priority 1\n# versions coming from archives which in their Release files are marked as \"NotAutomatic: yes\",\n# but not as \"ButAutomaticUpgrades: yes\" like the Debian experimental archive.\n#\n# priority 100\n# a version that is already installed (if any) and to the versions coming from archives which,\n# in their Release files are marked as \"NotAutomatic: yes\" and \"ButAutomaticUpgrades: yes\",\n# like the Debian backports archive since squeeze-backports.\n#\n# priority 500\n# versions that do not belong to the target release.\n#\n# priority 990\n# versions that belong to the target release.\n#\n## interpretation of priority (P)\n# P >= 1000\n# causes a version to be installed even if this constitutes a downgrade of the package\n#\n# 990 <= P < 1000\n# causes a version to be installed even if it does not come from the target release, unless the installed version is more recent\n#\n# 500 <= P < 990\n# causes a version to be installed unless there is a version available belonging to the target release or the installed version is more recent\n#\n# 100 <= P < 500\n# causes a version to be installed unless there is a version available belonging to some other distribution or the installed version is more recent\n#\n# 0 < P < 100\n# causes a version to be installed only if there is no installed version of the package\n#\n# P < 0\n# prevents the version from being installed\n#\n# P = 0\n# has undefined behaviour (do not use it)\n#\n\n#-------------------------------------------------------------------\n# priority for debian 9 (stretch) release packages\n#-------------------------------------------------------------------\nPackage: *\nPin: release o=Debian,n=stretch\nPin-Priority: 990\n\nPackage: *\nPin: release o=Debian,n=stretch-updates\nPin-Priority: 990\n\nPackage: *\nPin: release o=Debian,n=stretch-backports\nPin-Priority: 990\n\n#-------------------------------------------------------------------\n# priority for debian 10 (buster) release packages\n#-------------------------------------------------------------------\nPackage: *\nPin: release o=Debian,n=buster\nPin-Priority: 500\n\nPackage: *\nPin: release o=Debian,n=buster-updates\nPin-Priority: 500\n\nPackage: *\nPin: release o=Debian,n=buster-backports\nPin-Priority: 500\n\n#-------------------------------------------------------------------\n# priority for debian 11 (bullseye) release packages\n#-------------------------------------------------------------------\nPackage: *\nPin: release o=Debian,n=bullseye\nPin-Priority: 100\n\nPackage: *\nPin: release o=Debian,n=bullseye-updates\nPin-Priority: 100\n\nPackage: *\nPin: release o=Debian,n=bullseye-security\nPin-Priority: 100\n\nPackage: *\nPin: release o=Debian,n=bullseye-backports\nPin-Priority: 100\n"
  },
  {
    "path": "resources/apt/ubuntu/20.04/official-releases.list",
    "content": "#-------------------------------------------------------------------\n# packages from ubuntu 20.04 (focal) release\n#-------------------------------------------------------------------\ndeb https://nyc.mirrors.clouvider.net/ubuntu/ focal main restricted universe multiverse\n#deb-src https://nyc.mirrors.clouvider.net/ubuntu/ focal main restricted universe multiverse\n\ndeb https://nyc.mirrors.clouvider.net/ubuntu/ focal-security main restricted universe multiverse\n#deb-src https://nyc.mirrors.clouvider.net/ubuntu/ focal-security main restricted universe multiverse\n\ndeb https://nyc.mirrors.clouvider.net/ubuntu/ focal-updates main restricted universe multiverse\n#deb-src https://nyc.mirrors.clouvider.net/ubuntu/ focal-updates main restricted universe multiverse\n\ndeb https://nyc.mirrors.clouvider.net/ubuntu/ focal-backports main restricted universe multiverse\n#deb-src https://nyc.mirrors.clouvider.net/ubuntu/ focal-backports main restricted universe multiverse\n\n#deb https://nyc.mirrors.clouvider.net/ubuntu/ focal-proposed main restricted universe multiverse\n#deb-src https://nyc.mirrors.clouvider.net/ubuntu/ focal-proposed main restricted universe multiverse\n"
  },
  {
    "path": "resources/apt/ubuntu/20.04/official-releases.pref",
    "content": "## default priory assignments\n# priority 1\n# versions coming from archives which in their Release files are marked as \"NotAutomatic: yes\",\n# but not as \"ButAutomaticUpgrades: yes\" like the Ubuntu experimental archive.\n#\n# priority 100\n# a version that is already installed (if any) and to the versions coming from archives which,\n# in their Release files are marked as \"NotAutomatic: yes\" and \"ButAutomaticUpgrades: yes\",\n# like the Ubuntu backports archive since squeeze-backports.\n#\n# priority 500\n# versions that do not belong to the target release.\n#\n# priority 990\n# versions that belong to the target release.\n#\n## interpretation of priority (P)\n# P >= 1000\n# causes a version to be installed even if this constitutes a downgrade of the package\n#\n# 990 <= P < 1000\n# causes a version to be installed even if it does not come from the target release, unless the installed version is more recent\n#\n# 500 <= P < 990\n# causes a version to be installed unless there is a version available belonging to the target release or the installed version is more recent\n#\n# 100 <= P < 500\n# causes a version to be installed unless there is a version available belonging to some other distribution or the installed version is more recent\n#\n# 0 < P < 100\n# causes a version to be installed only if there is no installed version of the package\n#\n# P < 0\n# prevents the version from being installed\n#\n# P = 0\n# has undefined behaviour (do not use it)\n#\n\n#-------------------------------------------------------------------\n# priority for ubuntu 20.04 (focal) release packages\n#-------------------------------------------------------------------\nPackage: *\nPin: release o=Ubuntu,n=focal\nPin-Priority: 990\n\nPackage: *\nPin: release o=Ubuntu,a=focal\nPin-Priority: 990\n"
  },
  {
    "path": "resources/apt/ubuntu/22.04/official-releases.list",
    "content": "#-------------------------------------------------------------------\n# packages from ubuntu 22.04 (jammy) release\n#-------------------------------------------------------------------\ndeb https://nyc.mirrors.clouvider.net/ubuntu/ jammy main restricted universe multiverse\n#deb-src https://nyc.mirrors.clouvider.net/ubuntu/ jammy main restricted universe multiverse\n\ndeb https://nyc.mirrors.clouvider.net/ubuntu/ jammy-security main restricted universe multiverse\n#deb-src https://nyc.mirrors.clouvider.net/ubuntu/ jammy-security main restricted universe multiverse\n\ndeb https://nyc.mirrors.clouvider.net/ubuntu/ jammy-updates main restricted universe multiverse\n#deb-src https://nyc.mirrors.clouvider.net/ubuntu/ jammy-updates main restricted universe multiverse\n\ndeb https://nyc.mirrors.clouvider.net/ubuntu/ jammy-backports main restricted universe multiverse\n#deb-src https://nyc.mirrors.clouvider.net/ubuntu/ jammy-backports main restricted universe multiverse\n\n#deb https://nyc.mirrors.clouvider.net/ubuntu/ jammy-proposed main restricted universe multiverse\n#deb-src https://nyc.mirrors.clouvider.net/ubuntu/ jammy-proposed main restricted universe multiverse\n"
  },
  {
    "path": "resources/apt/ubuntu/22.04/official-releases.pref",
    "content": "## default priory assignments\n# priority 1\n# versions coming from archives which in their Release files are marked as \"NotAutomatic: yes\",\n# but not as \"ButAutomaticUpgrades: yes\" like the Ubuntu experimental archive.\n#\n# priority 100\n# a version that is already installed (if any) and to the versions coming from archives which,\n# in their Release files are marked as \"NotAutomatic: yes\" and \"ButAutomaticUpgrades: yes\",\n# like the Ubuntu backports archive since squeeze-backports.\n#\n# priority 500\n# versions that do not belong to the target release.\n#\n# priority 990\n# versions that belong to the target release.\n#\n## interpretation of priority (P)\n# P >= 1000\n# causes a version to be installed even if this constitutes a downgrade of the package\n#\n# 990 <= P < 1000\n# causes a version to be installed even if it does not come from the target release, unless the installed version is more recent\n#\n# 500 <= P < 990\n# causes a version to be installed unless there is a version available belonging to the target release or the installed version is more recent\n#\n# 100 <= P < 500\n# causes a version to be installed unless there is a version available belonging to some other distribution or the installed version is more recent\n#\n# 0 < P < 100\n# causes a version to be installed only if there is no installed version of the package\n#\n# P < 0\n# prevents the version from being installed\n#\n# P = 0\n# has undefined behaviour (do not use it)\n#\n\n#-------------------------------------------------------------------\n# priority for ubuntu 22.04 (jammy) release packages\n#-------------------------------------------------------------------\nPackage: *\nPin: release o=Ubuntu,n=jammy\nPin-Priority: 990\n\nPackage: *\nPin: release o=Ubuntu,a=jammy\nPin-Priority: 990\n"
  },
  {
    "path": "resources/apt/ubuntu/24.04/official-releases.list",
    "content": "#-------------------------------------------------------------------\n# packages from ubuntu 24.04 (noble) release\n#-------------------------------------------------------------------\ndeb https://nyc.mirrors.clouvider.net/ubuntu/ noble main restricted universe multiverse\n#deb-src https://nyc.mirrors.clouvider.net/ubuntu/ noble main restricted universe multiverse\n\ndeb https://nyc.mirrors.clouvider.net/ubuntu/ noble-security main restricted universe multiverse\n#deb-src https://nyc.mirrors.clouvider.net/ubuntu/ noble-security main restricted universe multiverse\n\ndeb https://nyc.mirrors.clouvider.net/ubuntu/ noble-updates main restricted universe multiverse\n#deb-src https://nyc.mirrors.clouvider.net/ubuntu/ noble-updates main restricted universe multiverse\n\ndeb https://nyc.mirrors.clouvider.net/ubuntu/ noble-backports main restricted universe multiverse\n#deb-src https://nyc.mirrors.clouvider.net/ubuntu/ noble-backports main restricted universe multiverse\n\n#deb https://nyc.mirrors.clouvider.net/ubuntu/ noble-proposed main restricted universe multiverse\n#deb-src https://nyc.mirrors.clouvider.net/ubuntu/ noble-proposed main restricted universe multiverse\n"
  },
  {
    "path": "resources/apt/ubuntu/24.04/official-releases.pref",
    "content": "## default priory assignments\n# priority 1\n# versions coming from archives which in their Release files are marked as \"NotAutomatic: yes\",\n# but not as \"ButAutomaticUpgrades: yes\" like the Ubuntu experimental archive.\n#\n# priority 100\n# a version that is already installed (if any) and to the versions coming from archives which,\n# in their Release files are marked as \"NotAutomatic: yes\" and \"ButAutomaticUpgrades: yes\",\n# like the Ubuntu backports archive since squeeze-backports.\n#\n# priority 500\n# versions that do not belong to the target release.\n#\n# priority 990\n# versions that belong to the target release.\n#\n## interpretation of priority (P)\n# P >= 1000\n# causes a version to be installed even if this constitutes a downgrade of the package\n#\n# 990 <= P < 1000\n# causes a version to be installed even if it does not come from the target release, unless the installed version is more recent\n#\n# 500 <= P < 990\n# causes a version to be installed unless there is a version available belonging to the target release or the installed version is more recent\n#\n# 100 <= P < 500\n# causes a version to be installed unless there is a version available belonging to some other distribution or the installed version is more recent\n#\n# 0 < P < 100\n# causes a version to be installed only if there is no installed version of the package\n#\n# P < 0\n# prevents the version from being installed\n#\n# P = 0\n# has undefined behaviour (do not use it)\n#\n\n#-------------------------------------------------------------------\n# priority for ubuntu 24.04 (noble) release packages\n#-------------------------------------------------------------------\nPackage: *\nPin: release o=Ubuntu,n=noble\nPin-Priority: 990\n\nPackage: *\nPin: release o=Ubuntu,a=noble\nPin-Priority: 990\n"
  },
  {
    "path": "resources/git/check_syntax.py",
    "content": "#!/usr/bin/env python3\n\nimport os, sys, re, subprocess, shutil\n\n\n# TODO: add support for other basic preprocessor checks (c/kamcfg)\n# TODO: add support for missing semi-colon / dangling curly brace (c/kamcfg)\n# TODO: add support for recursing through kamcfg include files (kamcfg)\n\n\n# global config variables\nproject_root = subprocess.Popen(['git', 'rev-parse', '--show-toplevel'], universal_newlines=True,\n                                    stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).communicate()[0].strip()\nif len(project_root) == 0:\n    project_root = os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0])))\n\n# find C src files in project\nmatched_csrc_files = subprocess.Popen(['find', project_root, '-type', 'f', '-regextype', 'posix-extended', '-regex', '.*\\.(cpp|hpp|c|h)$'],\n                                    universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).communicate()[0].strip().split()\n# find kamailio .cfg files in project\nshell_pipe = subprocess.Popen(['find', project_root, '-type', 'f', '-name', '*.cfg', '-print0'],\n                                    stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout\nmatched_kamcfg_files = subprocess.Popen(['xargs', '-0', 'sh', '-c', 'for arg do sed -n \"/^\\#\\!KAMAILIO/q 0;q 1\" ${arg} && echo \"${arg}\"; done', '_'],\n                                    universal_newlines=True, stdin=shell_pipe, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL\n                                    ).communicate()[0].strip().split()\nfiles_found = len(matched_csrc_files) + len(matched_kamcfg_files)\n\nterm_width = shutil.get_terminal_size((80, 24))[0]\n\n# global constants\nCSRC_STYLE_IFDEF_REGEX = re.compile(rb'^[ \\t]*#(?:ifdef|ifndef).*')\nCSRC_STYLE_ELSE_REGEX = re.compile(rb'^[ \\t]*#else.*')\nCSRC_STYLE_ENDIF_REGEX = re.compile(rb'^[ \\t]*#endif.*')\nCSRC_CURLYBRACE_OPEN_REGEX = re.compile(rb'^[ \\t]*(?!//|/\\*).*\\{[ \\t]*')\nCSRC_CURLYBRACE_CLOSE_REGEX = re.compile(rb'^[ \\t]*(?!//|/\\*)\\}[ \\t]*')\nKAMCFG_STYLE_IFDEF_REGEX = re.compile(rb'^[ \\t]*#\\!(?:ifdef|ifndef).*')\nKAMCFG_STYLE_ELSE_REGEX = re.compile(rb'^[ \\t]*#\\!else.*')\nKAMCFG_STYLE_ENDIF_REGEX = re.compile(rb'^[ \\t]*#\\!endif.*')\nKAMCFG_CURLYBRACE_OPEN_REGEX = re.compile(rb'^[ \\t]*(?!//|#|/\\*).*\\{[ \\t]*')\nKAMCFG_CURLYBRACE_CLOSE_REGEX = re.compile(rb'^[ \\t]*(?!//|#|/\\*)\\}[ \\t]*')\n\n# holds state for entire test\ntest_succeeded = True\nfiles_checked = 0\n\n# holds state for current file check\nunmatched_ifdefs = []\nunmatched_elses = []\noutoforder_elses = []\nunmatched_endifs = []\nunmatched_lcurly_braces = []\nunmatched_rcurly_braces = []\n\n# check for common syntax errors, currently supported checks:\n#   + preprocessor statement closure\ndef haveValidSyntax(test_files, syntax='c-src'):\n    global files_checked\n    global unmatched_ifdefs, unmatched_elses, outoforder_elses, unmatched_endifs\n    global unmatched_lcurly_braces, unmatched_rcurly_braces\n\n    if syntax == 'c-src':\n        ifdef_regex = CSRC_STYLE_IFDEF_REGEX\n        else_regex = CSRC_STYLE_ELSE_REGEX\n        endif_regex = CSRC_STYLE_ENDIF_REGEX\n        lcurly_regex = CSRC_CURLYBRACE_OPEN_REGEX\n        rcurly_regex = CSRC_CURLYBRACE_CLOSE_REGEX\n    elif syntax == 'kam-cfg':\n        ifdef_regex = KAMCFG_STYLE_IFDEF_REGEX\n        else_regex = KAMCFG_STYLE_ELSE_REGEX\n        endif_regex = KAMCFG_STYLE_ENDIF_REGEX\n        lcurly_regex = KAMCFG_CURLYBRACE_OPEN_REGEX\n        rcurly_regex = KAMCFG_CURLYBRACE_CLOSE_REGEX\n    else:\n        return False\n\n    for test_file in test_files:\n        with open(test_file, 'rb') as fp:\n            i = 1\n            for line in fp:\n                if ifdef_regex.match(line):\n                    unmatched_ifdefs.append([test_file,i,line])\n                elif else_regex.match(line):\n                    if len(unmatched_ifdefs) == 0:\n                        outoforder_elses.append([test_file,i,line])\n                    else:\n                        unmatched_elses.append([test_file,i,line])\n                elif endif_regex.match(line):\n                    try:\n                        unmatched_elses.pop()\n                    except IndexError:\n                        pass\n                    try:\n                        unmatched_ifdefs.pop()\n                    except IndexError:\n                        unmatched_endifs.append([test_file,i,line])\n                elif lcurly_regex.match(line):\n                    unmatched_lcurly_braces.append([test_file,i,line])\n                elif rcurly_regex.match(line):\n                    unmatched_rcurly_braces.append([test_file,i,line])\n                i += 1\n\n        files_checked += 1\n\n        if len(unmatched_ifdefs) + len(outoforder_elses) + len(unmatched_elses) + len(unmatched_endifs) + \\\n        len(unmatched_lcurly_braces) + len(unmatched_rcurly_braces) != 0:\n            return False\n\n    return True\n\n# print summary of test results\ndef printSummary():\n    print('|', '='*(term_width-2), '|', sep='')\n    if test_succeeded:\n        print('Test Result: PASSED')\n    else:\n        print('Test Result: FAILED')\n    print('Number Of Files Tested: {}'.format(str(files_checked)))\n    print('Number Of Files Matched: {}'.format(str(files_found)))\n    print('|', '='*(term_width-2), '|', sep='')\n\n# print error results for a single test\ndef printErrorBlock(header, test_results):\n    header_len = len(header)\n    avail_space = term_width - 4 - header_len\n    header_fill = '=' * (int(avail_space / 2))\n    header_pad = '=' * (avail_space % 2)\n    print('|', header_fill, ' ' + header + ' ', header_fill, header_pad, '|', sep='')\n    for result in test_results:\n        print('[{}] line {}: {}'.format(result[0], str(result[1]), result[2]), file=sys.stderr)\n    print('|', '=' * (term_width - 2), '|', sep='', file=sys.stderr)\n\n# print detailed failure info\ndef printErrorInfo():\n    if len(unmatched_ifdefs) != 0:\n        printErrorBlock('unmatched preprocessor ifdef statements', unmatched_ifdefs)\n    if len(outoforder_elses) != 0:\n        printErrorBlock('out of order preprocessor else statements', outoforder_elses)\n    if len(unmatched_elses) != 0:\n        printErrorBlock('unmatched preprocessor else statements', unmatched_elses)\n    if len(unmatched_endifs) != 0:\n        printErrorBlock('unmatched preprocessor endif statements', unmatched_endifs)\n    if len(unmatched_lcurly_braces) != 0:\n        printErrorBlock('unmatched left curly braces', unmatched_lcurly_braces)\n    if len(unmatched_rcurly_braces) != 0:\n        printErrorBlock('unmatched right curly braces', unmatched_rcurly_braces)\n\n# wrapper for the final cleanup\ndef printResultsAndExit():\n    printSummary()\n    if not test_succeeded:\n        printErrorInfo()\n    sys.exit(int(test_succeeded == False))\n\n# main testing logic\nif __name__ == \"__main__\":\n    if not haveValidSyntax(matched_csrc_files, syntax='c-src'):\n        test_succeeded = False\n    elif not haveValidSyntax(matched_kamcfg_files, syntax='kam-cfg'):\n        test_succeeded = False\n    printResultsAndExit()\n\n"
  },
  {
    "path": "resources/git/commit-msg",
    "content": "%s\n\nCommit:         %h\nTree:           %t\nAuthor:         %an\nEmail:          %ae\nDate:           %aD\n  \n%b\n\n%N\n\n"
  },
  {
    "path": "resources/git/gitattributes",
    "content": "CHANGELOG.md merge=merge-changelog\nCONTRIBUTORS.md merge=merge-contributors\n*requirements.txt merge=merge-requirements"
  },
  {
    "path": "resources/git/gitconfig",
    "content": "[merge \"merge-changelog\"]\n    name = A custom merge driver used to resolve CHANGELOG.md conflicts\n    driver = _merge-changelog %O %A %B %L %P\n[merge \"merge-contributors\"]\n    name = A custom merge driver used to resolve CONTRIBUTORS.md conflicts\n    driver = true\n[merge \"merge-requirements\"]\n    name = A custom merge driver used to resolve requirements.txt conflicts\n    driver = true"
  },
  {
    "path": "resources/git/gitignore",
    "content": "#============================================================\n# General Ignores\n*~\n.#*\n*.\n*.slo\n*.mk\n*.mem\n*.gcda\n*.gcno\n*.la\n*.lo\n*.loT\n*.o\n*.a\n*.ncb\n*.opt\n*.plg\n*swp\n*.patch\n*.tgz\n*.tar.gz\n*.tar.bz2\n*.tar.xz\n*.tar.gz.asc\n*.tar.bz2.asc\n*.tar.xz.asc\n.FBCIndex\n.FBCLockFolder\n.deps\n.libs\ndiff\nlibtool\n#============================================================\n\n#============================================================\n# JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# All .idea config files\n.idea*\n\n# IntelliJ\nout/\n*.iml\n*.iws\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editorconfig files\n*.editorconfig\n#============================================================\n\n#============================================================\n# Visual Studio\n.vs*\n#============================================================\n\n#============================================================\n# CMake\n\ncmake-build-*/\nCMakeLists.txt.user\nCMakeCache.txt\nCMakeFiles\nCMakeScripts\ncmake_install.cmake\ninstall_manifest.txt\ncompile_commands.json\nCTestTestfile.cmake\n_deps\n\n#============================================================\n\n# Custom Git Hook config files\n.commit\n.fetch\n.changelog_commited\n.postcommit_disabled\n.push_remote\n\n# Local repo git configs\n.git*\n\n# Docker configs\n.dockerignore\n\n# C extensions\n*.so\n\n#============================================================\n# Python\n\n# Byte-compiled / optimized / DLL files\n**/__pycache__/\n*.py[cod]\n*$py.class\n*.cpython-35.pyc\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# pyenv\n.python-version\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# Django stuff:\n*.log\n.static_storage/\n.media/\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# IPython\nprofile_default/\nipython_config.py\n#============================================================\n\n#============================================================\n# Terraform / Packer\n\n# Local .terraform directories\n**/.terraform/*\n\n# .tfstate files\n*.tfstate\n*.tfstate.*\n\n# Crash log files\ncrash.log\n\n# Ignore override files as they are usually used to override resources locally\noverride.tf\noverride.tf.json\n*_override.tf\n*_override.tf.json\n\n# Cache objects\npacker_cache/\n\n# For built boxes\n*.box\n#============================================================\n\n"
  },
  {
    "path": "resources/git/gitwrapper.sh",
    "content": "#!/usr/bin/env bash\n# git wrapper: should be sourced by a login script\n\n# extends git command functionality\n__gitwrapper() {\n    local ARGS=() COMMIT_FLAG=0 REMOTE_NAME=\"\"\n\n    while (( $# > 0 )); do\n        case \"$1\" in\n            commit)\n                ARGS+=(\"$1\")\n                COMMIT_FLAG=1\n                shift\n                ;;\n            --remote=*)\n                if (( ${COMMIT_FLAG} == 1 )); then\n                    REMOTE_NAME=$(printf '%s' \"$1\" | cut -d '=' -f 2-)\n                else\n                    ARGS+=(\"$1\")\n                fi\n                shift\n                ;;\n            *)\n                ARGS+=(\"$1\")\n                shift\n                ;;\n        esac\n    done\n\n    # if default remote used we have to lookup the remote name\n    if [[ \"${REMOTE_NAME}\" == \".\" ]]; then\n        REMOTE_NAME=$(git config --get checkout.defaultremote)\n    fi\n    # if using default and not set then use origin\n    REMOTE_NAME=${REMOTE_NAME:-origin}\n\n    export REMOTE_NAME\n    command git \"${ARGS[@]}\"\n    unset REMOTE_NAME\n}\n\n# Shadows git cmd\ngit() {\n   __gitwrapper \"$@\"\n}\n"
  },
  {
    "path": "resources/git/hooks/commit-msg",
    "content": "#!/usr/bin/env bash\n#\n# Summary:  Reformat proposed commit message\n# Author:   DevOpSec <https://github.com/devopsec>\n# Usage:    Copy to your repo in <repo>/.git/hooks/commit-msg\n# Notes:    Adding this hook to your workflow will fix crosslinking issues\n#           caused when referencing issues in one remotee and pushing to another\n#           This hook runs after prepare-commit-msg and before post-commit\n# TODO:     Support resolving gitlab url's for merges/issues/commits\n#\n\n\n# unshadow git command\nunset -f git\n# project root\nPROJECT_ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null)\"\n# indicator that we commited the changelog\nCHANGELOG_INDICATOR_FILE=\"${PROJECT_ROOT}/.changelog_commited\"\n# indicator that post commit hooks has been disabled\nDISABLED_INDICATOR_FILE=\"${PROJECT_ROOT}/.postcommit_disabled\"\n# should be exported by git wrapper script\nREMOTE_NAME=\"${REMOTE_NAME:-origin}\"\n# remote info used in script\nREMOTE_URL=$(git remote get-url ${REMOTE_NAME} 2>/dev/null | perl -pe 's%(?:git\\@|https\\://)(.+)(?:\\:|/)(.+)/(.+)\\.git%https://\\1/\\2/\\3%')\nREMOTE_SITE=$(cut -d '/' -f -3 <<<\"${REMOTE_URL}\")\nREMOTE_USER=$(cut -d '/' -f 4 <<<\"${REMOTE_URL}\")\nREMOTE_REPO=$(cut -d '/' -f 5- <<<\"${REMOTE_URL}\")\n# passed in when hook is called\nCOMMIT_MSG_FILE=\"$1\"\n\n\n# utility functions\nisRemoteGitlab() {\n    curl -L ${REMOTE_URL} 2>/dev/null | grep -q -ioP '<title>.*?gitlab</title>'\n    return $?\n}\n\n# reformat commit msg to follow our committing rules\nreformatCommitMsg() {\n    if isRemoteGitlab; then\n        # replace all issues/merges/commits with full path\n        perl -e \"\\$rs='${REMOTE_SITE}'; \\$ru='${REMOTE_USER}'; \\$rr='${REMOTE_REPO}';\" \\\n            -pe 's%(?:(?!#)([ \\t]+)(?:\\!|${rr}\\!|${ru}/${rr}\\!)|^(?:\\!|${rr}\\!|${ru}/${rr}\\!))(\\d+)%\\1${rs}/${ru}/${rr}/merge_requests/\\2%gm;\n            s%(?:(?!#)([ \\t]+)(?:#|${rr}#|${ru}/${rr}#)|^(?:#|${rr}#|${ru}/${rr}#))(\\d+)%\\1${rs}/${ru}/${rr}/issues/\\2%gm;\n            s%(?!#)(?:${rr}@|${ru}/${rr}@|${rs}/${ru}/${rr}/commit/)?([0-9a-f]{7,40})(?!\\w)%${rs}/${ru}/${rr}/commit/\\1%gm' \\\n            ${COMMIT_MSG_FILE} |\n        # try to fix one-liners\n        perl -e '$str=do{ local $/; <STDIN> }; if ( $str =~ m%^\\w.*?(?:\\n|\\r\\n)(?:\\n|\\r\\n)+$% )\n            { @lines=split(/, |\\. |- /, $str); foreach(@lines) { print \"$_\\n\"; } } else { print \"$str\"; }' |\n        # fix missing subject / missing double newline after subject\n        perl -0777 -pe 's%^((:[ \\t]+|- ?|[ \\t]+|- ?).*?[\\r\\n]+.*)%Updates Listed Below\\n\\n\\1%s; s%^(\\w.*?(?:\\n|\\r\\n)(?!\\n|\\r\\n))(.*)%\\1\\n\\2%s' \\\n            > ${COMMIT_MSG_FILE}.tmp\n        mv -f ${COMMIT_MSG_FILE}.tmp ${COMMIT_MSG_FILE}\n    else\n        # replace all issues/pulls/commits with full path\n        perl -e \"\\$rn='${REMOTE_NAME}'; \\$rs='${REMOTE_SITE}'; \\$ru='${REMOTE_USER}'; \\$rr='${REMOTE_REPO}';\" \\\n            -e 'my $tmp=`git ls-remote $rn \"pull/*head\" 2>/dev/null`; @pulls=($tmp =~ m|.*/pull/(\\d+)/.*|g); $pr_expr=join \"|\", @pulls;' \\\n            -pe 's%(?:(?!#)([ \\t]+)(?:#|GH-|${ru}/${rr}#)|^(?:GH-|${ru}/${rr}#))($pr_expr)(?!\\d)%\\1${rs}/${ru}/${rr}/pull/\\2%gm;\n            s%(?:(?!#)([ \\t]+)(?:#|GH-|${ru}/${rr}#)|^(?:GH-|${ru}/${rr}#))(\\d+)%\\1${rs}/${ru}/${rr}/issues/\\2%gm;\n            s%(?!#)(?:${ru}@|${ru}/${rr}@|${rs}/${ru}/${rr}/commit/)?([0-9a-f]{7,40})(?!\\w)%${rs}/${ru}/${rr}/commit/\\1%gm' \\\n            ${COMMIT_MSG_FILE} |\n        # try to fix one-liners\n        perl -e '$str=do{ local $/; <STDIN> }; if ( $str =~ m%^\\w.*?(?:\\n|\\r\\n)(?:\\n|\\r\\n)+$% )\n            { @lines=split(/, |\\. |- /, $str); foreach(@lines) { print \"$_\\n\"; } } else { print \"$str\"; }' |\n        # fix missing subject / missing double newline after subject\n        perl -0777 -pe 's%^((:[ \\t]+|- ?|[ \\t]+|- ?).*?[\\r\\n]+.*)%Updates Listed Below\\n\\n\\1%s; s%^(\\w.*?(?:\\n|\\r\\n)(?!\\n|\\r\\n))(.*)%\\1\\n\\2%s' \\\n            > ${COMMIT_MSG_FILE}.tmp\n        mv -f ${COMMIT_MSG_FILE}.tmp ${COMMIT_MSG_FILE}\n    fi\n}\n\n# don't run if hook is disabled\n# this is primarily for conflict resolution\nif [[ -e ${DISABLED_INDICATOR_FILE} ]]; then\n    exit 0\n# prevent recursion\n# we only reformat commit msg before changlog is commited\nelif [[ ! -e ${CHANGELOG_INDICATOR_FILE} ]]; then\n    reformatCommitMsg\nfi\n\nexit 0\n"
  },
  {
    "path": "resources/git/hooks/post-commit",
    "content": "#!/usr/bin/env bash\n#\n# Summary:  Create a changelog on commit\n# Author:   DevOpSec <https://github.com/devopsec>\n# Notes:    Original idea from Martin Seeler <https://github.com/MartinSeeler>\n# Usage:    Copy to your repo in <repo>/.git/hooks/post-commit\n#           Alternatively the functions can be run outside of git (using -exec option)\n# Notes:    Syntax for markdown comment: [//]: # (...)\n#           All sections in the changelog are tagged within markdown comments\n# TODO:     Support resolving gitlab url's for merges/issues/commits\n#\n\n\n# unshadow git command\nunset -f git\n# project root\nPROJECT_ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null)\"\n# destination changelog file\nCHANGELOG_FILE=\"CHANGELOG.md\"\n# where we will be creating our changes\nTMP_CHANGELOG_FILE=\"/tmp/CHANGELOG.md\"\n# title of our changelog\nCHANGELOG_TITLE=\"CHANGELOG\"\n# indicator that we commited the changelog\nCHANGELOG_INDICATOR_FILE=\"${PROJECT_ROOT}/.changelog_commited\"\n# indicator that post commit hooks have been disabled\nDISABLED_INDICATOR_FILE=\"${PROJECT_ROOT}/.postcommit_disabled\"\n# should be exported by git wrapper script\nREMOTE_NAME=\"${REMOTE_NAME:-origin}\"\n# file that will hold remote that must be pushed to\nREMOTE_FILE=\"${PROJECT_ROOT}/.push_remote\"\n# remote info used in script\nexport REMOTE_URL=$(git remote get-url ${REMOTE_NAME} 2>/dev/null | perl -pe 's%(?:git\\@|https\\://).+?@(.+)(?:\\:|/)(.+)/(.+)\\.git%https://\\1/\\2/\\3%')\nexport REMOTE_SITE=$(cut -d '/' -f -3 <<<\"${REMOTE_URL}\")\nexport REMOTE_USER=$(cut -d '/' -f 4 <<<\"${REMOTE_URL}\")\nexport REMOTE_REPO=$(cut -d '/' -f 5- <<<\"${REMOTE_URL}\")\n\n\n# utility functions\nisRemoteGitlab() {\n    curl -L ${REMOTE_URL} 2>/dev/null | grep -q -ioP '<title>.*?gitlab</title>'\n    return $?\n}\n\ncreateMDRenderedSection() {\n    local NAME=\"$1\"\n    local CONTENT=\"$2\"\n\n    printf '%s\\n' \"[//]: # (START_SECTION ${NAME})\"\n    printf '%s' \"$CONTENT\" | sed -e '$a\\'\n    printf '%s\\n' \"[//]: # (END_SECTION ${NAME})\"\n}\nexport -f createMDRenderedSection\n\ncreateMDCommentSection() {\n    local NAME=\"$1\"\n    local CONTENT=\"$2\"\n\n    printf '%s\\n' \"[//]: # (START_SECTION ${NAME}\"\n    printf '%s' \"$CONTENT\" | sed -e '$a\\'\n    printf '%s\\n' \"END_SECTION ${NAME})\"\n}\nexport -f createMDCommentSection\n\nremoveMDRenderedSection() {\n    local NAME=\"$1\"\n\n    perl -e \"\\$name='${NAME}';\" -0777 \\\n        -pe 's%\\[//\\]: # \\(START_SECTION ${name}\\)\\n(.*?)\\[//\\]: # \\(END_SECTION ${name}\\)\\n%%s'\n}\nexport -f removeMDRenderedSection\n\nremoveMDRenderedSections() {\n    local NAMES=$(printf '%s' \"$1\" | tr '\\n' '|')\n\n    perl -e \"\\$names='${NAMES}';\" -0777 \\\n        -pe 's%\\[//\\]: # \\(START_SECTION (?:${names})\\)\\n(.*?)\\[//\\]: # \\(END_SECTION (?:${names})\\)\\n%%sg'\n}\nexport -f removeMDRenderedSections\n\nremoveMDCommentSection() {\n    local NAME=\"$1\"\n\n    perl -e \"\\$name='${NAME}';\" -0777 \\\n        -pe 's%\\[//\\]: # \\(START_SECTION ${name}\\n(.*?)END_SECTION ${name}\\)\\n%%s'\n}\nexport -f removeMDCommentSection\n\ngetMDRenderedSection() {\n    local NAME=\"$1\"\n\n    perl -e \"\\$name='${NAME}';\" -0777 \\\n        -e ' while (<>) { if (s%.*\\[//\\]: # \\(START_SECTION ${name}\\)\\n(.*?)\\[//\\]: # \\(END_SECTION ${name}\\)\\n.*%\\1%s) { print; } }'\n}\nexport -f getMDRenderedSection\n\ngetMDCommentSection() {\n    local NAME=\"$1\"\n\n    perl -e \"\\$name='${NAME}';\" -0777 \\\n        -e ' while (<>) { if (s%.*\\[//\\]: # \\(START_SECTION ${name}\\n(.*?)END_SECTION ${name}\\)\\n.*%\\1%s) { print; } }'\n}\nexport -f getMDCommentSection\n\n# create a formatted commit section\ncreateCommitSection() {\n    local HASH=\"$1\" CONTENT=\"\" OIFS=$IFS\n    local PULL_OR_MERGE=${PULL_OR_MERGE:-pull}\n\n    IFS= read -rd '' CONTENT < <(\n        # format commit header\n        git --no-pager log -n 1 --format='### %s%n%N%n%N> Commit: %H  %n%N> Date: %aD  %n%N> Author: %aN (%aE)  %n%N> Committer: %cN (%cE)  %n%N> Signed: %GS  %n%N%n%N' ${HASH} 2>/dev/null |\n        perl -e \"\\$rs='${REMOTE_SITE}'; \\$ru='${REMOTE_USER}'; \\$rr='${REMOTE_REPO}';\" \\\n            -pe 's%([0-9a-f]{40})(?!\\w)%[\\1](${rs}/${ru}/${rr}/commit/\\1)%m'\n        # format commit body\n        git log --format='%b' -n 1 ${HASH} 2>/dev/null |\n        # format issues/pulls/commits -> links\n        # delete empty lines, lines -> bullets\n        perl -e \"\\$rs='${REMOTE_SITE}'; \\$ru='${REMOTE_USER}'; \\$rr='${REMOTE_REPO}';\" \\\n            -pe 's%(${rs}/${ru}/${rr}/'\"$PULL_OR_MERGE\"'/)(\\d+)%[#\\2](\\1\\2)%gm;\n            s%(${rs}/${ru}/${rr}/issues/)(\\d+)%[#\\2](\\1\\2)%gm;\n            s%(${rs}/${ru}/${rr}/commit/)([0-9a-f]{7})([0-9a-f]{0,33})(?!\\w)%[\\2](\\1\\2\\3)%gm;\n            s%^\\s+?$%%; s%^(?:[ \\t]+(?:- )?)?([^\\s])%- \\1%gm;'\n        # spacing between commit messages\n        printf '\\n\\n%s\\n\\n' '---'\n    )\n    createMDRenderedSection \"$HASH\" \"$CONTENT\"\n    IFS=$OIFS\n}\nexport -f createCommitSection\n\n# diff of newline delimeted string arrays (A - B)\nstrarrDiff() {\n    local SKIP A=\"$1\" B=\"$2\"\n\n    for i in $A; do\n        SKIP=\n        for j in $B; do\n            [[ \"$i\" == \"$j\" ]] && { SKIP=1; break; }\n        done\n        [[ -n \"$SKIP\" ]] || printf '%s\\n' \"$i\"\n    done\n}\n\n# create a changelog file\ncreateChangelog() {\n    local HEADER NEW_COMMIT_HASHES OLD_COMMIT_HASHES COMMITS_ADDED COMMITS_REMOVED\n    local NEW_CHANGELOG=1\n\n    # start at project root\n    cd ${PROJECT_ROOT}\n\n    # if gitlab we use merges if github we use pulls\n    if isRemoteGitlab; then\n        export PULL_OR_MERGE=\"merge_requests\"\n    else\n        export PULL_OR_MERGE=\"pull\"\n    fi\n\n    # if exists and in correct format we update the changelog\n    # otherwise we create new changelog from scratch\n    NEW_COMMIT_HASHES=$(git --no-pager log --no-merges --format='%H')\n    if [[ -e \"$CHANGELOG_FILE\" ]]; then\n        OLD_COMMIT_HASHES=$(getMDCommentSection 'COMMITS')\n        if [[ -n \"$OLD_COMMIT_HASHES\" ]]; then\n            NEW_CHANGELOG=0\n\n            # get commits added / removed from last changelog\n            COMMITS_ADDED=$(strarrDiff \"$NEW_COMMIT_HASHES\" \"$OLD_COMMIT_HASHES\")\n            COMMITS_REMOVED=$(strarrDiff \"$OLD_COMMIT_HASHES\" \"$NEW_COMMIT_HASHES\")\n        fi\n    fi\n\n    # create the new header\n    printf -v HEADER '%s\\n\\n\\n\\n' \"## $CHANGELOG_TITLE\"\n\n    # create changelog from scratch\n    if (( ${NEW_CHANGELOG} == 1 )); then\n        createMDRenderedSection 'HEADER' \"$HEADER\" > ${CHANGELOG_FILE}\n        createMDCommentSection 'COMMITS' \"$NEW_COMMIT_HASHES\" >> ${CHANGELOG_FILE}\n        parallel --env -k -j 0 createCommitSection ::: $NEW_COMMIT_HASHES >>${CHANGELOG_FILE}\n    # create temp changelog and merge with old\n    else\n        createMDRenderedSection 'HEADER' \"$HEADER\" > ${TMP_CHANGELOG_FILE}\n        createMDCommentSection 'COMMITS' \"$NEW_COMMIT_HASHES\" >> ${TMP_CHANGELOG_FILE}\n        parallel --env -k -j 0 createCommitSection ::: $COMMITS_ADDED >>${TMP_CHANGELOG_FILE}\n        (\n            cat ${TMP_CHANGELOG_FILE}\n            cat ${CHANGELOG_FILE} |\n                removeMDRenderedSection 'HEADER' |\n                removeMDCommentSection 'COMMITS' |\n\t\t\t    removeMDRenderedSections \"$COMMITS_REMOVED\"\n        ) > ${CHANGELOG_FILE}\n        rm -f ${TMP_CHANGELOG_FILE}\n    fi\n}\n\nmain() {\n    touch ${CHANGELOG_INDICATOR_FILE}\n    createChangelog\n    git add ${CHANGELOG_FILE} &&\n    git commit --amend -C HEAD --no-verify &&\n    echo \"$REMOTE_NAME\" > ${REMOTE_FILE} &&\n    exit 0 ||\n    exit 1\n}\n\n# allow execution outside of git hook\nif (( $# > 0 )) && [[ \"$1\" == \"-exec\" ]]; then\n    main\nfi\n\n# don't run if hook is disabled\n# this is primarily for conflict resolution\nif [[ -e ${DISABLED_INDICATOR_FILE} ]]; then\n    rm -f ${DISABLED_INDICATOR_FILE}\n    exit 0\n# prevent recursion\n# since a 'commit --amend' will trigger the post-commit script again\n# we have to check if the changelog file has been commited yet\n# if changelog commited this is recursive call, do nothing\nelif [[ -e ${CHANGELOG_INDICATOR_FILE} ]]; then\n    rm -f ${CHANGELOG_INDICATOR_FILE}\n    exit 0\nelse\n    main\nfi\n"
  },
  {
    "path": "resources/git/hooks/pre-commit",
    "content": "#!/usr/bin/env bash\n#\n# Summary:  Create CONTRIBUTORS.md and requirements.txt on commit\n# Author:   DevOpSec <https://github.com/devopsec>\n# Usage:    Copy to your repo in <repo>/.git/hooks/pre-commit\n#           Alternatively the functions can be run outside of git (using -exec option)\n# Notes:    Requires pipreqs -> pip install pipreqs\n#           To add directories to recursive search, change INCLUDE_DIRS (its an array)\n# TODO:     Need to add automated CVE checking for python libs, such as with safety\n#           more info: https://github.com/pyupio/safety\n# TODO:     Need to add python linting / validation checks, here are a couple examples:\n#           https://github.com/pre-commit/pre-commit-hooks\n#           https://econ-project-templates.readthedocs.io/en/stable/pre-commit.html\n#\n\n\n# unshadow git command\nunset -f git\n# project root\nPROJECT_ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null)\"\n# destination file\nCONTRIBUTING_FILE=\"CONTRIBUTORS.md\"\n# indicator that we commited the changelog\nCHANGELOG_INDICATOR_FILE=\"${PROJECT_ROOT}/.changelog_commited\"\n# where are the python projects for this repo?\nINCLUDE_DIRS=(\"${PROJECT_ROOT}/gui\")\n# dirs to exclude from requirements\nEXCLUDE_DIRS=( )\n# destination file\nREQUIREMENTS_FILE=\"requirements.txt\"\n# pipreqs only catches stdlib libraries\nINCLUDE_LIBS=(\n\t'Jinja2==3.0.3' 'Werkzeug==2.0.2' 'itsdangerous==2.0.1' 'Flask~=2.2.0' 'docutils<0.17,>=0.12'\n\t'mysqlclient' 'docker' 'sphinx' 'recommonmark' 'sphinxcontrib-httpdomain' 'sphinx-rtd-theme'\n\t'pem' 'twilio' 'SQLAlchemy~=2.0' 'acme'\n)\n# excludes for conflicting libs\nEXCLUDE_LIBS=(\n\t'Jinja2' 'Werkzeug' 'itsdangerous' 'Flask' 'docker_py' 'pyspark' 'acme.hello' 'MySQL-python'\n\t'SQLAlchemy' 'certbot'\n)\n\n\n# utility function\njoinwith() {\n    local START=\"$1\" IFS=\"$2\" END=\"$3\" ARR=()\n    shift;shift;shift\n\n    for VAR in \"$@\"; do\n        ARR+=(\"${START}${VAR}${END}\")\n    done\n\n    echo \"${ARR[*]}\"\n}\n\n# creates CONTRIBUTORS.md for project\ncreateContributors() {\n    local OUT_FILE=\"${PROJECT_ROOT}/${CONTRIBUTING_FILE}\"\n\n    printf '%s\\n\\n%s\\n\\n' \\\n        \"## Thank you to all contributors for your hard work\" \\\n        \"### Contributors\" > ${OUT_FILE}\n\n    git shortlog -sn HEAD | grep -oP '[\\s\\d]*\\K.*'| awk '{for (i=1; i<=NF; i++) print \"- \" $0}' | sort -u >> ${OUT_FILE}\n\n    git add ${OUT_FILE}\n}\n\n# creates requirements.txt for python projects\n# TODO: reformat so code is more readable\ncreateRequirements() {\n    local OUT_FILE=\"\"\n    local EXCLUDE_ARGS=\"\"\n\n    # sanity check, is this a python project?\n    if (( $(find ${INCLUDE_DIRS[@]} -type f -name \"*.py\" 2>/dev/null | wc -l) == 0 )); then\n        return\n    fi\n\n    if (( ${#EXCLUDE_DIRS[@]} > 0 )); then\n        EXCLUDE_ARGS=\"--ignore $(joinwith '' ',' '' ${EXCLUDE_DIRS[@]})\"\n    fi\n\n    for PYTHON_PROJECT in ${INCLUDE_DIRS[@]}; do\n        OUT_FILE=\"${PYTHON_PROJECT}/${REQUIREMENTS_FILE}\"\n\n        if (( ${#EXCLUDE_LIBS[@]} != 0 )); then\n            LIBS=( ${INCLUDE_LIBS[@]} $(pipreqs --mode no-pin --print ${EXCLUDE_ARGS} ${PYTHON_PROJECT} 2>/dev/null | sed -r '/^\\s*$/d' | grep -E -v $(joinwith '^' '|' '$' ${EXCLUDE_LIBS[@]}); exit ${PIPESTATUS[0]};) )\n        else\n            LIBS=( ${INCLUDE_LIBS[@]} $(pipreqs --mode no-pin --print ${EXCLUDE_ARGS} ${PYTHON_PROJECT} 2>/dev/null | sed -r '/^\\s*$/d'; exit ${PIPESTATUS[0]};) )\n        fi\n\n        # make sure pipreqs didn't fail\n        if (( $? != 0 )); then\n            exit 1\n        fi\n\n        # only create requirements.txt if we found dependencies\n        if (( ${#LIBS[@]} > 0 )); then\n            printf '%s\\n' \"${LIBS[@]}\" | sort -u > ${OUT_FILE}\n            git add ${OUT_FILE}\n        fi\n    done\n}\n\n# prevent some common syntax errors from getting committed\ncheckSyntaxErrors() {\n    if ! _git_check_syntax >/dev/null; then\n        exit 1\n    fi\n}\n\n# make sure merge conflicts are all handled\ncheckMergeConflicts() {\n    if [[ \"$(git diff-index --cached -G '^<<<<<<< HEAD' HEAD)\" != \"\" ]]; then\n        exit 1\n    fi\n}\n\nmain() {\n    # make sure un-staged and un-tracked code is not checked by this script\n    # in the future this will be more important as we add linting\n    BACKUPS_DIR=\"/tmp/$(date +%s)\"\n\n    # reset unstaged files before exiting even if we fail early\n    # if resetting files fail then they will still exist in backup dir\n    resetUnstagedFiles() {\n        find ${BACKUPS_DIR} -type f -print 2>/dev/null |\n            perl -e \"\\$bd='${BACKUPS_DIR}';\" -pe 's%^${bd}/(.*)$%\\1%gm' |\n            xargs sh -c 'for arg do mkdir -p \"'\"${PROJECT_ROOT}\"'/$(dirname ${arg} 2>/dev/null)\"; mv -f \"'\"${BACKUPS_DIR}\"'/${arg}\" \"'\"${PROJECT_ROOT}\"'/${arg}\"; done' _\n    }\n    trap resetUnstagedFiles EXIT\n\n    git ls-files -z -o --exclude-standard |\n        xargs -0 sh -c 'for arg do mkdir -p \"'\"${BACKUPS_DIR}\"'/$(dirname ${arg} 2>/dev/null)\"; mv -f \"${arg}\" \"'\"${BACKUPS_DIR}\"'/${arg}\"; done' _\n    git ls-files -z -m --exclude-standard |\n        xargs -0 sh -c 'for arg do mkdir -p \"'\"${BACKUPS_DIR}\"'/$(dirname ${arg} 2>/dev/null)\"; cp -f \"${arg}\" \"'\"${BACKUPS_DIR}\"'/${arg}\"; done' _\n    git checkout -- ${PROJECT_ROOT}\n\n    checkMergeConflicts\n    checkSyntaxErrors\n    createContributors\n    createRequirements\n\n    exit 0\n}\n\n# allow execution outside of git hook\nif (( $# > 0 )) && [[ \"$1\" == \"-exec\" ]]; then\n    main\nfi\n\n# prevent recursion\nif [[ -e ${CHANGELOG_INDICATOR_FILE} ]]; then\n    exit 0\nelse\n    main\nfi\n"
  },
  {
    "path": "resources/git/hooks/pre-push",
    "content": "#!/usr/bin/env bash\n#\n# Summary:  Allow / Disallow push based on checks\n# Author:   DevOpSec <https://github.com/devopsec>\n# Usage:    Copy to your repo in <repo>/.git/hooks/pre-push\n# Notes:    Adding this hook to your workflow works with the git wrapper\n#           script to verify the remote url's were changed correctly\n#           This hook runs after post-commit\n#\n# Params:   This hook is called with the following parameters:\n#\n# $1 -- Name of the remote to which the push is being done\n# $2 -- URL to which the push is being done\n#\n# If pushing without using a named remote those arguments will be equal\n#\n# Information about the commits which are being pushed is supplied as lines to\n# the standard input in the form:\n#\n#   <local ref> <local sha1> <remote ref> <remote sha1>\n#\n\n\n# project root\nPROJECT_ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null)\"\n# passed in when hook is called\nPUSH_REMOTE=\"$1\"\nPUSH_URL=\"$2\"\n# created by post-commit hook\nREMOTE_FILE=\"${PROJECT_ROOT}/.push_remote\"\n\n\n# if remote file exists we have commits that are not pushed yet\nif [[ -e \"$REMOTE_FILE\" ]]; then\n    REMOTE_NAME=\"$(cat ${REMOTE_FILE})\"\n\n    # if default remote used we have to lookup the remote name\n    if [[ \"${PUSH_REMOTE}\" == \".\" ]]; then\n        PUSH_REMOTE=$(git config --get checkout.defaultremote)\n    fi\n    # if default is not set (likely going to fail) then use origin\n    PUSH_REMOTE=${PUSH_REMOTE:-origin}\n\n    # if user is pushing to a different remote without rewriting url's fail\n    if [[ \"${PUSH_REMOTE}\" != \"${REMOTE_NAME}\" ]]; then\n        echo >&2 \"Can not push to a different remote without rewriting commit msg url's\"\n        echo >&2 \"Recommit with the following cmd:\tgit commit --amend --remote=<new remote>\"\n        exit 1\n    else\n        rm -f ${REMOTE_FILE}\n    fi\nfi\n\nexit 0\n"
  },
  {
    "path": "resources/git/hooks/prepare-commit-msg",
    "content": "#!/usr/bin/env bash\n#\n# Summary:  Allow / Disallow commit hooks dependent on situation\n# Author:   DevOpSec <https://github.com/devopsec>\n# Usage:    Copy to your repo in <repo>/.git/hooks/prepare-commit-msg\n# Notes:    Adding this hook to your workflow can alleviate some merging issues\n#           caused by the automated file creation in the commit hooks\n#           This hook runs after pre-commit and before commit-msg\n#\n\n\n# unshadow git command\nunset -f git\n# project root\nPROJECT_ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null)\"\n# passed in when hook is called\nCOMMIT_MSG_FILE=\"$1\"\nCOMMIT_SOURCE=\"$2\"\nCOMMIT_HASH=\"$3\"\n# indicator that post commit hooks has been disabled\nDISABLED_INDICATOR_FILE=\"${PROJECT_ROOT}/.postcommit_disabled\"\n\n\n# fast-forward pulls/commits/merges\nif git reflog -1 | grep -q 'Fast-forward' 2>/dev/null; then\n    RUN_COMMIT_HOOKS=0\n# git revert\nelif [[ -e \"${PROJECT_ROOT}/.git/REVERT_HEAD\" ]]; then\n    RUN_COMMIT_HOOKS=0\n# git cherry-pick\nelif [[ -e \"${PROJECT_ROOT}/.git/CHERRY_PICK_HEAD\" ]]; then\n    RUN_COMMIT_HOOKS=0\n# squashed commits/merges/rebases\nelif [[ \"$COMMIT_SOURCE\" == \"squash\" || -e \"${PROJECT_ROOT}/.git/SQUASH_MSG\" ]]; then\n    RUN_COMMIT_HOOKS=1\n# git merge (will only run if not an automatic merge)\nelif [[ \"$COMMIT_SOURCE\" == \"merge\" || -e \"${PROJECT_ROOT}/.git/MERGE_MSG\" ]]; then\n    RUN_COMMIT_HOOKS=1\n# git commit\nelif [[ \"$COMMIT_SOURCE\" == \"commit\" ]]; then\n    RUN_COMMIT_HOOKS=1\n# default is allow hooks if executed by git\nelse\n    RUN_COMMIT_HOOKS=1\nfi\n\nif (( $RUN_COMMIT_HOOKS == 0 )); then\n    touch ${DISABLED_INDICATOR_FILE}\nelse\n    rm -f ${DISABLED_INDICATOR_FILE} 2>/dev/null\nfi\n\nexit 0\n"
  },
  {
    "path": "resources/git/merge-changelog.sh",
    "content": "#\n# Summary:  Merge driver for CHANGELOG conflicts\n# Author:   DevOpSec <https://github.com/devopsec>\n# Usage:    Copy gitconfig to <repo>/.git/config and gitattributes to <repo>/.gitattributes\n#\n# $1 == %O - temporary file name for the merge base (origin)\n# $2 == %A - temporary file name for our version (ours)\n# $3 == %B - temporary file name for the other branches version (theirs)\n# $4 == %L - conflict marker length\n# $5 == %P - the original path quoted for the shell\n#\n\n# TODO: make more elegant solution; such as diffing & merging then rewriting changelog\n# possible example (if no other conflicts update changelog):\n#\n#PROJECT_ROOT=\"$(git rev-parse --show-toplevel)\"\n#\n#CONFLICTS=$(git diff --cached --name-only -S '<<<<<<')\n#if ! echo \"$CONFLICTS\" | grep -q -v 'CHANGELOG.md'; then\n#    ${PROJECT_ROOT}/.git/hooks/post-commit\n#fi\n\n# For now just keep our version of CHANGELOG.md and update on next commit\nexit 0\n"
  },
  {
    "path": "resources/logrotate/consul",
    "content": "/var/log/consul*.log  {\n    daily\n    missingok\n    rotate 5\n    compress\n    delaycompress\n    create 640 root root\n    sharedscripts\n    copytruncate\n    postrotate\n        systemctl kill -s HUP --kill-who=main rsyslog 2>/dev/null || true\n    endscript\n}\n"
  },
  {
    "path": "resources/logrotate/dsiprouter",
    "content": "/var/log/dsiprouter*.log  {\n    daily\n    missingok\n    rotate 5\n    compress\n    delaycompress\n    create 640 root root\n    sharedscripts\n    copytruncate\n    postrotate\n        systemctl kill -s HUP --kill-who=main rsyslog 2>/dev/null || true\n    endscript\n}\n"
  },
  {
    "path": "resources/logrotate/kamailio",
    "content": "/var/log/kamailio*.log  {\n    daily\n    missingok\n    rotate 5\n    compress\n    delaycompress\n    create 640 root root\n    sharedscripts\n    postrotate\n        systemctl kill -s HUP --kill-who=main rsyslog 2>/dev/null || true\n    endscript\n}\n"
  },
  {
    "path": "resources/logrotate/rtpengine",
    "content": "/var/log/rtpengine*.log  {\n    daily\n    missingok\n    rotate 5\n    compress\n    delaycompress\n    create 640 root root\n    sharedscripts\n    copytruncate\n    postrotate\n        systemctl kill -s HUP --kill-who=main rsyslog 2>/dev/null || true\n    endscript\n}\n"
  },
  {
    "path": "resources/man/dsiprouter.1",
    "content": ".\\\" Process this file with\n.\\\" groff -man -Tascii dsiprouter.1\n.\\\"\n.TH DSIPROUTER 1 \"SEPTEMBER 2022\" Linux \"User Manuals\"\n\n.SH NAME\ndsiprouter \\- command line toolm for configuring a dSIPRouter platform.\n\n.SH SYNOPSIS\n.B dsiprouter {COMMAND} [OPTIONS]\n\n.SH DESCRIPTION\n.B dSIPRouter allows you to quickly turn Kamailio into an easy to use SIP Service Provider platform.\n\n.SH COMMAND\n.IP install\nInstalls dSIPRouter and related components as specified.\n.IP uninstall\nUninstall dSIPRouter.\n.IP clusterinstall\nInstall dSIPRouter in a cluster of nodes\n.IP upgrade\nUpgrade dSIPRouter version to the latest release.\n.IP start\nStarts dSIPRouter services.\n.IP stop\nStops dSIPRouter services.\n.IP restart\nRestarts dSIPRouter services.\n.IP configurekam\nGenerate new config files for Kamailio based on the dSIPRouter settings.\n.IP configuredsip\nGenerate new config files for dSIPRouter based on the environment variables (don't use unless you have read through dsiprouter.sh).\n.IP configurertp\nGenerate new config files for RTPEngine based on the dSIPRouter settings.\n.IP renewsslcert\nRenew configured letsencrypt SSL certificate.\n.IP configuresslcert\nConfigure a new SSL certificate.\n.IP installmodules\nInstall / uninstall dSIPRouter modules.\n.IP resetpassword\nGenerate new unique credentials for dSIPRouter services.\n.IP setcredentials\nSet credentials for dSIPRouter services manually.\n.IP licensemanager\nManage the dSIPRouter licenses for this system.\n.IP backup\nBackup the dSIPRouter data and settings in a SQL file.\n.IP restore\nRestore the dSIPRouter settings and data from a SQL file.\n.IP version|-v|--version\nShow the currently installed dSIPRouter version.\n.IP help|-h|--help\nShow the dSIPRouter CLI usage.\n\n.SH OPTIONS\n.IP \"install options\"\n-debug\nDebug mode to display install step by step.\n\n-all|--all\nInstall dsiprouter, kamailio, and rtpengine.\n\n-kam|--kamailio\nInstall kamailio.\n\n-dsip|--dsiprouter\nInstall dSIPRouter GUI.\n\n-rtp|--rtpengine\nInstall rtpengine.\n\n-dns|--dnsmasq\nInstall DNSMasq and the dSIPRouter network stack.\n\n-homer <homerhost[:heplifyport]>\nSet the Homer node for dSIPRouter to integrate with.\n\n-db <db conn uri>|--database=<db conn uri>\nSet the database connection uri.\n\n-dsipcid <num>|--dsip-clusterid=<num>\nSet the dsiprouter cluster id.\n\n-dbadmin <[user[:pass]@]dbhost[:port][/dbname]>|--database-admin=<[user[:pass]@]dbhost[:port][/dbname]>\nSet the connection settings for the root/admin database account.\n\n-dsipcsync <num>|--dsip-clustersync=<num>\nEnable or disable cluster synchronization between dSIPRouter nodes.\n\n-dsipkey <32 chars>|--dsip-privkey=<32 chars>\nSet the private key for dSIPRouter.\n\n-with_lcr|--with_lcr=<num>\nEnable or disable least-cost routing module.\n\n-with_dev|--with_dev=<num>\nWhether to install development tools and testing tools.\n\n.IP \"uninstall options\"\n-debug\nDebug mode to display uninstall step by step.\n\n-all|--all\nUninstall entire dSIPRouter platform.\n\n-kam|--kamailio\nUninstall kamailio.\n\n-dsip|--dsiprouter\nUninstall dSIPRouter GUI.\n\n-rtp|--rtpengine\nUninstall rtpengine.\n\n.IP \"clusterinstall options\"\n-debug\nDebug mode to display install step by step.\n\n<[user1[:pass1]@]node1[:port1]> <[user2[:pass2]@]node2[:port2> ... -- [<install options>]\nURIs denoting the connection parameters for the nodes to install dSIPRouter on.\n\n-- [INSTALL OPTIONS]\nAfter the \"--\", supply all the arguments to the \"install\" command as normal to configure the dSIPRouter install.\n\n.IP \"upgrade options\"\n-debug\nDebug mode to display upgrade step by step.\n\n-dsipcid <num>|--dsip-clusterid=<num>\nSet the dsiprouter cluster id.\n\n-rel|--release <release number>\nThe release number to upgrade to.\n\n.IP \"start options\"\n-debug\nDebug mode to display starting step by step.\n\n-all|--all\nStart all services.\n\n-kam|--kamailio\nStart kamailio.\n\n-dsip|--dsiprouter\nStart dsiprouter GUI.\n\n-rtp|--rtpengine\nStart rtpengine.\n\n.IP \"stop options\"\n-debug\nDebug mode to display stopping step by step.\n\n-all|--all\nStop all services.\n\n-kam|--kamailio\nStop kamailio.\n\n-dsip|--dsiprouter\nStop dsiprouter GUI.\n\n-rtp|--rtpengine\nStop rtpengine.\n\n.IP \"restart options\"\n-debug\nDebug mode to display restarting step by step.\n\n-all|--all\nRestart all services.\n\n-kam|--kamailio\nRestart kamailio.\n\n-dsip|--dsiprouter\nRestart dsiprouter.\n\n-rtp|--rtpengine\nRestart rtpengine.\n\n.IP \"configurekam options\"\n-debug\nShow detailed info while configuring kamailio settings.\n\n.IP \"renewsslcert options\"\n-debug\nDebug mode to display renewing ssl certificate step by step.\n\n.IP \"configuresslcert options\"\n-debug\nDebug mode to display configuring ssl certificate step by step.\n\n-f|--force\nRemove previous SSL ceritificates and configs and configure new one.\n\n.IP \"installmodules options\"\n-debug\nDebug mode to display installing modules step by step.\n\n.IP \"resetpassword options\"\n-debug\nDebug mode to display resetting password step by step.\n\n-all|--all\nUsed to reset all passwords.\n\n-dc|--dsip-creds\nUsed to reset dsiprouter gui password.\n\n-ac|--api-creds\nUsed to reset api password.\n\n-kc|--kam-creds\nUsed to reset kamailio password.\n\n-ic|--ipc-creds\nUsed to reset ipc password.\n\n-fid|--force-instance-id\nForce dSIPRouter to use the cloud instance ID as the GUI password.\n\n.IP \"setcredentials options\"\n-debug\nDebug mode to display setting credentials step by step.\n\n-dc <pass>|--dsip-creds=<pass>\nUsed to set dSIPRouter GUI username/password manually.\n\n-ac <token>|--api-creds=<token>\nUsed to set the dSIPRouter API token manually.\n\n-kc <pass>|--kam-creds=<pass>\nUsed to set kamalio username/password/host/port/database name manually.\n\n-mc <pass>|--mail-creds=<pass>\nUsed to set email useername/password manually.\n\n-ic <pass>|--ipc-creds=<pass>\nUsed to set the dSIPRouter IPC token manually.\n\n-dac <[user[:pass]@]dbhost[:port][/dbname]>|--db-admin-creds=<[user[:pass]@]dbhost[:port][/dbname]>\nUpdate the root/admin database connection settings.\n\n-sc <key>|--session-creds=<key>\nUsed to set the key for the flask session manager manually.\n\n.IP \"licensemanager options\"\n-debug\nShow detailed info while running the licensemanager sub-command.\n\n-list\nList all the licenses associated with this machine.\n\n\n-retrieve <license_key or 'tag=<tag>'>\nGet detailed information about the license(s) either by key or by tag.\nTo filter by tag the argument should be passed as \"tag=THE_TAG_TO_FILTER_ON\".\nSimilar filtering is done for options that support filtering by tag below.\n\n-activate <license_key>\nAssociate and activate a license on this system.\n\n-import <file containing keys>\nImport a file contianing license keys.\nThe file should contain only license keys, one per line.\n\n-clear\nRemove all licenses associated with this machine.\nNote that this should be run before decommissioning an dSIPRouter instance.\nIf the licenses are not cleared from the machine before de-provisioning then dOpenSource support staff will have to manually fix them.\n\n-deactivate <license_key or 'tag=<tag>'>\nDissociate and deactivate a license that is currently installed on this system.\n\n-check <license_key or 'tag=<tag>'\nCheck whether the license key(s) is valid and active on this machine.\n\n.IP \"backup options\"\n-debug\nShow detailed info while running the backup sub-command.\n\n-f <sql file>\nSpecify the path to output the backup file.\n\n.IP \"restore options\"\n-debug\nShow detailed info while running the restore sub-command.\n\n-f <sql file>\nSpecify the path to import the backup file from.\n\n.SH BUGS\nReport to Github Issues: https://github.com/dOpensource/dsiprouter.git\n\n.SH AUTHOR\ndOpenSource/dSIPRouter\n"
  },
  {
    "path": "resources/mysql/asterisk-realtime-config.sql",
    "content": "### asterisk realtime db\n\n# configure asterisk server for realtime DB connections/updates\n\n# explanation and how to configure:\n#https://www.voip-info.org/asterisk-realtime/\n\n\n#https://www.voip-info.org/asterisk-realtime-static\n\nCREATE TABLE `bit_ast_config` (\n`id` int(11) NOT NULL auto_increment,\n`cat_metric` int(11) NOT NULL default '0',\n`var_metric` int(11) NOT NULL default '0',\n`commented` int(11) NOT NULL default '0',\n`filename` varchar(128) NOT NULL default '',\n`category` varchar(128) NOT NULL default 'default',\n`var_name` varchar(128) NOT NULL default '',\n`var_val` varchar(128) NOT NULL default '',\nPRIMARY KEY (`id`),\nKEY `filename_comment` (`filename`,`commented`)\n);\n\n\n\n#https://www.voip-info.org/asterisk-realtime-sip\n\nCREATE TABLE `bit_sip_buddies` (\n`id` int(11) NOT NULL auto_increment,\n`name` varchar(80) NOT NULL default '',\n`host` varchar(31) NOT NULL default '',\n`nat` varchar(5) NOT NULL default 'no',\n`type` enum('user','peer','friend') NOT NULL default 'friend',\n`accountcode` varchar(20) default NULL,\n`amaflags` varchar(13) default NULL,\n`call-limit` smallint(5) unsigned default NULL,\n`callgroup` varchar(10) default NULL,\n`callerid` varchar(80) default NULL,\n`cancallforward` char(3) default 'yes',\n`canreinvite` char(3) default 'yes',\n`context` varchar(80) default NULL,\n`defaultip` varchar(15) default NULL,\n`dtmfmode` varchar(7) default NULL,\n`fromuser` varchar(80) default NULL,\n`fromdomain` varchar(80) default NULL,\n`insecure` varchar(4) default NULL,\n`language` char(2) default NULL,\n`mailbox` varchar(50) default NULL,\n`md5secret` varchar(80) default NULL,\n`deny` varchar(95) default NULL,\n`permit` varchar(95) default NULL,\n`mask` varchar(95) default NULL,\n`musiconhold` varchar(100) default NULL,\n`pickupgroup` varchar(10) default NULL,\n`qualify` char(3) default NULL,\n`regexten` varchar(80) default NULL,\n`restrictcid` char(3) default NULL,\n`rtptimeout` char(3) default NULL,\n`rtpholdtimeout` char(3) default NULL,\n`secret` varchar(80) default NULL,\n`setvar` varchar(100) default NULL,\n`disallow` varchar(100) default 'all',\n`allow` varchar(100) default 'g729;ilbc;gsm;ulaw;alaw',\n`fullcontact` varchar(80) NOT NULL default '',\n`ipaddr` varchar(15) NOT NULL default '',\n`port` smallint(5) unsigned NOT NULL default '0',\n`regserver` varchar(100) default NULL,\n`regseconds` int(11) NOT NULL default '0',\n`lastms` int(11) NOT NULL default '0',\n`username` varchar(80) NOT NULL default '',\n`defaultuser` varchar(80) NOT NULL default '',\n`subscribecontext` varchar(80) default NULL,\n`useragent` varchar(20) default NULL,\nPRIMARY KEY (`id`),\nUNIQUE KEY `name` (`name`),\nKEY `name_2` (`name`)\n) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;\n\n## OR ##\n\n#https://wiki.asterisk.org/wiki/display/AST/SIP+Realtime%2C+MySQL+table+structure\n\nCREATE TABLE IF NOT EXISTS `sipfriends` (\n`id` int(11) NOT NULL AUTO_INCREMENT,\n`name` varchar(10) NOT NULL,\n`ipaddr` varchar(15) DEFAULT NULL,\n`port` int(5) DEFAULT NULL,\n`regseconds` int(11) DEFAULT NULL,\n`defaultuser` varchar(10) DEFAULT NULL,\n`fullcontact` varchar(35) DEFAULT NULL,\n`regserver` varchar(20) DEFAULT NULL,\n`useragent` varchar(20) DEFAULT NULL,\n`lastms` int(11) DEFAULT NULL,\n`host` varchar(40) DEFAULT NULL,\n`type` enum('friend','user','peer') DEFAULT NULL,\n`context` varchar(40) DEFAULT NULL,\n`permit` varchar(40) DEFAULT NULL,\n`deny` varchar(40) DEFAULT NULL,\n`secret` varchar(40) DEFAULT NULL,\n`md5secret` varchar(40) DEFAULT NULL,\n`remotesecret` varchar(40) DEFAULT NULL,\n`transport` enum('udp','tcp','udp,tcp','tcp,udp') DEFAULT NULL,\n`dtmfmode` enum('rfc2833','info','shortinfo','inband','auto') DEFAULT NULL,\n`directmedia` enum('yes','no','nonat','update') DEFAULT NULL,\n`nat` enum('yes','no','never','route') DEFAULT NULL,\n`callgroup` varchar(40) DEFAULT NULL,\n`pickupgroup` varchar(40) DEFAULT NULL,\n`language` varchar(40) DEFAULT NULL,\n`allow` varchar(40) DEFAULT NULL,\n`disallow` varchar(40) DEFAULT NULL,\n`insecure` varchar(40) DEFAULT NULL,\n`trustrpid` enum('yes','no') DEFAULT NULL,\n`progressinband` enum('yes','no','never') DEFAULT NULL,\n`promiscredir` enum('yes','no') DEFAULT NULL,\n`useclientcode` enum('yes','no') DEFAULT NULL,\n`accountcode` varchar(40) DEFAULT NULL,\n`setvar` varchar(40) DEFAULT NULL,\n`callerid` varchar(40) DEFAULT NULL,\n`amaflags` varchar(40) DEFAULT NULL,\n`callcounter` enum('yes','no') DEFAULT NULL,\n`busylevel` int(11) DEFAULT NULL,\n`allowoverlap` enum('yes','no') DEFAULT NULL,\n`allowsubscribe` enum('yes','no') DEFAULT NULL,\n`videosupport` enum('yes','no') DEFAULT NULL,\n`maxcallbitrate` int(11) DEFAULT NULL,\n`rfc2833compensate` enum('yes','no') DEFAULT NULL,\n`mailbox` varchar(40) DEFAULT NULL,\n`session-timers` enum('accept','refuse','originate') DEFAULT NULL,\n`session-expires` int(11) DEFAULT NULL,\n`session-minse` int(11) DEFAULT NULL,\n`session-refresher` enum('uac','uas') DEFAULT NULL,\n`t38pt_usertpsource` varchar(40) DEFAULT NULL,\n`regexten` varchar(40) DEFAULT NULL,\n`fromdomain` varchar(40) DEFAULT NULL,\n`fromuser` varchar(40) DEFAULT NULL,\n`qualify` varchar(40) DEFAULT NULL,\n`defaultip` varchar(40) DEFAULT NULL,\n`rtptimeout` int(11) DEFAULT NULL,\n`rtpholdtimeout` int(11) DEFAULT NULL,\n`sendrpid` enum('yes','no') DEFAULT NULL,\n`outboundproxy` varchar(40) DEFAULT NULL,\n`callbackextension` varchar(40) DEFAULT NULL,\n`registertrying` enum('yes','no') DEFAULT NULL,\n`timert1` int(11) DEFAULT NULL,\n`timerb` int(11) DEFAULT NULL,\n`qualifyfreq` int(11) DEFAULT NULL,\n`constantssrc` enum('yes','no') DEFAULT NULL,\n`contactpermit` varchar(40) DEFAULT NULL,\n`contactdeny` varchar(40) DEFAULT NULL,\n`usereqphone` enum('yes','no') DEFAULT NULL,\n`textsupport` enum('yes','no') DEFAULT NULL,\n`faxdetect` enum('yes','no') DEFAULT NULL,\n`buggymwi` enum('yes','no') DEFAULT NULL,\n`auth` varchar(40) DEFAULT NULL,\n`fullname` varchar(40) DEFAULT NULL,\n`trunkname` varchar(40) DEFAULT NULL,\n`cid_number` varchar(40) DEFAULT NULL,\n`callingpres` enum('allowed_not_screened','allowed_passed_screen','allowed_failed_screen','allowed','prohib_not_screened','prohib_passed_screen','prohib_failed_screen','prohib') DEFAULT NULL,\n`mohinterpret` varchar(40) DEFAULT NULL,\n`mohsuggest` varchar(40) DEFAULT NULL,\n`parkinglot` varchar(40) DEFAULT NULL,\n`hasvoicemail` enum('yes','no') DEFAULT NULL,\n`subscribemwi` enum('yes','no') DEFAULT NULL,\n`vmexten` varchar(40) DEFAULT NULL,\n`autoframing` enum('yes','no') DEFAULT NULL,\n`rtpkeepalive` int(11) DEFAULT NULL,\n`call-limit` int(11) DEFAULT NULL,\n`g726nonstandard` enum('yes','no') DEFAULT NULL,\n`ignoresdpversion` enum('yes','no') DEFAULT NULL,\n`allowtransfer` enum('yes','no') DEFAULT NULL,\n`dynamic` enum('yes','no') DEFAULT NULL,\nPRIMARY KEY (`id`),\nUNIQUE KEY `name` (`name`),\nKEY `ipaddr` (`ipaddr`,`port`),\nKEY `host` (`host`,`port`)\n) ENGINE=MyISAM;\n\n\n#https://www.voip-info.org/asterisk-realtime-iax\n\nCREATE TABLE bit_iax_buddies (\nname varchar(30) primary key NOT NULL,\nusername varchar(30),\ntype varchar(6) NOT NULL,\nsecret varchar(50),\nmd5secret varchar(32),\ndbsecret varchar(100),\nnotransfer varchar(10),\ninkeys varchar(100),\noutkey varchar(100),\nauth varchar(100),\naccountcode varchar(100),\namaflags varchar(100),\ncallerid varchar(100),\ncontext varchar(100),\ndefaultip varchar(15),\nhost varchar(31) NOT NULL default 'dynamic',\nlanguage char(5),\nmailbox varchar(50),\ndeny varchar(95),\npermit varchar(95),\nqualify varchar(4),\ndisallow varchar(100),\nallow varchar(100),\nipaddr varchar(15),\nport integer default 0,\nregseconds integer default 0\n);\nCREATE UNIQUE INDEX bit_iax_buddies_username_idx ON bit_iax_buddies(username);\n\n\n\n#https://www.voip-info.org/asterisk-realtime-h323\n\nDROP TABLE IF EXISTS h323_peer;\n\nCREATE TABLE h323_peer(\nid BIGINT AUTO_INCREMENT PRIMARY KEY,\nname VARCHAR(128) NOT NULL UNIQUE,\nhost VARCHAR(15) DEFAULT NULL ,\nsecret VARCHAR(64) DEFAULT NULL,\ncontext VARCHAR(64) NOT NULL,\ntype VARCHAR(6) NOT NULL,\nport INT DEFAULT NULL,\npermit VARCHAR(128) DEFAULT NULL,\ndeny VARCHAR(128) DEFAULT NULL,\nmailbox VARCHAR(128) DEFAULT NULL,\ne164 VARCHAR(128) DEFAULT NULL,\nprefix VARCHAR(128) DEFAULT NULL,\nallow VARCHAR(128) DEFAULT NULL,\ndisallow VARCHAR(128) DEFAULT NULL,\ndtmfmode VARCHAR(128) DEFAULT NULL,\naccountcode INT DEFAULT NULL,\namaflags varchar(13) DEFAULT NULL,\nINDEX idx_name(name),\nINDEX idx_host(host)\n);\n\n\n\n#https://www.voip-info.org/asterisk-realtime-voicemail\n\nCREATE TABLE `bit_voicemail` (\n `uniqueid` INT(4) NOT NULL AUTO_INCREMENT,\n `customer_id` VARCHAR(10) COLLATE utf8_bin DEFAULT NULL,\n `context` VARCHAR(10) COLLATE utf8_bin NOT NULL,\n `mailbox` VARCHAR(10) COLLATE utf8_bin NOT NULL,\n `password` INT(4) NOT NULL,\n `fullname` VARCHAR(150) COLLATE utf8_bin DEFAULT NULL,\n `email` VARCHAR(50) COLLATE utf8_bin DEFAULT NULL,\n `pager` VARCHAR(50) COLLATE utf8_bin DEFAULT NULL,\n `tz` VARCHAR(10) COLLATE utf8_bin DEFAULT 'central',\n `attach` ENUM('yes','no') COLLATE utf8_bin NOT NULL DEFAULT 'yes',\n `saycid` ENUM('yes','no') COLLATE utf8_bin NOT NULL DEFAULT 'yes',\n `dialout` VARCHAR(10) COLLATE utf8_bin DEFAULT NULL,\n `callback` VARCHAR(10) COLLATE utf8_bin DEFAULT NULL,\n `review` ENUM('yes','no') COLLATE utf8_bin NOT NULL DEFAULT 'no',\n `operator` ENUM('yes','no') COLLATE utf8_bin NOT NULL DEFAULT 'no',\n `envelope` ENUM('yes','no') COLLATE utf8_bin NOT NULL DEFAULT 'no',\n `sayduration` ENUM('yes','no') COLLATE utf8_bin NOT NULL DEFAULT 'no',\n `saydurationm` TINYINT(4) NOT NULL DEFAULT '1',\n `sendvoicemail` ENUM('yes','no') COLLATE utf8_bin NOT NULL DEFAULT 'no',\n `delete` ENUM('yes','no') COLLATE utf8_bin NOT NULL DEFAULT 'no',\n `nextaftercmd` ENUM('yes','no') COLLATE utf8_bin NOT NULL DEFAULT 'yes',\n `forcename` ENUM('yes','no') COLLATE utf8_bin NOT NULL DEFAULT 'no',\n `forcegreetings` ENUM('yes','no') COLLATE utf8_bin NOT NULL DEFAULT 'no',\n `hidefromdir` ENUM('yes','no') COLLATE utf8_bin NOT NULL DEFAULT 'yes',\n `stamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n `attachfmt` VARCHAR(10) COLLATE utf8_bin DEFAULT NULL,\n `searchcontexts` ENUM('yes','no') COLLATE utf8_bin DEFAULT NULL,\n `cidinternalcontexts` VARCHAR(10) COLLATE utf8_bin DEFAULT NULL,\n `exitcontext` VARCHAR(10) COLLATE utf8_bin DEFAULT NULL,\n `volgain` VARCHAR(4) COLLATE utf8_bin DEFAULT NULL,\n `tempgreetwarn` ENUM('yes','no') COLLATE utf8_bin DEFAULT 'yes',\n `messagewrap` ENUM('yes','no') COLLATE utf8_bin DEFAULT 'no',\n `minpassword` INT(2) DEFAULT '4',\n `vm-password` VARCHAR(10) COLLATE utf8_bin DEFAULT NULL,\n `vm-newpassword` VARCHAR(10) COLLATE utf8_bin DEFAULT NULL,\n `vm-passchanged` VARCHAR(10) COLLATE utf8_bin DEFAULT NULL,\n `vm-reenterpassword` VARCHAR(10) COLLATE utf8_bin DEFAULT NULL,\n `vm-mismatch` VARCHAR(10) COLLATE utf8_bin DEFAULT NULL,\n `vm-invalid-password` VARCHAR(10) COLLATE utf8_bin DEFAULT NULL,\n `vm-pls-try-again` VARCHAR(10) COLLATE utf8_bin DEFAULT NULL,\n `listen-control-forward-key` VARCHAR(2) COLLATE utf8_bin DEFAULT NULL,\n `listen-control-reverse-key` VARCHAR(1) COLLATE utf8_bin DEFAULT NULL,\n `listen-control-pause-key` VARCHAR(1) COLLATE utf8_bin DEFAULT NULL,\n `listen-control-restart-key` VARCHAR(1) COLLATE utf8_bin DEFAULT NULL,\n `listen-control-stop-key` VARCHAR(13) COLLATE utf8_bin DEFAULT NULL,\n `backupdeleted` VARCHAR(3) COLLATE utf8_bin DEFAULT '25',\n  PRIMARY KEY  (`uniqueid`),\n KEY `mailbox_context` (`mailbox`,`context`)\n) ENGINE=INNODB DEFAULT CHARSET=latin1;\n\n\n\n#https://www.voip-info.org/asterisk-realtime-queue\n\nCREATE TABLE queue_table (\nname VARCHAR(128) PRIMARY KEY,\nmusiconhold VARCHAR(128),\nannounce VARCHAR(128),\ncontext VARCHAR(128),\ntimeout INT(11),\nmonitor_join BOOL,\nmonitor_format VARCHAR(128),\nqueue_youarenext VARCHAR(128),\nqueue_thereare VARCHAR(128),\nqueue_callswaiting VARCHAR(128),\nqueue_holdtime VARCHAR(128),\nqueue_minutes VARCHAR(128),\nqueue_seconds VARCHAR(128),\nqueue_lessthan VARCHAR(128),\nqueue_thankyou VARCHAR(128),\nqueue_reporthold VARCHAR(128),\nannounce_frequency INT(11),\nannounce_round_seconds INT(11),\nannounce_holdtime VARCHAR(128),\nretry INT(11),\nwrapuptime INT(11),\nmaxlen INT(11),\nservicelevel INT(11),\nstrategy VARCHAR(128),\njoinempty VARCHAR(128),\nleavewhenempty VARCHAR(128),\neventmemberstatus BOOL,\neventwhencalled BOOL,\nreportholdtime BOOL,\nmemberdelay INT(11),\nweight INT(11),\ntimeoutrestart BOOL,\nperiodic_announce VARCHAR(50),\nperiodic_announce_frequency INT(11),\nringinuse BOOL,\nsetinterfacevar BOOL\n);\n\n\n\n#https://www.voip-info.org/asterisk-realtime-extensions\n\nCREATE TABLE `bit_extensions_table` (\n`id` int(11) NOT NULL auto_increment,\n`context` varchar(20) NOT NULL default '',\n`exten` varchar(20) NOT NULL default '',\n`priority` tinyint(4) NOT NULL default '0',\n`app` varchar(20) NOT NULL default '',\n`appdata` varchar(128) NOT NULL default '',\nPRIMARY KEY (`context`,`exten`,`priority`),\nKEY `id` (`id`)\n);\n\n\n\n#https://www.voip-info.org/ldap\n\n\n\n#https://www.voip-info.org/asterisk-realtime-meetme\n\nCREATE TABLE `bit_meetme` (\n`confno` varchar(80) DEFAULT '0' NOT NULL,\n`pin` varchar(20) NULL,\n`adminpin` varchar(20) NULL,\n`members` integer DEFAULT 0 NOT NULL,\nPRIMARY KEY (confno)\n);\n\n\n\n#https://www.voip-info.org/asterisk-realtime-chansccp2\n\nCREATE TABLE `sccpdevices` (\n`name` varchar(15) NOT NULL default '',\n`type` varchar(45) default NULL,\n`autologin` varchar(45) default NULL,\n`description` varchar(45) default NULL,\n`tzoffset` varchar(45) default NULL,\n`transfer` varchar(45) default NULL,\n`speeddial` varchar(45) default NULL,\n`cfwdall` varchar(45) default NULL,\n`cfwdbusy` varchar(45) default NULL,\n`dtmfmode` varchar(45) default NULL,\n`imageversion` varchar(45) default NULL,\n`deny` varchar(45) default NULL,\n`permit` varchar(45) default NULL,\n`dnd` varchar(45) default NULL,\nPRIMARY KEY (`name`)\n);\n\nCREATE TABLE `sccplines` (\n`name` varchar(45) NOT NULL default '',\n`id` varchar(45) default NULL,\n`pin` varchar(45) default NULL,\n`label` varchar(45) default NULL,\n`description` varchar(45) default NULL,\n`context` varchar(45) default NULL,\n`incominglimit` varchar(45) default NULL,\n`transfer` varchar(45) default NULL,\n`mailbox` varchar(45) default NULL,\n`vmnum` varchar(45) default NULL,\n`cid_name` varchar(45) default NULL,\n`cid_num` varchar(45) default NULL,\n`trnsfvm` varchar(45) default NULL,\n`secondary_dialtone_digits` varchar(45) default NULL,\n`secondary_dialtone_tone` varchar(45) default NULL,\n`musicclass` varchar(45) default NULL,\n`language` varchar(45) default NULL,\n`accountcode` varchar(45) default NULL,\n`rtptos` varchar(45) default NULL,\n`echocancel` varchar(45) default NULL,\n`silencesuppression` varchar(45) default NULL,\n`callgroup` varchar(45) default NULL,\n`pickupgroup` varchar(45) default NULL,\n`amaflags` varchar(45) default NULL,\nPRIMARY KEY (`name`)\n);\n\n\n\n## another version of realtime db for replication\n\n#http://www.ntegratedsolutions.com/wp-content/uploads/2012/07/Asterisk_MySQL_Cluster_Presentation.pdf\n\nCREATE database asteriskdb;\nCREATE TABLE `extensions` (\n`id` int(11) NOT NULL auto_increment,\n`context` varchar(20) NOT NULL default '',\n`exten` varchar(20) NOT NULL default '',\n`priority` tinyint(4) NOT NULL default '0',\n`app` varchar(20) NOT NULL default '',\n`appdata` varchar(128) NOT NULL default '',\n`accountcode` varchar(20) default NULL,\n`notes` varchar(255) default NULL,\nPRIMARY KEY (`context`,`exten`,`priority`),\nKEY `id` (`id`)\n);\nCREATE TABLE `voicemail` (\n`uniqueid` int(11) NOT NULL auto_increment,\n`customer_id` varchar(11) NOT NULL default '0',\n`context` varchar(50) NOT NULL default '',\n`mailbox` varchar(11) NOT NULL default '0',\n`password` varchar(5) NOT NULL default '0',\n`fullname` varchar(150) NOT NULL default '',\n`email` varchar(50) NOT NULL default '',\n`pager` varchar(50) NOT NULL default '',\n`tz` varchar(10) NOT NULL default 'central',\n`attach` varchar(4) NOT NULL default 'yes',\n`saycid` varchar(4) NOT NULL default 'yes',\n`dialout` varchar(10) NOT NULL default '',\n`callback` varchar(10) NOT NULL default '',\n`review` varchar(4) NOT NULL default 'no',\n`operator` varchar(4) NOT NULL default 'no',\n`envelope` varchar(4) NOT NULL default 'no',\n`sayduration` varchar(4) NOT NULL default 'no',\n`saydurationm` tinyint(4) NOT NULL default '1',\n`sendvoicemail` varchar(4) NOT NULL default 'no',\n`delete` varchar(4) NOT NULL default 'no',\n`nextaftercmd` varchar(4) NOT NULL default 'yes',\n`forcename` varchar(4) NOT NULL default 'no',\n`forcegreetings` varchar(4) NOT NULL default 'no',\n`hidefromdir` varchar(4) NOT NULL default 'yes',\nPRIMARY KEY (`uniqueid`),\nKEY `mailbox_context` (`mailbox`,`context`)\n);\nCREATE TABLE `sip` (\n`id` int(11) NOT NULL auto_increment,\n`name` varchar(80) NOT NULL default '',\n`accountcode` varchar(20) default NULL,\n`amaflags` varchar(13) default NULL,\n`callgroup` varchar(10) default NULL,\n`callerid` varchar(80) default NULL,\n`canreinvite` char(3) default 'yes',\n`context` varchar(80) default NULL,\n`defaultip` varchar(15) default NULL,\n`dtmfmode` varchar(7) default NULL,\n`fromuser` varchar(80) default NULL,\n`fromdomain` varchar(80) default NULL,\n`host` varchar(31) NOT NULL default '',\n`insecure` varchar(4) default NULL,\n`language` char(2) default NULL,\n`mailbox` varchar(50) default NULL,\n`md5secret` varchar(80) default NULL,\n`nat` varchar(5) NOT NULL default 'no',\n`deny` varchar(95) default NULL,\n`permit` varchar(95) default NULL,\n`mask` varchar(95) default NULL,\n`pickupgroup` varchar(10) default NULL,\n`port` varchar(5) NOT NULL default '',\n`qualify` char(3) default NULL,\n`restrictcid` char(1) default NULL,\n`rtptimeout` char(3) default NULL,\n`rtpholdtimeout` char(3) default NULL,\n`secret` varchar(80) default NULL,\n`type` varchar(6) NOT NULL default 'friend',\n`username` varchar(80) NOT NULL default '',\n`disallow` varchar(100) default 'all',\n`allow` varchar(100) default 'gsm;ulaw;alaw',\n`musiconhold` varchar(100) default NULL,\n`regseconds` int(11) NOT NULL default '0',\n`ipaddr` varchar(15) NOT NULL default '',\n`regexten` varchar(80) NOT NULL default '',\n`cancallforward` char(3) default 'yes',\n`setvar` varchar(100) NOT NULL default '',\n`fullcontact` varchar(80) default NULL,\nPRIMARY KEY (`id`),\nUNIQUE KEY `name` (`name`),\nKEY `name_2` (`name`)\n);\nCREATE TABLE `pins` (\n`id` int(11) NOT NULL auto_increment,\n`company` varchar(20) NOT NULL default '',\n`pin` varchar(10) NOT NULL default '',\n`active` varchar(5) NOT NULL default 'no',\n`accountcode` varchar(20) NOT NULL default '',\n`notes` varchar(255) default NULL,\nPRIMARY KEY (`company`,`pin`),\nKEY `id` (`id`)\n);\n\n"
  },
  {
    "path": "resources/mysql/asterisk-realtime-setup.sql",
    "content": "use kamailio;\ninsert into domain values (null,'test.ca','test.ca',NOW());\n/* Most likely we will need to add a setid to this table so that we can load balance between multiple PBX's */\ninsert into dsip_domain_mapping values (null,0,1,'','',1);\n/*  Type 0=integer 2=string */\ninsert into domain_attrs values (null,'test.ca','dispatcher_setid',0,'1',NOW());\ninsert into domain_attrs values (null,'test.ca','dispatcher_inv_alg',0,'4',NOW());\ninsert into domain_attrs values (null,'test.ca','dispatcher_reg_alg',0,'4',NOW());\ninsert into domain_attrs values (null,'test.ca','db_host',2,'realtime-settings.mysql.database.test.ca',NOW());\ninsert into domain_attrs values (null,'test.ca','db_user',2,'vmrealtime@realtime-settings',NOW());\ninsert into domain_attrs values (null,'test.ca','db_pass',2,'SecretPasssword',NOW());\ninsert into domain_attrs values (null,'test.ca','db_name',2,'asterisk',NOW());\ninsert into domain_attrs values (null,'test.ca','db_table',2,'sipusers',NOW());\ninsert into domain_attrs values (null,'test.ca','db_userfield',2,'name',NOW());\ninsert into domain_attrs values (null,'test.ca','db_passfield',2,'secret',NOW());\ninsert into domain_attrs values (null,'test.ca','db_pass_alg',2,'secret',NOW());\n\n\n/* dispatcher with Setid being 1 */\ninsert into dispatcher values (null,1,'sip:208.79.81.99',0,0,'','');\ninsert into dispatcher values (null,1,'sip:192.73.246.131',0,0,'','');\ninsert into dispatcher values (null,1,'sip:162.248.220.121',0,0,'','');\n\n"
  },
  {
    "path": "resources/stir_shaken/generate_self_signed_cert.sh",
    "content": "\n# Install dependencies\n\napt -y install openssl coreutils\n\nTMP_CERT_DIR=/tmp/stir-shaken-ca\nDSIP_CERT_DIR=/etc/dsiprouter/certs/stirshaken\n\n# Generate Root Certificate Private Key\nmkdir $TMP_CERT_DIR\ncd $TMP_CERT_DIR\nopenssl ecparam -noout -name prime256v1 -genkey -out ca-key.pem\n\n\n# Generate Public Key\nopenssl req -x509 -new -nodes -key ca-key.pem -sha256 -days 1825 -out ca-cert.pem\n\n\n#Generate EC Private Key\nopenssl ecparam -noout -name prime256v1 -genkey -out sp-key.pem\n\n#Generate an openssl.conf file which includes a hex-encoded TNAuthList extension\ncat >TNAuthList.conf << EOF\nasn1=SEQUENCE:tn_auth_list\n[tn_auth_list]\nfield1=EXP:0,IA5:1001\nEOF\n\nopenssl asn1parse -genconf TNAuthList.conf -out TNAuthList.der\n\ncat >openssl.conf << EOF\n[ req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n[ req_distinguished_name ]\ncommonName = \"SHAKEN\"\n[ v3_req ]\nEOF\n\nod -An -t x1 -w TNAuthList.der | sed -e 's/ /:/g' -e 's/^/1.3.6.1.5.5.7.1.26=DER/' >>openssl.conf\n\n\n# Generate a Certificate Signing Request, which includes our required TNAuthorizationList\nopenssl req -new -nodes -key sp-key.pem -keyform PEM \\\n    -subj '/C=US/ST=VA/L=Somewhere/O=AcmeTelecom, Inc./OU=VOIP/CN=SHAKEN' \\\n    -sha256 -config openssl.conf \\\n    -out sp-csr.pem\n\n\n# On the CA side, generate the certificate (User-CSR + CA-cert + CA-key => User-cert)\nopenssl x509 -req -in sp-csr.pem -CA ../stir-shaken-ca/ca-cert.pem -CAkey ../stir-shaken-ca/ca-key.pem -CAcreateserial \\\n  -days 825 -sha256 -extfile openssl.conf -extensions v3_req -out sp-cert.pem\n\n#  verify that the SP certificate contains the TNAuthList extension\nopenssl x509 -in sp-cert.pem -text -noout\n\n# Copy Key and Certificate to /opt/dsiprouter\ncp $TMP_CERT_DIR/sp-cert.pem $DSIP_CERT_DIR\ncp $TMP_CERT_DIR/sp-key.pem  $DSIP_CERT_DIR\n\n#Change the ownership of the certificates and key\nchown -R dsiprouter:kamailio $DSIP_CERT_DIR\n\n#Change permissions\nchmod -R 755 $DSIP_CERT_DIR\n"
  },
  {
    "path": "resources/syslog/consul.conf",
    "content": "local3.*        /var/log/consul.log\n& stop\n"
  },
  {
    "path": "resources/syslog/dsiprouter.conf",
    "content": "$EscapeControlCharactersOnReceive off\n$Escape8BitCharactersOnReceive off\n$template dsipFormat,\"%rawmsg:6:$:%\\n\"\nlocal2.*        /var/log/dsiprouter.log;dsipFormat\n& stop\n"
  },
  {
    "path": "resources/syslog/kamailio.conf",
    "content": "local0.*        /var/log/kamailio.log\n& stop\n"
  },
  {
    "path": "resources/syslog/rsyslog.conf",
    "content": "#############################################################\n# dSIPRouter rsyslog.conf(5) - rsyslogd(8) configuration file\n#############################################################\n\n\n#############################################################\n# MODULES\n#############################################################\n\n# provides support for local system logging\nmodule(load=\"imuxsock\")\n\n# provides kernel logging support\nmodule(load=\"imklog\")\n\n# provides --MARK-- message capability\n#module(load=\"immark\")\n\n# provides UDP syslog reception\n#module(load=\"imudp\")\n#input(type=\"imudp\" port=\"514\")\n\n# provides TCP syslog reception\n#module(load=\"imtcp\")\n#input(type=\"imtcp\" port=\"514\")\n\n\n#############################################################\n# GLOBAL DIRECTIVES\n#############################################################\n\n# Use traditional timestamp format\n# For high precision timestamps comment this line out\n$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat\n\n# File syncing capability is disabled by default\n# Usually not required and an extreme performance hit\n#$ActionFileEnableSync on\n\n# Set the default permissions for all log files\n$FileOwner root\n$FileGroup adm\n$FileCreateMode 0640\n$DirCreateMode 0755\n$Umask 0022\n\n# Where to place spool and state files\n$WorkDirectory /var/spool/rsyslog\n\n# Include all config files in /etc/rsyslog.d/\n$IncludeConfig /etc/rsyslog.d/*.conf\n\n\n#############################################################\n# RULES\n#############################################################\n\n*.*;auth.none;authpriv.none;cron.none   /var/log/syslog\nauth.*,authpriv.*                       /var/log/auth.log\ncron.*                                  /var/log/cron.log\ndaemon.*                                /var/log/daemon.log\nkern.*                                  /var/log/kern.log\nlpr.*                                   /var/log/lpr.log\nmail.*                                  /var/log/mail.log\nuser.*                                  /var/log/user.log\n*.emerg                                 :omusrmsg:*\n\n"
  },
  {
    "path": "resources/syslog/rtpengine.conf",
    "content": "local1.*        /var/log/rtpengine.log\n& stop\n"
  },
  {
    "path": "resources/terraform/do/.gitignore",
    "content": ".terraform\n.terraform*\nterraform.tfvars\nterraform.tfstate\nterraform.tfstate.backup"
  },
  {
    "path": "resources/terraform/do/README.md",
    "content": "## Installing dSIPRouter Using Terraform on Digital Ocean\n\n1. Generate an SSH key if you don't already have one\n\n2. Configure the SSH key into your Digital Ocean Account\n\n3. Obtain a Digital Ocean API key and store the key as an environment variable\n\n```\nexport DIGITALOCEAN_TOKEN='put your token here'\n```\n\n4. Copy terraform.tfvars.sample to terraform.tfvars\n\n```\ncp terraform.tfvars.sample terraform.tfvars\n```\n5. Modify terraform.tfvars so that it overrides your variables, which is located in variables.tf.  The pvt_key_path is the location of your private key.  The pub_key_name is the name of the public key you defined when you uploaded your SSH key.  The dsiprouter_prefix is the prefix that will be concatenated to the name of the droplet that will be created.  The number_of_environments is used to specify how many instances will be crated.\n\n```\npvt_key_path=\"your path to key\"\ndns_domain=\"dsiprouter.net\"\ndns_hostname=\"training\"\nnumber_of_environments=1\npub_key_name=\"dopensource-training\"\nadditional_commands=\"echo\"\n```\n\nAll of the variables and any default values can be found in variables.tf.  \n\n6. Create a new instance of dSIPRouter\n\nThe following command will create a new instance of dSIPRouter based on the master branch.  The OS image will be Debian 11 and the dsiprouter_prefix will be overriden by demo.\n\n```\nterraform apply -var branch=master\n```\n\nIf you want to create a demo instance of dSIPRouter in your Digitalocean environment with a DNS record use this.  \nNote, you will need to change the dns_demo_domain variable to a domain that you have hosted with DigitalOcean.\n\n```\nterraform apply -var branch=master -var dns_hostname=test -var dns_demo_domain=dsiprouter.org -var additional_commands='dsiprouter setcredentials -dc admin:ZmIwMTdmY2I5NjE4'\n```\n\n7. Destroy your instance if you are done with it by using the terraform destroy command\n\n```\nterraform destroy\n```\n\n## Need Help?\n\nWe offer paid support for this Terraform script!  You can purchase 2 hours of support from [here](https://dopensource.com/product-category/prepaid-support/)\n"
  },
  {
    "path": "resources/terraform/do/main.tf",
    "content": "terraform {\n  required_providers {\n    digitalocean = {\n      source = \"digitalocean/digitalocean\"\n      version = \"2.60.0\"\n    }\n  }\n}\n\nprovider \"digitalocean\" {}\n\ndata \"digitalocean_ssh_key\" \"ssh_key\" {\n  name = var.pub_key_name\n}\n\nresource \"digitalocean_reserved_ip\" \"floating_ip\" {\n  region = var.region\n}\n\nresource \"digitalocean_record\" \"dns_record\" {\n  domain = var.dns_domain\n  type = \"A\"\n  name = var.dns_hostname\n  value = digitalocean_reserved_ip.floating_ip.ip_address\n}\n\nresource \"digitalocean_droplet\" \"dsiprouter\" {\n  name = digitalocean_record.dns_record.fqdn\n  region = var.region\n  size = var.image_size\t\n  image = var.image\n  ssh_keys = [data.digitalocean_ssh_key.ssh_key.fingerprint]\n}\n\n# we w for the assignment to complete and then run provisioner\n# this allows for us to have a valid DNS record prior to install\nresource \"digitalocean_reserved_ip_assignment\" \"floating_ip_assn\" {\n  ip_address = digitalocean_reserved_ip.floating_ip.ip_address\n  droplet_id = digitalocean_droplet.dsiprouter.id\n\n  provisioner \"remote-exec\" {\n    connection {\n      type = \"ssh\"\n      user = \"root\"\n      host = digitalocean_reserved_ip.floating_ip.ip_address\n      private_key = file(var.pvt_key_path)\n      timeout = \"5m\"\n    }\n\n    inline = [\n      \"for i in `seq 0 10`; do [ $i -eq 10 ] && { echo 'failed waiting on cloud-init boot'; exit 1; } || { [ -f /var/lib/cloud/instance/boot-finished ] && break; sleep 3; }; done\",\n      \"apt-get update -y\",\n      \"for i in `seq 0 10`; do [ $i -eq 10 ] && { echo 'failed waiting on DNS record'; exit 1; } || { getent hosts ${digitalocean_record.dns_record.fqdn} >/dev/null && break; sleep 3; }; done\",\n      \"apt-get install -y git\",\n      var.pull_request != \"\" ? \"git clone https://github.com/dOpensource/dsiprouter.git /opt/dsiprouter && cd /opt/dsiprouter && git fetch origin pull/${var.pull_request}/head:pr_${var.pull_request}; git switch pr_${var.pull_request}\" : \"git clone -b ${var.branch}  https://github.com/dOpensource/dsiprouter.git /opt/dsiprouter\",\n      \"/opt/dsiprouter/dsiprouter.sh install -all\",\n      \"${var.additional_commands}\"\n    ]\n  }\n}\n"
  },
  {
    "path": "resources/terraform/do/terraform.tfvars.sample",
    "content": "pvt_key_path=\"/Users/mackhendricks/.ssh/dopensource-training\"\ndns_domain=\"dsiprouter.net\"\ndns_hostname=\"training\"\nnumber_of_environments=1\npub_key_name=\"dopensource-training\"\nadditional_commands=\"echo\""
  },
  {
    "path": "resources/terraform/do/variables.tf",
    "content": "variable \"pvt_key_path\" {\n\ttype=string\n}\n\nvariable \"pub_key_name\" {\n\ttype=string\n}\n\nvariable \"dns_domain\" {\n\ttype=string\n\tdefault=\"\"\n}\n\nvariable \"dns_hostname\" {\n\ttype=string\n\tdefault=\"demo\"\n}\n\nvariable \"branch\" {\n\ttype=string\n\tdefault=\"master\"\n}\n\nvariable \"pull_request\" {\n\ttype=string\n\tdefault=\"\"\n}\n\nvariable \"region\" {\n\ttype=string\n\tdefault=\"tor1\"\n}\n\nvariable \"image\" {\n\ttype=string\n\tdefault=\"debian-12-x64\"\n}\n\nvariable \"image_size\" {\n\ttype=string\n\tdefault=\"2gb\"\n}\n\nvariable \"additional_commands\" {\n\ttype=string\n\tdefault=\"echo\"\n}\n"
  },
  {
    "path": "resources/upgrade/v0.72/scripts/bootstrap.sh",
    "content": "#!/usr/bin/env bash\n\nexport BOOTSTRAPPING_UPGRADE=1\nexport DSIP_PROJECT_DIR='/tmp/dsiprouter'\nTAG_NAME='v0.72-rel'\nREPO_URL='https://github.com/dOpensource/dsiprouter.git'\nrm -f /etc/dsiprouter/.requirementsinstalled\nrm -rf \"$DSIP_PROJECT_DIR\" 2>/dev/null\ngit clone --depth 1 -c advice.detachedHead=false -b \"$TAG_NAME\" \"$REPO_URL\" \"$DSIP_PROJECT_DIR\"\n${DSIP_PROJECT_DIR}/dsiprouter.sh upgrade -rel v0.72\n"
  },
  {
    "path": "resources/upgrade/v0.72/scripts/migrate.sh",
    "content": "#!/usr/bin/env bash\n\n# set project dir (where src files are located)\nexport DSIP_PROJECT_DIR=${DSIP_PROJECT_DIR:-/opt/dsiprouter}\n# import dsip_lib utility / shared functions\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nprintdbg 'backing up configs just in case the upgrade fails'\nBACKUP_DIR=\"/var/backups\"\nCURR_BACKUP_DIR=\"${BACKUP_DIR}/$(date '+%s')\"\nmkdir -p ${CURR_BACKUP_DIR}/{opt/dsiprouter,var/lib/mysql,${HOME},etc/dsiprouter,etc/kamailio,etc/rtpengine}\ncp -rf /opt/dsiprouter/. ${CURR_BACKUP_DIR}/opt/dsiprouter/\ncp -rf /etc/kamailio/. ${CURR_BACKUP_DIR}/etc/kamailio/\ncp -rf /var/lib/mysql/. ${CURR_BACKUP_DIR}/var/lib/mysql/\ncp -f /etc/my.cnf ${CURR_BACKUP_DIR}/etc/ 2>/dev/null\ncp -rf /etc/mysql/. ${CURR_BACKUP_DIR}/etc/mysql/\ncp -f ${HOME}/.my.cnf ${CURR_BACKUP_DIR}/${HOME}/ 2>/dev/null\n# TODO: backup current systemd service files\n\nprintdbg 'retrieving current settings'\nexport DSIP_ID=$(cat /etc/machine-id | hashCreds)\nexport DSIP_CLUSTER_ID=$(getConfigAttrib \"DSIP_CLUSTER_ID\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_CLUSTER_SYNC=$(getConfigAttrib \"DSIP_CLUSTER_SYNC\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_PROTO=$(getConfigAttrib \"DSIP_PROTO\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_PORT=$(getConfigAttrib \"DSIP_PORT\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_USERNAME=$(getConfigAttrib \"DSIP_USERNAME\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_API_PROTO=$(getConfigAttrib \"DSIP_API_PROTO\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_API_PORT=$(getConfigAttrib \"DSIP_API_PORT\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_PRIV_KEY=$(getConfigAttrib \"DSIP_PRIV_KEY\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_PID_FILE=$(getConfigAttrib \"DSIP_PID_FILE\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_UNIX_SOCK=$(getConfigAttrib \"DSIP_UNIX_SOCK\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_IPC_SOCK=$(getConfigAttrib \"DSIP_IPC_SOCK\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_LOG_LEVEL=$(getConfigAttrib \"DSIP_LOG_LEVEL\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_LOG_FACILITY=$(getConfigAttrib \"DSIP_LOG_FACILITY\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_SSL_KEY=$(getConfigAttrib \"DSIP_SSL_KEY\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_SSL_CERT=$(getConfigAttrib \"DSIP_SSL_CERT\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_SSL_CA=$(getConfigAttrib \"DSIP_SSL_CA\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_SSL_EMAIL=$(getConfigAttrib \"DSIP_SSL_EMAIL\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_CERTS_DIR=$(getConfigAttrib \"DSIP_CERTS_DIR\" \"/etc/dsiprouter/gui/settings.py\")\nexport ROLE=$(getConfigAttrib \"ROLE\" \"/etc/dsiprouter/gui/settings.py\")\nexport GUI_INACTIVE_TIMEOUT=$(getConfigAttrib \"GUI_INACTIVE_TIMEOUT\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_DB_HOST=$(getConfigAttrib \"KAM_DB_HOST\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_DB_DRIVER=$(getConfigAttrib \"KAM_DB_DRIVER\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_DB_TYPE=$(getConfigAttrib \"KAM_DB_TYPE\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_DB_PORT=$(getConfigAttrib \"KAM_DB_PORT\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_DB_NAME=$(getConfigAttrib \"KAM_DB_NAME\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_DB_USER=$(getConfigAttrib \"KAM_DB_USER\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_KAMCMD_PATH=$(getConfigAttrib \"KAM_KAMCMD_PATH\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_CFG_PATH=$(getConfigAttrib \"KAM_CFG_PATH\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_TLSCFG_PATH=$(getConfigAttrib \"KAM_TLSCFG_PATH\" \"/etc/dsiprouter/gui/settings.py\")\nexport RTP_CFG_PATH=$(getConfigAttrib \"RTP_CFG_PATH\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLT_CARRIER=$(getConfigAttrib \"FLT_CARRIER\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLT_PBX=$(getConfigAttrib \"FLT_PBX\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLT_MSTEAMS=$(getConfigAttrib \"FLT_MSTEAMS\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLT_OUTBOUND=$(getConfigAttrib \"FLT_OUTBOUND\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLT_INBOUND=$(getConfigAttrib \"FLT_INBOUND\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLT_LCR_MIN=$(getConfigAttrib \"FLT_LCR_MIN\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLT_FWD_MIN=$(getConfigAttrib \"FLT_FWD_MIN\" \"/etc/dsiprouter/gui/settings.py\")\nexport DEFAULT_AUTH_DOMAIN=$(getConfigAttrib \"DEFAULT_AUTH_DOMAIN\" \"/etc/dsiprouter/gui/settings.py\")\nexport TELEBLOCK_GW_ENABLED=$(getConfigAttrib \"TELEBLOCK_GW_ENABLED\" \"/etc/dsiprouter/gui/settings.py\")\nexport TELEBLOCK_GW_IP=$(getConfigAttrib \"TELEBLOCK_GW_IP\" \"/etc/dsiprouter/gui/settings.py\")\nexport TELEBLOCK_GW_PORT=$(getConfigAttrib \"TELEBLOCK_GW_PORT\" \"/etc/dsiprouter/gui/settings.py\")\nexport TELEBLOCK_MEDIA_IP=$(getConfigAttrib \"TELEBLOCK_MEDIA_IP\" \"/etc/dsiprouter/gui/settings.py\")\nexport TELEBLOCK_MEDIA_PORT=$(getConfigAttrib \"TELEBLOCK_MEDIA_PORT\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLOWROUTE_ACCESS_KEY=$(getConfigAttrib \"FLOWROUTE_ACCESS_KEY\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLOWROUTE_SECRET_KEY=$(getConfigAttrib \"FLOWROUTE_SECRET_KEY\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLOWROUTE_API_ROOT_URL=$(getConfigAttrib \"FLOWROUTE_API_ROOT_URL\" \"/etc/dsiprouter/gui/settings.py\")\nexport HOMER_ID=$(cat /etc/machine-id | hashCreds -l 4 | dd if=/dev/stdin of=/dev/stdout bs=1 count=8 2>/dev/null | hextoint)\nexport HOMER_HEP_HOST=$(getConfigAttrib \"HOMER_HEP_HOST\" \"/etc/dsiprouter/gui/settings.py\")\nexport HOMER_HEP_PORT=$(getConfigAttrib \"HOMER_HEP_PORT\" \"/etc/dsiprouter/gui/settings.py\")\nexport NETWORK_MODE='0'\nexport UPLOAD_FOLDER=$(getConfigAttrib \"UPLOAD_FOLDER\" \"/etc/dsiprouter/gui/settings.py\")\nexport MAIL_SERVER=$(getConfigAttrib \"MAIL_SERVER\" \"/etc/dsiprouter/gui/settings.py\")\nexport MAIL_PORT=$(getConfigAttrib \"MAIL_PORT\" \"/etc/dsiprouter/gui/settings.py\")\nexport MAIL_USE_TLS=$(getConfigAttrib \"MAIL_USE_TLS\" \"/etc/dsiprouter/gui/settings.py\")\nexport MAIL_USERNAME=$(getConfigAttrib \"MAIL_USERNAME\" \"/etc/dsiprouter/gui/settings.py\")\nexport MAIL_ASCII_ATTACHMENTS=$(getConfigAttrib \"MAIL_ASCII_ATTACHMENTS\" \"/etc/dsiprouter/gui/settings.py\")\nexport MAIL_DEFAULT_SENDER=$(getConfigAttrib \"MAIL_DEFAULT_SENDER\" \"/etc/dsiprouter/gui/settings.py\")\nexport MAIL_DEFAULT_SUBJECT=$(getConfigAttrib \"MAIL_DEFAULT_SUBJECT\" \"/etc/dsiprouter/gui/settings.py\")\nexport BACKUP_FOLDER=$(getConfigAttrib \"BACKUP_FOLDER\" \"/etc/dsiprouter/gui/settings.py\")\nexport TRANSNEXUS_AUTHSERVICE_HOST=$(getConfigAttrib \"TRANSNEXUS_AUTHSERVICE_HOST\" \"/etc/dsiprouter/gui/settings.py\")\nexport TRANSNEXUS_VERIFYSERVICE_HOST=$(getConfigAttrib \"TRANSNEXUS_VERIFYSERVICE_HOST\" \"/etc/dsiprouter/gui/settings.py\")\nexport STIR_SHAKEN_PREFIX_A=$(getConfigAttrib \"STIR_SHAKEN_PREFIX_A\" \"/etc/dsiprouter/gui/settings.py\")\nexport STIR_SHAKEN_PREFIX_B=$(getConfigAttrib \"STIR_SHAKEN_PREFIX_B\" \"/etc/dsiprouter/gui/settings.py\")\nexport STIR_SHAKEN_PREFIX_C=$(getConfigAttrib \"STIR_SHAKEN_PREFIX_C\" \"/etc/dsiprouter/gui/settings.py\")\nexport STIR_SHAKEN_PREFIX_INVALID=$(getConfigAttrib \"STIR_SHAKEN_PREFIX_INVALID\" \"/etc/dsiprouter/gui/settings.py\")\nexport STIR_SHAKEN_BLOCK_INVALID=$(getConfigAttrib \"STIR_SHAKEN_BLOCK_INVALID\" \"/etc/dsiprouter/gui/settings.py\")\nexport STIR_SHAKEN_CERT_URL=$(getConfigAttrib \"STIR_SHAKEN_CERT_URL\" \"/etc/dsiprouter/gui/settings.py\")\nexport STIR_SHAKEN_KEY_PATH=$(getConfigAttrib \"STIR_SHAKEN_KEY_PATH\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_DOCS_DIR=\"${DSIP_PROJECT_DIR}/docs\"\nexport ROOT_DB_USER=$(getConfigAttrib \"ROOT_DB_USER\" \"/etc/dsiprouter/gui/settings.py\")\nexport ROOT_DB_NAME=$(getConfigAttrib \"ROOT_DB_NAME\" \"/etc/dsiprouter/gui/settings.py\")\nexport LOAD_SETTINGS_FROM=$(getConfigAttrib \"LOAD_SETTINGS_FROM\" \"/etc/dsiprouter/gui/settings.py\")\n\n# TODO: currently no way of easily transferring the license keys to the upgraded platform\n#TRANSNEXUS_LICENSE_KEY -> DSIP_TRANSNEXUS_LICENSE\n\ngetCredentials() {\n    local SALT_LEN='64'\n    local DK_LEN_DEFAULT='64'\n    local CREDS_MAX_LEN='64'\n    local HASH_ITERATIONS='10000'\n    local HASHED_CREDS_ENCODED_MAX_LEN='256'\n    local AESCTR_CREDS_ENCODED_MAX_LEN='160'\n\n    printwarn 'dSIPRouter admin password hash can not be undone, generating new one'\n    export DSIP_PASSWORD=$(urandomChars 64)\n    printdbg \"temporary password: $DSIP_PASSWORD\"\n    export DSIP_API_TOKEN=$(decryptConfigAttrib \"DSIP_API_TOKEN\" \"/etc/dsiprouter/gui/settings.py\")\n    export DSIP_IPC_PASS=$(decryptConfigAttrib \"DSIP_IPC_PASS\" \"/etc/dsiprouter/gui/settings.py\")\n    export KAM_DB_PASS=$(decryptConfigAttrib \"KAM_DB_PASS\" \"/etc/dsiprouter/gui/settings.py\")\n    export MAIL_PASSWORD=$(decryptConfigAttrib \"MAIL_PASSWORD\" \"/etc/dsiprouter/gui/settings.py\")\n    export ROOT_DB_PASS=$(decryptConfigAttrib \"ROOT_DB_PASS\" \"/etc/dsiprouter/gui/settings.py\")\n}\ngetCredentials\n\nencryptCreds() { (\n    if (( ${BOOTSTRAPPING_UPGRADE:-0} == 1 )); then\n        cd /tmp/dsiprouter/gui\n    else\n        cd ${DSIP_PROJECT_DIR}/gui\n    fi\n    python3 -c \"from util.security import AES_CTR; print(AES_CTR.encrypt('$1').decode('utf-8'), end='');\"\n) }\nDSIP_PASSWORD_HASH=$(hashCreds \"$DSIP_PASSWORD\")\nDSIP_API_TOKEN_CIPHERTEXT=$(encryptCreds \"$DSIP_API_TOKEN\")\nDSIP_IPC_PASS_CIPHERTEXT=$(encryptCreds \"$DSIP_IPC_PASS\")\nKAM_DB_PASS_CIPHERTEXT=$(encryptCreds \"$KAM_DB_PASS\")\nMAIL_PASSWORD_CIPHERTEXT=$(encryptCreds \"$MAIL_PASSWORD\")\nROOT_DB_PASS_CIPHERTEXT=$(encryptCreds \"$ROOT_DB_PASS\")\n\nprintdbg 'migrating database schema'\n(\ncat <<'EOF'\nALTER TABLE address\n  MODIFY tag VARCHAR(255) NOT NULL DEFAULT '';\n\nALTER TABLE dispatcher\n  MODIFY description VARCHAR(255) NOT NULL DEFAULT '';\n\nALTER TABLE dr_gateways\n  MODIFY pri_prefix VARCHAR(64) NOT NULL DEFAULT '',\n  MODIFY attrs VARCHAR(255) NOT NULL DEFAULT '',\n  MODIFY description VARCHAR(255) NOT NULL DEFAULT '';\n\nALTER TABLE dr_gw_lists\n  MODIFY description VARCHAR(255) NOT NULL DEFAULT '';\n\nALTER TABLE dr_rules\n  MODIFY description VARCHAR(255) NOT NULL DEFAULT '';\n\nALTER TABLE dsip_cdrinfo\n  MODIFY email VARCHAR(255) NOT NULL DEFAULT '';\n\nALTER TABLE subscriber\n  ADD IF NOT EXISTS  email_address VARCHAR(128) NOT NULL DEFAULT '',\n  ADD IF NOT EXISTS  rpid          VARCHAR(128) NOT NULL DEFAULT '';\n\nALTER TABLE `acc`\n  MODIFY `from_tag` VARCHAR (128) NOT NULL DEFAULT '',\n  MODIFY `to_tag` VARCHAR (128) NOT NULL DEFAULT '',\n  MODIFY `callid` VARCHAR (255) NOT NULL DEFAULT '',\n  MODIFY `sip_reason` VARCHAR (255) NOT NULL DEFAULT '',\n  MODIFY `time` DATETIME NOT NULL DEFAULT NOW(),\n  MODIFY `dst_ouser` VARCHAR (128) NOT NULL DEFAULT '',\n  MODIFY `dst_user` VARCHAR (128) NOT NULL DEFAULT '',\n  MODIFY `dst_domain` VARCHAR (255) NOT NULL DEFAULT '',\n  MODIFY `src_user` VARCHAR (128) NOT NULL DEFAULT '',\n  MODIFY `src_domain` VARCHAR (255) NOT NULL DEFAULT '',\n  MODIFY `src_gwgroupid` VARCHAR (10) NOT NULL DEFAULT '',\n  MODIFY `dst_gwgroupid` VARCHAR (10) NOT NULL DEFAULT '';\n\nALTER TABLE `cdrs`\n  MODIFY `cdr_id`          BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,\n  MODIFY `src_username`    VARCHAR(128)        NOT NULL DEFAULT '',\n  MODIFY `src_domain`      VARCHAR(255)        NOT NULL DEFAULT '',\n  MODIFY `dst_username`    VARCHAR(128)        NOT NULL DEFAULT '',\n  MODIFY `dst_domain`      VARCHAR(255)        NOT NULL DEFAULT '',\n  MODIFY `dst_ousername`   VARCHAR(128)        NOT NULL DEFAULT '',\n  MODIFY `call_start_time` DATETIME            NOT NULL,\n  MODIFY `sip_call_id`     VARCHAR(255)        NOT NULL DEFAULT '',\n  MODIFY `created`         DATETIME            NOT NULL DEFAULT NOW(),\n  MODIFY `src_gwgroupid`   VARCHAR(10)         NOT NULL DEFAULT '',\n  MODIFY `dst_gwgroupid`   VARCHAR(10)         NOT NULL DEFAULT '';\n\nDROP PROCEDURE IF EXISTS `kamailio_cdrs`;\nDELIMITER //\nCREATE PROCEDURE `kamailio_cdrs`()\nBEGIN\n  DECLARE done INT DEFAULT 0;\n  DECLARE bye_record INT DEFAULT 0;\n  DECLARE v_src_user,v_src_domain,v_dst_user,v_dst_domain,v_callid,v_from_tag,\n    v_to_tag,v_src_ip,v_calltype VARCHAR(255);\n  DECLARE v_src_gwgroupid, v_dst_gwgroupid INT(11);\n  DECLARE v_inv_time, v_bye_time DATETIME;\n  DECLARE inv_cursor CURSOR FOR\n    SELECT src_user,\n           src_domain,\n           dst_user,\n           dst_domain,\n           time,\n           callid,\n           from_tag,\n           to_tag,\n           src_ip,\n           calltype,\n           src_gwgroupid,\n           dst_gwgroupid\n    FROM acc\n    WHERE method = 'INVITE'\n      AND cdr_id = '0';\n  DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;\n  OPEN inv_cursor;\n  REPEAT\n    FETCH inv_cursor INTO v_src_user, v_src_domain, v_dst_user, v_dst_domain,\n      v_inv_time, v_callid, v_from_tag, v_to_tag, v_src_ip, v_calltype,\n      v_src_gwgroupid, v_dst_gwgroupid;\n    IF NOT done THEN\n      SET bye_record = 0;\n      SELECT 1, time\n      INTO bye_record, v_bye_time\n      FROM acc\n      WHERE method = 'BYE'\n        AND callid = v_callid\n        AND ((from_tag = v_from_tag\n        AND to_tag = v_to_tag)\n        OR (from_tag = v_to_tag AND to_tag = v_from_tag))\n      ORDER BY time ASC\n      LIMIT 1;\n      IF bye_record = 1 THEN\n        INSERT INTO cdrs (src_username, src_domain, dst_username, dst_domain,\n                          call_start_time, duration, sip_call_id, sip_from_tag,\n                          sip_to_tag, src_ip, created, calltype, src_gwgroupid, dst_gwgroupid)\n        VALUES (v_src_user, v_src_domain, v_dst_user, v_dst_domain, v_inv_time,\n                UNIX_TIMESTAMP(v_bye_time) - UNIX_TIMESTAMP(v_inv_time),\n                v_callid, v_from_tag, v_to_tag, v_src_ip, NOW(), v_calltype,\n                v_src_gwgroupid, v_dst_gwgroupid);\n        UPDATE acc\n        SET cdr_id=LAST_INSERT_ID()\n        WHERE callid = v_callid\n          AND from_tag = v_from_tag\n          AND to_tag = v_to_tag;\n      END IF;\n      SET done = 0;\n    END IF;\n  UNTIL done END REPEAT;\nEND //\nDELIMITER ;\n\nDROP PROCEDURE IF EXISTS `kamailio_rating`;\nDELIMITER //\nCREATE PROCEDURE `kamailio_rating`(`rgroup` VARCHAR(64))\nBEGIN\n  DECLARE done, rate_record, vx_cost INT DEFAULT 0;\n  DECLARE v_cdr_id BIGINT DEFAULT 0;\n  DECLARE v_duration, v_rate_unit, v_time_unit INT DEFAULT 0;\n  DECLARE v_dst_username VARCHAR(255);\n  DECLARE cdrs_cursor CURSOR FOR SELECT cdr_id, dst_username, duration\n                                 FROM cdrs\n                                 WHERE rated = 0;\n  DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;\n  OPEN cdrs_cursor;\n  REPEAT\n    FETCH cdrs_cursor INTO v_cdr_id, v_dst_username, v_duration;\n    IF NOT done THEN\n      SET rate_record = 0;\n      SELECT 1, rate_unit, time_unit\n      INTO rate_record, v_rate_unit, v_time_unit\n      FROM billing_rates\n      WHERE rate_group = rgroup\n        AND v_dst_username LIKE CONCAT(prefix, '%')\n      ORDER BY prefix DESC\n      LIMIT 1;\n      IF rate_record = 1 THEN\n        SET vx_cost = v_rate_unit * CEIL(v_duration / v_time_unit);\n        UPDATE cdrs SET rated=1, cost=vx_cost WHERE cdr_id = v_cdr_id;\n      END IF;\n      SET done = 0;\n    END IF;\n  UNTIL done END REPEAT;\nEND //\nDELIMITER ;\nEOF\n) | sqlAsTransaction --user=\"$ROOT_DB_USER\" --password=\"$ROOT_DB_PASS\" --host=\"$KAM_DB_HOST\" --port=\"$KAM_DB_PORT\"\n\nif (( $? != 0 )); then\n    printerr 'Failed merging DB schema'\n    exit 1\nfi\n\nif (( ${BOOTSTRAPPING_UPGRADE:-0} == 1 )); then\n    PROJECT_DSIP_DEFAULTS_DIR='/tmp/dsiprouter/kamailio/defaults'\nelse\n    PROJECT_DSIP_DEFAULTS_DIR='/opt/dsiprouter/kamailio/defaults'\nfi\nperl -e \"\\$hlen='$HASHED_CREDS_ENCODED_MAX_LEN'; \\$clen='$AESCTR_CREDS_ENCODED_MAX_LEN';\" \\\n    -pe 's%\\@HASHED_CREDS_ENCODED_MAX_LEN%$hlen%g; s%\\@AESCTR_CREDS_ENCODED_MAX_LEN%$clen%g;' \\\n    ${PROJECT_DSIP_DEFAULTS_DIR}/dsip_settings.sql |\n    mysql -s -N --user=\"$ROOT_DB_USER\" --password=\"$ROOT_DB_PASS\" --host=\"$KAM_DB_HOST\" --port=\"$KAM_DB_PORT\" \"$KAM_DB_NAME\"\n\nif (( $? != 0 )); then\n    printerr 'Failed merging DB schema'\n    exit 1\nfi\n\nprintdbg 'configuring dsiprouter GUI'\nif (( ${BOOTSTRAPPING_UPGRADE:-0} == 1 )); then\n    # a few stragglers that need copied over\n    cp -f /opt/dsiprouter/gui/modules/fusionpbx/certs/cert.key /tmp/dsiprouter/gui/modules/fusionpbx/certs/cert.key\n    cp -f /opt/dsiprouter/gui/modules/fusionpbx/certs/cert_combined.crt /tmp/dsiprouter/gui/modules/fusionpbx/certs/cert.key\n    # use the bootstrap repo instead cloning again\n    rm -rf /opt/dsiprouter\n    mv -f /tmp/dsiprouter /opt/dsiprouter\nelse\n    # fresh repo coming up\n    rm -rf /opt/dsiprouter\n    git clone --depth 1 -c advice.detachedHead=false -b v0.72-rel https://github.com/dOpensource/dsiprouter.git /opt/dsiprouter\nfi\nexport DSIP_PROJECT_DIR=/opt/dsiprouter\n\nprintdbg 'installing python dependencies for the GUI'\npython3 -m pip install -U Flask~=2.0 psycopg2_binary requests SQLAlchemy~=2.0 Werkzeug~=2.0\n\nprintdbg 'generating dynamic config files for the GUI'\ndsiprouter configuredsip &&\nsetConfigAttrib 'DSIP_USERNAME' \"$DSIP_USERNAME\" /etc/dsiprouter/gui/settings.py -q &&\nsetConfigAttrib 'DSIP_PASSWORD' \"$DSIP_PASSWORD_HASH\" /etc/dsiprouter/gui/settings.py -qb &&\nsetConfigAttrib 'DSIP_API_TOKEN' \"$DSIP_API_TOKEN_CIPHERTEXT\" /etc/dsiprouter/gui/settings.py -qb &&\nsetConfigAttrib 'DSIP_IPC_PASS' \"$DSIP_IPC_PASS_CIPHERTEXT\" /etc/dsiprouter/gui/settings.py -qb &&\nsetConfigAttrib 'KAM_DB_USER' \"$KAM_DB_USER\" /etc/dsiprouter/gui/settings.py -q &&\nsetConfigAttrib 'KAM_DB_PASS' \"$KAM_DB_PASS_CIPHERTEXT\" /etc/dsiprouter/gui/settings.py -qb &&\nsetConfigAttrib 'KAM_DB_HOST' \"$KAM_DB_HOST\" /etc/dsiprouter/gui/settings.py -q &&\nsetConfigAttrib 'KAM_DB_PORT' \"$KAM_DB_PORT\" /etc/dsiprouter/gui/settings.py -q &&\nsetConfigAttrib 'KAM_DB_NAME' \"$KAM_DB_NAME\" /etc/dsiprouter/gui/settings.py -q &&\nsetConfigAttrib 'MAIL_USERNAME' \"$MAIL_USERNAME\" /etc/dsiprouter/gui/settings.py -q &&\nsetConfigAttrib 'MAIL_PASSWORD' \"$MAIL_PASSWORD_CIPHERTEXT\" /etc/dsiprouter/gui/settings.py -qb &&\nsetConfigAttrib 'ROOT_DB_USER' \"$ROOT_DB_USER\" /etc/dsiprouter/gui/settings.py -q &&\n{\n    if ! grep -q -oP '(b\"\"\".*\"\"\"|'\"b'''.*'''\"'|b\".*\"|'\"b'.*')\" <<<\"$ROOT_DB_PASS\"; then\n        setConfigAttrib 'ROOT_DB_PASS' \"$ROOT_DB_PASS\" /etc/dsiprouter/gui/settings.py -q\n    else\n        setConfigAttrib 'ROOT_DB_PASS' \"$ROOT_DB_PASS_CIPHERTEXT\" /etc/dsiprouter/gui/settings.py -qb\n    fi\n} &&\nsetConfigAttrib 'ROOT_DB_NAME' \"$ROOT_DB_NAME\" /etc/dsiprouter/gui/settings.py -q &&\nprintdbg 'successfully generated new settings file' ||\n{\n    printerr 'failed generating new settings file'\n    exit 1\n}\n\nif [[ \"$LOAD_SETTINGS_FROM\" == \"db\" ]]; then\n    printdbg 'updating dsip_settings table, the GUI will be restarted multiple times...'\n    setConfigAttrib 'LOAD_SETTINGS_FROM' 'file' /etc/dsiprouter/gui/settings.py &&\n    systemctl restart dsiprouter &&\n    setConfigAttrib 'LOAD_SETTINGS_FROM' 'db' /etc/dsiprouter/gui/settings.py &&\n    systemctl restart dsiprouter ||\n    {\n        printerr 'failed updating dsip_settings DB table'\n        exit 1\n    }\nelse\n    printdbg 'the dsip_settings table will be updated when the GUI service is restarted..'\nfi\n\nprintdbg 'generating documentation for the GUI'\n(\n    cd ${DSIP_PROJECT_DIR}/docs\n    make html >/dev/null 2>&1\n)\n\nprintdbg 'generating documentation for the CLI'\ncp -f ${DSIP_PROJECT_DIR}/resources/man/dsiprouter.1 /usr/share/man/man1/\ngzip -f /usr/share/man/man1/dsiprouter.1\nmandb\ncp -f ${DSIP_PROJECT_DIR}/dsiprouter/dsip_completion.sh /etc/bash_completion.d/dsiprouter\n\nprintdbg 'upgrading systemd service configurations'\nexport DISTRO=$(getDistroName)\nexport DISTRO_VER=$(getDistroVer)\nexport DISTRO_MAJOR_VER=$(cut -d '.' -f 1 <<<\"$DISTRO_VER\")\nexport DISTRO_MINOR_VER=$(cut -s -d '.' -f 2 <<<\"$DISTRO_VER\")\n\nexport INTERNAL_IP_ADDR=$(getInternalIP -4)\nexport INTERNAL_IP_NET=$(getInternalCIDR -4)\nexport INTERNAL_IP6_ADDR=$(getInternalIP -6)\nexport INTERNAL_IP_NET6=$(getInternalCIDR -6)\nEXTERNAL_IP_ADDR=$(getExternalIP -4)\nexport EXTERNAL_IP_ADDR=${EXTERNAL_IP_ADDR:-$INTERNAL_IP_ADDR}\nEXTERNAL_IP6_ADDR=$(getExternalIP -6)\nexport EXTERNAL_IP6_ADDR=${EXTERNAL_IP6_ADDR:-$INTERNAL_IP6_ADDR}\nexport INTERNAL_FQDN=$(getInternalFQDN)\nexport EXTERNAL_FQDN=$(getExternalFQDN)\nif [[ -z \"$EXTERNAL_FQDN\" ]] || ! checkConn \"$EXTERNAL_FQDN\"; then\n    export EXTERNAL_FQDN=\"$INTERNAL_FQDN\"\nfi\n\n    case \"$DISTRO\" in\n        debian|ubuntu)\n            cat << 'EOF' >/etc/systemd/system/dnsmasq.service\n[Unit]\nDescription=dnsmasq - A lightweight DHCP and caching DNS server\nRequires=basic.target network.target\nAfter=network.target network-online.target basic.target\nWants=nss-lookup.target\nBefore=nss-lookup.target\nDefaultDependencies=no\n\n[Service]\nType=forking\nPIDFile=/run/dnsmasq/dnsmasq.pid\nEnvironment='RUN_DIR=/run/dnsmasq'\n# make sure everything is setup correctly before starting\nExecStartPre=!-/usr/bin/dsiprouter chown -dnsmasq\nExecStartPre=/usr/sbin/dnsmasq --test\n# We run dnsmasq via the /etc/init.d/dnsmasq script which acts as a\n# wrapper picking up extra configuration files and then execs dnsmasq\n# itself, when called with the \"systemd-exec\" function.\nExecStart=/etc/init.d/dnsmasq systemd-exec\n# The systemd-*-resolvconf functions configure (and deconfigure)\n# resolvconf to work with the dnsmasq DNS server. They're called like\n# this to get correct error handling (ie don't start-resolvconf if the\n# dnsmasq daemon fails to start.\nExecStartPost=/etc/init.d/dnsmasq systemd-start-resolvconf\nExecStop=/etc/init.d/dnsmasq systemd-stop-resolvconf\nExecReload=/bin/kill -HUP $MAINPID\n\n[Install]\nWantedBy=multi-user.target\nEOF\n            ;;\n        almalinux|rocky)\n            cat << 'EOF' >/etc/systemd/system/dnsmasq.service\n[Unit]\nDescription=dnsmasq - A lightweight DHCP and caching DNS server\nRequires=basic.target network.target\nAfter=network.target network-online.target basic.target\nBefore=multi-user.target\nDefaultDependencies=no\n\n[Service]\nType=simple\nPIDFile=/run/dnsmasq/dnsmasq.pid\nEnvironment='RUN_DIR=/run/dnsmasq'\n# make sure everything is setup correctly before starting\nExecStartPre=!-/usr/bin/dsiprouter chown -dnsmasq\nExecStartPre=/usr/sbin/dnsmasq --test\nExecStart=/usr/sbin/dnsmasq -k\nExecReload=/bin/kill -HUP $MAINPID\n\n[Install]\nWantedBy=multi-user.target\nEOF\n            ;;\n        amzn|rhel)\n            cat << 'EOF' >/etc/systemd/system/dnsmasq.service\n[Unit]\nDescription=dnsmasq - A lightweight DHCP and caching DNS server\nRequires=basic.target network.target\nAfter=network.target network-online.target basic.target\nBefore=multi-user.target\nDefaultDependencies=no\n\n[Service]\nType=simple\nPermissionsStartOnly=true\nPIDFile=/run/dnsmasq/dnsmasq.pid\nEnvironment='RUN_DIR=/run/dnsmasq'\n# make sure everything is setup correctly before starting\nExecStartPre=/usr/bin/dsiprouter chown -dnsmasq\nExecStartPre=/usr/sbin/dnsmasq --test\nExecStart=/usr/sbin/dnsmasq -k\nExecReload=/bin/kill -HUP $MAINPID\n\n[Install]\nWantedBy=multi-user.target\nEOF\n            ;;\n    esac\n\nfor SERVICE in kamailio nginx dsiprouter; do\n    if [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.${SERVICE}installed\" ]]; then\n        SVC_FILE=$(grep -oP \"$SERVICE-v[0-9]+\\.service\" ${DSIP_PROJECT_DIR}/$SERVICE/${DISTRO}/${DISTRO_MAJOR_VER}.sh)\n        cp -f ${DSIP_PROJECT_DIR}/$SERVICE/systemd/$SVC_FILE /etc/systemd/system/$SERVICE.service\n    fi\ndone\n\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\" ]]; then\n    SVC_FILE=$(grep -m 1 -oP \"rtpengine-v[0-9]+\\.service\" ${DSIP_PROJECT_DIR}/rtpengine/${DISTRO}/install.sh)\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/systemd/$SVC_FILE /etc/systemd/system/rtpengine.service\nfi\n\nDSIP_SYSTEM_CONFIG_DIR=\"/etc/dsiprouter\"\nDSIP_CERTS_DIR=\"${DSIP_SYSTEM_CONFIG_DIR}/certs\"\ncp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.service /etc/systemd/system/nginx-watcher.service\nperl -p \\\n    -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n    ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/etc/systemd/system/nginx-watcher.path\nchmod 644 /etc/systemd/system/nginx-watcher.service\nchmod 644 /etc/systemd/system/nginx-watcher.path\n\nsystemctl daemon-reload\n\n# generate mysql service if needed\nreconfigureMysqlSystemdService\n\nprintdbg 'upgrading kamailio configs'\ndsiprouter configurekam\n\nprintdbg 'upgrading rtpengine configs'\ndsiprouter updatertpconfig\n\nprintdbg 'upgrading dnsmasq configs'\ndsiprouter updatednsconfig\n\nprintdbg 'updating file permissions'\ndsiprouter chown\n\nprintdbg 'restarting services'\nsystemctl restart dnsmasq\nsystemctl restart kamailio\nsystemctl restart nginx\nsystemctl restart dsiprouter\nsystemctl restart rtpengine\n\nexit 0\n"
  },
  {
    "path": "resources/upgrade/v0.72/settings.json",
    "content": "{\n  \"version\": \"0.72\",\n  \"depends\": \"0.70\",\n  \"install_location\": \"/opt/dsiprouter\",\n  \"dsiprouter\": [\n    \"migrate.sh\"\n  ]\n}"
  },
  {
    "path": "resources/upgrade/v0.721/scripts/bootstrap.sh",
    "content": "#!/usr/bin/env bash\n\nexport BOOTSTRAPPING_UPGRADE=1\nexport DSIP_PROJECT_DIR='/tmp/dsiprouter'\nTAG_NAME='v0.721-rel'\nREPO_URL='https://github.com/dOpensource/dsiprouter.git'\nrm -f /etc/dsiprouter/.requirementsinstalled\nrm -rf \"$DSIP_PROJECT_DIR\" 2>/dev/null\ngit clone --depth 1 -c advice.detachedHead=false -b \"$TAG_NAME\" \"$REPO_URL\" \"$DSIP_PROJECT_DIR\"\n${DSIP_PROJECT_DIR}/dsiprouter.sh upgrade -rel v0.721\n"
  },
  {
    "path": "resources/upgrade/v0.721/scripts/migrate.sh",
    "content": "#!/usr/bin/env bash\n\n# set project dir (where src files are located)\nexport DSIP_PROJECT_DIR=${DSIP_PROJECT_DIR:-/opt/dsiprouter}\n# import dsip_lib utility / shared functions\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n# function defs\ndecryptOldSetting() { (\n    if (( ${BOOTSTRAPPING_UPGRADE:-0} == 1 )); then\n        cd /opt/dsiprouter/gui\n    else\n        cd ${DSIP_PROJECT_DIR}/gui\n    fi\n\n    VALUE=$(grep -oP '^(?!#)(?:'$1')[ \\t]*=[ \\t]*\\K(?:\\w+\\(.*\\)[ \\t\\v]*$|[\\w\\d\\.]+[ \\t]*$|\\{.*\\}|\\[.*\\][ \\t]*$|\\(.*\\)[ \\t]*$|b?\"\"\".*\"\"\"[ \\t]*$|'\"b?'''.*'''\"'[ \\v]*$|b?\".*\"[ \\t]*$|'\"b?'.*'\"')' /etc/dsiprouter/gui/settings.py)\n    if ! printf '%s' \"${VALUE}\" | grep -q -oP '(b\"\"\".*\"\"\"|'\"b'''.*'''\"'|b\".*\"|'\"b'.*')\"; then\n        printf '%s' \"${VALUE}\" | perl -0777 -pe 's~^b?[\"'\"'\"']+(.*?)[\"'\"'\"']+$|(.*)~\\1\\2~g'\n    else\n        python3 -c \"import sys; sys.path.insert(0, '/etc/dsiprouter/gui'); import settings; from util.security import AES_CTR; print(AES_CTR.decrypt(settings.$1).decode('utf-8'), end='')\"\n    fi\n) }\nencryptNewCreds() { (\n    if (( ${BOOTSTRAPPING_UPGRADE:-0} == 1 )); then\n        cd /tmp/dsiprouter/gui\n    else\n        cd ${DSIP_PROJECT_DIR}/gui\n    fi\n    python3 -c \"from util.security import AES_CTR; print(AES_CTR.encrypt('$1').decode('utf-8'), end='');\"\n) }\n\nprintdbg 'verifying version requirements'\nCURRENT_VERSION=$(getConfigAttrib \"VERSION\" \"/etc/dsiprouter/gui/settings.py\")\nUPGRADE_DEPENDS=$(jq -r '.depends' <\"$(dirname \"$(dirname \"$(readlink -f \"$0\")\")\")/settings.json\")\nif [[ \"$CURRENT_VERSION\" != \"$UPGRADE_DEPENDS\" ]]; then\n    printerr \"unsupported upgrade scenario ($CURRENT_VERSION -> 0.721)\"\n    exit 1\nfi\n\nprintdbg 'backing up configs just in case the upgrade fails'\nBACKUP_DIR=\"/var/backups\"\nCURR_BACKUP_DIR=\"${BACKUP_DIR}/$(date '+%s')\"\nmkdir -p ${CURR_BACKUP_DIR}/{opt/dsiprouter,var/lib/mysql,${HOME},etc/dsiprouter,etc/kamailio,etc/rtpengine,etc/systemd/system,lib/systemd/system,etc/default}\ncp -rf /opt/dsiprouter/. ${CURR_BACKUP_DIR}/opt/dsiprouter/\ncp -rf /etc/kamailio/. ${CURR_BACKUP_DIR}/etc/kamailio/\ncp -rf /var/lib/mysql/. ${CURR_BACKUP_DIR}/var/lib/mysql/\ncp -f /etc/my.cnf ${CURR_BACKUP_DIR}/etc/ 2>/dev/null\ncp -rf /etc/mysql/. ${CURR_BACKUP_DIR}/etc/mysql/\ncp -f ${HOME}/.my.cnf ${CURR_BACKUP_DIR}/${HOME}/ 2>/dev/null\ncp -f /etc/systemd/system/{dnsmasq.service,kamailio.service,nginx.service,rtpengine.service} ${CURR_BACKUP_DIR}/etc/systemd/system/\ncp -f /lib/systemd/system/dsiprouter.service ${CURR_BACKUP_DIR}/lib/systemd/system/\ncp -f /etc/default/kamailio ${CURR_BACKUP_DIR}/etc/default/\nprintdbg \"files were backed up here: ${CURR_BACKUP_DIR}/\"\n\nprintdbg 'retrieving system info'\nexport DISTRO=$(getDistroName)\nexport DISTRO_VER=$(getDistroVer)\nexport DISTRO_MAJOR_VER=$(cut -d '.' -f 1 <<<\"$DISTRO_VER\")\nexport DISTRO_MINOR_VER=$(cut -s -d '.' -f 2 <<<\"$DISTRO_VER\")\n\nprintdbg 'retrieving current settings'\nMACHINE_ID=$(</etc/machine-id)\nexport DSIP_ID=$(hashCreds \"$MACHINE_ID\")\nexport DSIP_CLUSTER_ID=$(getConfigAttrib \"DSIP_CLUSTER_ID\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_CLUSTER_SYNC=$(getConfigAttrib \"DSIP_CLUSTER_SYNC\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_PROTO=$(getConfigAttrib \"DSIP_PROTO\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_PORT=$(getConfigAttrib \"DSIP_PORT\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_USERNAME=$(getConfigAttrib \"DSIP_USERNAME\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_API_PROTO=$(getConfigAttrib \"DSIP_API_PROTO\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_API_PORT=$(getConfigAttrib \"DSIP_API_PORT\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_PRIV_KEY=$(getConfigAttrib \"DSIP_PRIV_KEY\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_PID_FILE=$(getConfigAttrib \"DSIP_PID_FILE\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_UNIX_SOCK=$(getConfigAttrib \"DSIP_UNIX_SOCK\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_IPC_SOCK=$(getConfigAttrib \"DSIP_IPC_SOCK\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_LOG_LEVEL=$(getConfigAttrib \"DSIP_LOG_LEVEL\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_LOG_FACILITY=$(getConfigAttrib \"DSIP_LOG_FACILITY\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_SSL_KEY=$(getConfigAttrib \"DSIP_SSL_KEY\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_SSL_CERT=$(getConfigAttrib \"DSIP_SSL_CERT\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_SSL_CA=$(getConfigAttrib \"DSIP_SSL_CA\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_SSL_EMAIL=$(getConfigAttrib \"DSIP_SSL_EMAIL\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_CERTS_DIR=$(getConfigAttrib \"DSIP_CERTS_DIR\" \"/etc/dsiprouter/gui/settings.py\")\nexport ROLE=$(getConfigAttrib \"ROLE\" \"/etc/dsiprouter/gui/settings.py\")\nexport GUI_INACTIVE_TIMEOUT=$(getConfigAttrib \"GUI_INACTIVE_TIMEOUT\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_DB_HOST=$(getConfigAttrib \"KAM_DB_HOST\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_DB_DRIVER=$(getConfigAttrib \"KAM_DB_DRIVER\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_DB_TYPE=$(getConfigAttrib \"KAM_DB_TYPE\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_DB_PORT=$(getConfigAttrib \"KAM_DB_PORT\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_DB_NAME=$(getConfigAttrib \"KAM_DB_NAME\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_DB_USER=$(getConfigAttrib \"KAM_DB_USER\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_KAMCMD_PATH=$(getConfigAttrib \"KAM_KAMCMD_PATH\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_CFG_PATH=$(getConfigAttrib \"KAM_CFG_PATH\" \"/etc/dsiprouter/gui/settings.py\")\nexport KAM_TLSCFG_PATH=$(getConfigAttrib \"KAM_TLSCFG_PATH\" \"/etc/dsiprouter/gui/settings.py\")\nexport RTP_CFG_PATH=$(getConfigAttrib \"RTP_CFG_PATH\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLT_CARRIER=$(getConfigAttrib \"FLT_CARRIER\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLT_PBX=$(getConfigAttrib \"FLT_PBX\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLT_MSTEAMS=$(getConfigAttrib \"FLT_MSTEAMS\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLT_OUTBOUND=$(getConfigAttrib \"FLT_OUTBOUND\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLT_INBOUND=$(getConfigAttrib \"FLT_INBOUND\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLT_LCR_MIN=$(getConfigAttrib \"FLT_LCR_MIN\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLT_FWD_MIN=$(getConfigAttrib \"FLT_FWD_MIN\" \"/etc/dsiprouter/gui/settings.py\")\nexport DEFAULT_AUTH_DOMAIN=$(getConfigAttrib \"DEFAULT_AUTH_DOMAIN\" \"/etc/dsiprouter/gui/settings.py\")\nexport TELEBLOCK_GW_ENABLED=$(getConfigAttrib \"TELEBLOCK_GW_ENABLED\" \"/etc/dsiprouter/gui/settings.py\")\nexport TELEBLOCK_GW_IP=$(getConfigAttrib \"TELEBLOCK_GW_IP\" \"/etc/dsiprouter/gui/settings.py\")\nexport TELEBLOCK_GW_PORT=$(getConfigAttrib \"TELEBLOCK_GW_PORT\" \"/etc/dsiprouter/gui/settings.py\")\nexport TELEBLOCK_MEDIA_IP=$(getConfigAttrib \"TELEBLOCK_MEDIA_IP\" \"/etc/dsiprouter/gui/settings.py\")\nexport TELEBLOCK_MEDIA_PORT=$(getConfigAttrib \"TELEBLOCK_MEDIA_PORT\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLOWROUTE_ACCESS_KEY=$(getConfigAttrib \"FLOWROUTE_ACCESS_KEY\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLOWROUTE_SECRET_KEY=$(getConfigAttrib \"FLOWROUTE_SECRET_KEY\" \"/etc/dsiprouter/gui/settings.py\")\nexport FLOWROUTE_API_ROOT_URL=$(getConfigAttrib \"FLOWROUTE_API_ROOT_URL\" \"/etc/dsiprouter/gui/settings.py\")\nexport HOMER_ID=$(cat /etc/machine-id | hashCreds -l 4 | dd if=/dev/stdin of=/dev/stdout bs=1 count=8 2>/dev/null | hextoint)\nexport HOMER_HEP_HOST=$(getConfigAttrib \"HOMER_HEP_HOST\" \"/etc/dsiprouter/gui/settings.py\")\nexport HOMER_HEP_PORT=$(getConfigAttrib \"HOMER_HEP_PORT\" \"/etc/dsiprouter/gui/settings.py\")\nexport NETWORK_MODE='0'\nexport UPLOAD_FOLDER=$(getConfigAttrib \"UPLOAD_FOLDER\" \"/etc/dsiprouter/gui/settings.py\")\nexport MAIL_SERVER=$(getConfigAttrib \"MAIL_SERVER\" \"/etc/dsiprouter/gui/settings.py\")\nexport MAIL_PORT=$(getConfigAttrib \"MAIL_PORT\" \"/etc/dsiprouter/gui/settings.py\")\nexport MAIL_USE_TLS=$(getConfigAttrib \"MAIL_USE_TLS\" \"/etc/dsiprouter/gui/settings.py\")\nexport MAIL_USERNAME=$(getConfigAttrib \"MAIL_USERNAME\" \"/etc/dsiprouter/gui/settings.py\")\nexport MAIL_ASCII_ATTACHMENTS=$(getConfigAttrib \"MAIL_ASCII_ATTACHMENTS\" \"/etc/dsiprouter/gui/settings.py\")\nexport MAIL_DEFAULT_SENDER=$(getConfigAttrib \"MAIL_DEFAULT_SENDER\" \"/etc/dsiprouter/gui/settings.py\")\nexport MAIL_DEFAULT_SUBJECT=$(getConfigAttrib \"MAIL_DEFAULT_SUBJECT\" \"/etc/dsiprouter/gui/settings.py\")\nexport BACKUP_FOLDER=$(getConfigAttrib \"BACKUP_FOLDER\" \"/etc/dsiprouter/gui/settings.py\")\nTRANSNEXUS_AUTHSERVICE_ENABLED=$(getConfigAttrib \"TRANSNEXUS_AUTHSERVICE_ENABLED\" \"/etc/dsiprouter/gui/settings.py\")\nTRANSNEXUS_VERIFYSERVICE_ENABLED=$(getConfigAttrib \"TRANSNEXUS_VERIFYSERVICE_ENABLED\" \"/etc/dsiprouter/gui/settings.py\")\nTRANSNEXUS_AUTHSERVICE_HOST=$(getConfigAttrib \"TRANSNEXUS_AUTHSERVICE_HOST\" \"/etc/dsiprouter/gui/settings.py\")\nTRANSNEXUS_VERIFYSERVICE_HOST=$(getConfigAttrib \"TRANSNEXUS_VERIFYSERVICE_HOST\" \"/etc/dsiprouter/gui/settings.py\")\nTRANSNEXUS_LICENSE_KEY=$(getConfigAttrib \"TRANSNEXUS_LICENSE_KEY\" \"/etc/dsiprouter/gui/settings.py\")\nif [[ -n \"$TRANSNEXUS_LICENSE_KEY\" ]]; then\n    DSIP_TRANSNEXUS_LICENSE=$(encryptNewCreds \"${TRANSNEXUS_LICENSE_KEY}${MACHINE_ID}\")\nelse\n    if (( $TRANSNEXUS_AUTHSERVICE_ENABLED == 1 )) || (( $TRANSNEXUS_VERIFYSERVICE_ENABLED == 1 )); then\n        printwarn 'transnexus support requires a license, add a valid license and activate via the GUI'\n    fi\n    TRANSNEXUS_AUTHSERVICE_ENABLED=0\n    TRANSNEXUS_VERIFYSERVICE_ENABLED=0\nfi\nSTIR_SHAKEN_PREFIX_A=$(getConfigAttrib \"STIR_SHAKEN_PREFIX_A\" \"/etc/dsiprouter/gui/settings.py\")\nSTIR_SHAKEN_PREFIX_B=$(getConfigAttrib \"STIR_SHAKEN_PREFIX_B\" \"/etc/dsiprouter/gui/settings.py\")\nSTIR_SHAKEN_PREFIX_C=$(getConfigAttrib \"STIR_SHAKEN_PREFIX_C\" \"/etc/dsiprouter/gui/settings.py\")\nSTIR_SHAKEN_PREFIX_INVALID=$(getConfigAttrib \"STIR_SHAKEN_PREFIX_INVALID\" \"/etc/dsiprouter/gui/settings.py\")\nSTIR_SHAKEN_BLOCK_INVALID=$(getConfigAttrib \"STIR_SHAKEN_BLOCK_INVALID\" \"/etc/dsiprouter/gui/settings.py\")\nSTIR_SHAKEN_CERT_URL=$(getConfigAttrib \"STIR_SHAKEN_CERT_URL\" \"/etc/dsiprouter/gui/settings.py\")\nSTIR_SHAKEN_KEY_PATH=$(getConfigAttrib \"STIR_SHAKEN_KEY_PATH\" \"/etc/dsiprouter/gui/settings.py\")\nexport DSIP_DOCS_DIR=\"${DSIP_PROJECT_DIR}/docs\"\nexport ROOT_DB_USER=$(getConfigAttrib \"ROOT_DB_USER\" \"/etc/dsiprouter/gui/settings.py\")\nexport ROOT_DB_NAME=$(getConfigAttrib \"ROOT_DB_NAME\" \"/etc/dsiprouter/gui/settings.py\")\nexport LOAD_SETTINGS_FROM=$(getConfigAttrib \"LOAD_SETTINGS_FROM\" \"/etc/dsiprouter/gui/settings.py\")\n\nprintdbg 'updating select settings'\nexport DSIP_API_TOKEN=$(decryptOldSetting \"DSIP_API_TOKEN\")\nexport DSIP_IPC_PASS=$(decryptOldSetting \"DSIP_IPC_PASS\")\nexport KAM_DB_PASS=$(decryptOldSetting \"KAM_DB_PASS\")\nexport MAIL_PASSWORD=$(decryptOldSetting \"MAIL_PASSWORD\")\nexport ROOT_DB_PASS=$(decryptOldSetting \"ROOT_DB_PASS\")\n\nprintwarn 'dSIPRouter admin password hash can not be undone, generating new one'\nexport DSIP_PASSWORD=$(urandomChars 64)\nprintdbg \"temporary password: $DSIP_PASSWORD\"\n\nDSIP_PASSWORD_HASH=$(hashCreds \"$DSIP_PASSWORD\")\nDSIP_API_TOKEN_CIPHERTEXT=$(encryptNewCreds \"$DSIP_API_TOKEN\")\nDSIP_IPC_PASS_CIPHERTEXT=$(encryptNewCreds \"$DSIP_IPC_PASS\")\nKAM_DB_PASS_CIPHERTEXT=$(encryptNewCreds \"$KAM_DB_PASS\")\nMAIL_PASSWORD_CIPHERTEXT=$(encryptNewCreds \"$MAIL_PASSWORD\")\nROOT_DB_PASS_CIPHERTEXT=$(encryptNewCreds \"$ROOT_DB_PASS\")\n\nexport INTERNAL_IP_ADDR=$(getInternalIP -4)\nexport INTERNAL_IP_NET=$(getInternalCIDR -4)\nexport INTERNAL_IP6_ADDR=$(getInternalIP -6)\nexport INTERNAL_IP_NET6=$(getInternalCIDR -6)\nEXTERNAL_IP_ADDR=$(getExternalIP -4)\nexport EXTERNAL_IP_ADDR=${EXTERNAL_IP_ADDR:-$INTERNAL_IP_ADDR}\nEXTERNAL_IP6_ADDR=$(getExternalIP -6)\nexport EXTERNAL_IP6_ADDR=${EXTERNAL_IP6_ADDR:-$INTERNAL_IP6_ADDR}\nexport INTERNAL_FQDN=$(getInternalFQDN)\nexport EXTERNAL_FQDN=$(getExternalFQDN)\nif [[ -z \"$EXTERNAL_FQDN\" ]] || ! checkConn \"$EXTERNAL_FQDN\"; then\n    export EXTERNAL_FQDN=\"$INTERNAL_FQDN\"\nfi\n\nprintdbg 'migrating database schema'\n(\ncat <<'EOF'\nALTER TABLE address\n  MODIFY tag VARCHAR(255) NOT NULL DEFAULT '';\n\nALTER TABLE dispatcher\n  MODIFY description VARCHAR(255) NOT NULL DEFAULT '';\n\nALTER TABLE dr_gateways\n  MODIFY pri_prefix VARCHAR(64) NOT NULL DEFAULT '',\n  MODIFY attrs VARCHAR(255) NOT NULL DEFAULT '',\n  MODIFY description VARCHAR(255) NOT NULL DEFAULT '';\n\nALTER TABLE dr_gw_lists\n  MODIFY description VARCHAR(255) NOT NULL DEFAULT '';\n\nALTER TABLE dr_rules\n  MODIFY description VARCHAR(255) NOT NULL DEFAULT '';\n\nALTER TABLE dsip_cdrinfo\n  MODIFY email VARCHAR(255) NOT NULL DEFAULT '';\n\nALTER TABLE subscriber\n  ADD IF NOT EXISTS  email_address VARCHAR(128) NOT NULL DEFAULT '',\n  ADD IF NOT EXISTS  rpid          VARCHAR(128) NOT NULL DEFAULT '';\n\nALTER TABLE `acc`\n  MODIFY `from_tag` VARCHAR (128) NOT NULL DEFAULT '',\n  MODIFY `to_tag` VARCHAR (128) NOT NULL DEFAULT '',\n  MODIFY `callid` VARCHAR (255) NOT NULL DEFAULT '',\n  MODIFY `sip_reason` VARCHAR (255) NOT NULL DEFAULT '',\n  MODIFY `time` DATETIME NOT NULL DEFAULT NOW(),\n  MODIFY `dst_ouser` VARCHAR (128) NOT NULL DEFAULT '',\n  MODIFY `dst_user` VARCHAR (128) NOT NULL DEFAULT '',\n  MODIFY `dst_domain` VARCHAR (255) NOT NULL DEFAULT '',\n  MODIFY `src_user` VARCHAR (128) NOT NULL DEFAULT '',\n  MODIFY `src_domain` VARCHAR (255) NOT NULL DEFAULT '',\n  MODIFY `src_gwgroupid` VARCHAR (10) NOT NULL DEFAULT '',\n  MODIFY `dst_gwgroupid` VARCHAR (10) NOT NULL DEFAULT '';\n\nALTER TABLE `cdrs`\n  MODIFY `cdr_id`          BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,\n  MODIFY `src_username`    VARCHAR(128)        NOT NULL DEFAULT '',\n  MODIFY `src_domain`      VARCHAR(255)        NOT NULL DEFAULT '',\n  MODIFY `dst_username`    VARCHAR(128)        NOT NULL DEFAULT '',\n  MODIFY `dst_domain`      VARCHAR(255)        NOT NULL DEFAULT '',\n  MODIFY `dst_ousername`   VARCHAR(128)        NOT NULL DEFAULT '',\n  MODIFY `call_start_time` DATETIME            NOT NULL,\n  MODIFY `sip_call_id`     VARCHAR(255)        NOT NULL DEFAULT '',\n  MODIFY `created`         DATETIME            NOT NULL DEFAULT NOW(),\n  MODIFY `src_gwgroupid`   VARCHAR(10)         NOT NULL DEFAULT '',\n  MODIFY `dst_gwgroupid`   VARCHAR(10)         NOT NULL DEFAULT '';\n\nDROP PROCEDURE IF EXISTS `kamailio_cdrs`;\nDELIMITER //\nCREATE PROCEDURE `kamailio_cdrs`()\nBEGIN\n  DECLARE done INT DEFAULT 0;\n  DECLARE bye_record INT DEFAULT 0;\n  DECLARE v_src_user,v_src_domain,v_dst_user,v_dst_domain,v_callid,v_from_tag,\n    v_to_tag,v_src_ip,v_calltype VARCHAR(255);\n  DECLARE v_src_gwgroupid, v_dst_gwgroupid INT(11);\n  DECLARE v_inv_time, v_bye_time DATETIME;\n  DECLARE inv_cursor CURSOR FOR\n    SELECT src_user,\n           src_domain,\n           dst_user,\n           dst_domain,\n           time,\n           callid,\n           from_tag,\n           to_tag,\n           src_ip,\n           calltype,\n           src_gwgroupid,\n           dst_gwgroupid\n    FROM acc\n    WHERE method = 'INVITE'\n      AND cdr_id = '0';\n  DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;\n  OPEN inv_cursor;\n  REPEAT\n    FETCH inv_cursor INTO v_src_user, v_src_domain, v_dst_user, v_dst_domain,\n      v_inv_time, v_callid, v_from_tag, v_to_tag, v_src_ip, v_calltype,\n      v_src_gwgroupid, v_dst_gwgroupid;\n    IF NOT done THEN\n      SET bye_record = 0;\n      SELECT 1, time\n      INTO bye_record, v_bye_time\n      FROM acc\n      WHERE method = 'BYE'\n        AND callid = v_callid\n        AND ((from_tag = v_from_tag\n        AND to_tag = v_to_tag)\n        OR (from_tag = v_to_tag AND to_tag = v_from_tag))\n      ORDER BY time ASC\n      LIMIT 1;\n      IF bye_record = 1 THEN\n        INSERT INTO cdrs (src_username, src_domain, dst_username, dst_domain,\n                          call_start_time, duration, sip_call_id, sip_from_tag,\n                          sip_to_tag, src_ip, created, calltype, src_gwgroupid, dst_gwgroupid)\n        VALUES (v_src_user, v_src_domain, v_dst_user, v_dst_domain, v_inv_time,\n                UNIX_TIMESTAMP(v_bye_time) - UNIX_TIMESTAMP(v_inv_time),\n                v_callid, v_from_tag, v_to_tag, v_src_ip, NOW(), v_calltype,\n                v_src_gwgroupid, v_dst_gwgroupid);\n        UPDATE acc\n        SET cdr_id=LAST_INSERT_ID()\n        WHERE callid = v_callid\n          AND from_tag = v_from_tag\n          AND to_tag = v_to_tag;\n      END IF;\n      SET done = 0;\n    END IF;\n  UNTIL done END REPEAT;\nEND //\nDELIMITER ;\n\nDROP PROCEDURE IF EXISTS `kamailio_rating`;\nDELIMITER //\nCREATE PROCEDURE `kamailio_rating`(`rgroup` VARCHAR(64))\nBEGIN\n  DECLARE done, rate_record, vx_cost INT DEFAULT 0;\n  DECLARE v_cdr_id BIGINT DEFAULT 0;\n  DECLARE v_duration, v_rate_unit, v_time_unit INT DEFAULT 0;\n  DECLARE v_dst_username VARCHAR(255);\n  DECLARE cdrs_cursor CURSOR FOR SELECT cdr_id, dst_username, duration\n                                 FROM cdrs\n                                 WHERE rated = 0;\n  DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;\n  OPEN cdrs_cursor;\n  REPEAT\n    FETCH cdrs_cursor INTO v_cdr_id, v_dst_username, v_duration;\n    IF NOT done THEN\n      SET rate_record = 0;\n      SELECT 1, rate_unit, time_unit\n      INTO rate_record, v_rate_unit, v_time_unit\n      FROM billing_rates\n      WHERE rate_group = rgroup\n        AND v_dst_username LIKE CONCAT(prefix, '%')\n      ORDER BY prefix DESC\n      LIMIT 1;\n      IF rate_record = 1 THEN\n        SET vx_cost = v_rate_unit * CEIL(v_duration / v_time_unit);\n        UPDATE cdrs SET rated=1, cost=vx_cost WHERE cdr_id = v_cdr_id;\n      END IF;\n      SET done = 0;\n    END IF;\n  UNTIL done END REPEAT;\nEND //\nDELIMITER ;\nEOF\n) | sqlAsTransaction --user=\"$ROOT_DB_USER\" --password=\"$ROOT_DB_PASS\" --host=\"$KAM_DB_HOST\" --port=\"$KAM_DB_PORT\"\n\nif (( $? != 0 )); then\n    printerr 'Failed merging DB schema'\n    exit 1\nfi\n\nif (( ${BOOTSTRAPPING_UPGRADE:-0} == 1 )); then\n    PROJECT_DSIP_DEFAULTS_DIR='/tmp/dsiprouter/kamailio/defaults'\nelse\n    PROJECT_DSIP_DEFAULTS_DIR='/opt/dsiprouter/kamailio/defaults'\nfi\nperl -e \"\\$hlen='$HASHED_CREDS_ENCODED_MAX_LEN'; \\$clen='$AESCTR_CREDS_ENCODED_MAX_LEN';\" \\\n    -pe 's%\\@HASHED_CREDS_ENCODED_MAX_LEN%$hlen%g; s%\\@AESCTR_CREDS_ENCODED_MAX_LEN%$clen%g;' \\\n    ${PROJECT_DSIP_DEFAULTS_DIR}/dsip_settings.sql |\n    mysql -s -N --user=\"$ROOT_DB_USER\" --password=\"$ROOT_DB_PASS\" --host=\"$KAM_DB_HOST\" --port=\"$KAM_DB_PORT\" \"$KAM_DB_NAME\"\n\nif (( $? != 0 )); then\n    printerr 'Failed merging DB schema'\n    exit 1\nfi\n\nprintdbg 'configuring dsiprouter GUI'\nif (( ${BOOTSTRAPPING_UPGRADE:-0} == 1 )); then\n    # a few stragglers that need copied over\n    cp -rf /opt/dsiprouter/gui/modules/fusionpbx/certs/. /tmp/dsiprouter/gui/modules/fusionpbx/certs/\n    # use the bootstrap repo instead cloning again\n    rm -rf /opt/dsiprouter\n    mv -f /tmp/dsiprouter /opt/dsiprouter\nelse\n    # fresh repo coming up\n    rm -rf /opt/dsiprouter\n    git clone --depth 1 -c advice.detachedHead=false -b v0.721-rel https://github.com/dOpensource/dsiprouter.git /opt/dsiprouter\nfi\nexport DSIP_PROJECT_DIR=/opt/dsiprouter\n\nprintdbg 'installing python dependencies for the GUI'\npython3 -m pip install -U Flask~=2.2.0 psycopg2_binary requests SQLAlchemy~=2.0 Werkzeug~=2.0\nif cmdExists 'apt-get'; then\n    apt-get remove -y *certbot*\n    apt-get install -y python3-venv\nelif cmdExists 'dnf'; then\n    dnf remove -y *certbot*\n    dnf install -y python3-virtualenv\nelif cmdExists 'yum'; then\n    yum remove -y *certbot*\n    yum install -y python3-virtualenv\nfi\npython3 -m pip remove certbot 2>/dev/null\npython3 -m pip install -U acme josepy\npython3 -m venv /opt/certbot/\n/opt/certbot/bin/pip install --upgrade pip\n/opt/certbot/bin/pip install certbot\nln -sf /opt/certbot/bin/certbot /usr/bin/certbot\n\nprintdbg 'generating dynamic config files for the GUI'\ndsiprouter configuredsip &&\nsetConfigAttrib 'DSIP_USERNAME' \"$DSIP_USERNAME\" /etc/dsiprouter/gui/settings.py -q &&\nsetConfigAttrib 'DSIP_PASSWORD' \"$DSIP_PASSWORD_HASH\" /etc/dsiprouter/gui/settings.py -qb &&\nsetConfigAttrib 'DSIP_API_TOKEN' \"$DSIP_API_TOKEN_CIPHERTEXT\" /etc/dsiprouter/gui/settings.py -qb &&\nsetConfigAttrib 'DSIP_IPC_PASS' \"$DSIP_IPC_PASS_CIPHERTEXT\" /etc/dsiprouter/gui/settings.py -qb &&\nsetConfigAttrib 'KAM_DB_USER' \"$KAM_DB_USER\" /etc/dsiprouter/gui/settings.py -q &&\nsetConfigAttrib 'KAM_DB_PASS' \"$KAM_DB_PASS_CIPHERTEXT\" /etc/dsiprouter/gui/settings.py -qb &&\nsetConfigAttrib 'KAM_DB_HOST' \"$KAM_DB_HOST\" /etc/dsiprouter/gui/settings.py -q &&\nsetConfigAttrib 'KAM_DB_PORT' \"$KAM_DB_PORT\" /etc/dsiprouter/gui/settings.py -q &&\nsetConfigAttrib 'KAM_DB_NAME' \"$KAM_DB_NAME\" /etc/dsiprouter/gui/settings.py -q &&\nsetConfigAttrib 'MAIL_USERNAME' \"$MAIL_USERNAME\" /etc/dsiprouter/gui/settings.py -q &&\nsetConfigAttrib 'MAIL_PASSWORD' \"$MAIL_PASSWORD_CIPHERTEXT\" /etc/dsiprouter/gui/settings.py -qb &&\nsetConfigAttrib 'ROOT_DB_USER' \"$ROOT_DB_USER\" /etc/dsiprouter/gui/settings.py -q &&\n{\n    if ! grep -q -oP '(b\"\"\".*\"\"\"|'\"b'''.*'''\"'|b\".*\"|'\"b'.*')\" <<<\"$ROOT_DB_PASS\"; then\n        setConfigAttrib 'ROOT_DB_PASS' \"$ROOT_DB_PASS\" /etc/dsiprouter/gui/settings.py -q\n    else\n        setConfigAttrib 'ROOT_DB_PASS' \"$ROOT_DB_PASS_CIPHERTEXT\" /etc/dsiprouter/gui/settings.py -qb\n    fi\n} &&\nsetConfigAttrib 'ROOT_DB_NAME' \"$ROOT_DB_NAME\" /etc/dsiprouter/gui/settings.py -q &&\n(\n    setConfigAttrib 'TRANSNEXUS_AUTHSERVICE_ENABLED' \"$TRANSNEXUS_AUTHSERVICE_ENABLED\" /etc/dsiprouter/gui/settings.py || exit 1\n    setConfigAttrib 'TRANSNEXUS_VERIFYSERVICE_ENABLED' \"$TRANSNEXUS_VERIFYSERVICE_ENABLED\" /etc/dsiprouter/gui/settings.py || exit 1\n    setConfigAttrib 'TRANSNEXUS_AUTHSERVICE_HOST' \"$TRANSNEXUS_AUTHSERVICE_HOST\" /etc/dsiprouter/gui/settings.py -q || exit 1\n    setConfigAttrib 'TRANSNEXUS_VERIFYSERVICE_HOST' \"$TRANSNEXUS_VERIFYSERVICE_HOST\" /etc/dsiprouter/gui/settings.py -q || exit 1\n    # if transnexus license is present we need to reactivate it on the new server\n    [[ -n \"$DSIP_TRANSNEXUS_LICENSE\" ]] && {\n        RESPONSE=$(\n            curl -sk -u \"ck_068f510a518ff5ecf1cbdcbc7db7f9bac2331613:cs_5ae2f3decfa59f427a59b41f2e41459d18023dd7\" \\\n                \"https://dopensource.com/wp-json/lmfwc/v2/licenses/${TRANSNEXUS_LICENSE_KEY}\"\n        )\n        if (( $? != 0 )); then\n            printerr 'could not contact license server, check your internet connection and try again'\n            exit 1\n        fi\n        if [[ \"$(jq -r '.success' <<<\"$RESPONSE\")\" != \"true\" ]]; then\n                printerr 'invalid transnexus license can not be migrated'\n                printwarn 'licenses can be purchased here: https://dopensource.com/product/transnexus-clearip-module/'\n                exit 0\n        fi\n        NOW=$(date -u +'%s')\n        if [[ \"$(jq -r '.data.expiresAt' <<<\"$RESPONSE\")\" != \"null\" ]]; then\n            EXPIRES=$(\n                jq -r '.data.expiresAt' <<<\"$RESPONSE\" \\\n                | date -u -f /dev/stdin +'%s'\n            )\n        else\n            EXPIRES=$(\n                jq -r '.data.createdAt' <<<\"$RESPONSE\" \\\n                | date -u -f /dev/stdin +\"%Y-%m-%d +$(jq -r '.data.validFor' <<<\"$RESPONSE\")day %H:%M:%S\" \\\n                | date -u -f /dev/stdin +'%s'\n            )\n        fi\n        if (( $(jq -r '.data.status' <<<\"$RESPONSE\") == 3 )) && (( $(jq -r '.data.timesActivated' <<<\"$RESPONSE\") > 0 )) && (( $EXPIRES > $NOW )); then\n            setConfigAttrib 'DSIP_TRANSNEXUS_LICENSE' \"$DSIP_TRANSNEXUS_LICENSE\" /etc/dsiprouter/gui/settings.py -qb\n        else\n            RESPONSE=$(\n                curl -sk -u \"ck_068f510a518ff5ecf1cbdcbc7db7f9bac2331613:cs_5ae2f3decfa59f427a59b41f2e41459d18023dd7\" \\\n                    \"https://dopensource.com/wp-json/lmfwc/v2/licenses/activate/${TRANSNEXUS_LICENSE_KEY}\"\n                curl -skf -X PUT -H \"Authorization: Bearer ${DSIP_API_TOKEN}\" -H 'Content-Type: application/json' \\\n                    -d \"{\\\"license_key\\\":\\\"${TRANSNEXUS_LICENSE_KEY}\\\"}\" 'https://localhost:5000/api/v1/licensing/activate'\n            )\n            if (( $? != 0 )) || [[ \"$(jq -r '.success' <<<\"$RESPONSE\")\" != \"true\" ]]; then\n                printerr 'failed to activate transnexus license'\n                printwarn 'contact dOpenSource to migrate your license: https://dopensource.com/'\n                exit 0\n            fi\n            setConfigAttrib 'DSIP_TRANSNEXUS_LICENSE' \"$DSIP_TRANSNEXUS_LICENSE\" /etc/dsiprouter/gui/settings.py -qb\n        fi\n    }\n    exit 0\n) &&\n(\n    setConfigAttrib 'STIR_SHAKEN_PREFIX_A' \"$STIR_SHAKEN_PREFIX_A\" /etc/dsiprouter/gui/settings.py -q || exit 1\n    setConfigAttrib 'STIR_SHAKEN_PREFIX_B' \"$STIR_SHAKEN_PREFIX_B\" /etc/dsiprouter/gui/settings.py -q || exit 1\n    setConfigAttrib 'STIR_SHAKEN_PREFIX_C' \"$STIR_SHAKEN_PREFIX_C\" /etc/dsiprouter/gui/settings.py -q || exit 1\n    setConfigAttrib 'STIR_SHAKEN_PREFIX_INVALID' \"$STIR_SHAKEN_PREFIX_INVALID\" /etc/dsiprouter/gui/settings.py -q || exit 1\n    setConfigAttrib 'STIR_SHAKEN_BLOCK_INVALID' \"$STIR_SHAKEN_BLOCK_INVALID\" /etc/dsiprouter/gui/settings.py || exit 1\n    setConfigAttrib 'STIR_SHAKEN_CERT_URL' \"$STIR_SHAKEN_CERT_URL\" /etc/dsiprouter/gui/settings.py -q || exit 1\n    setConfigAttrib 'STIR_SHAKEN_KEY_PATH' \"$STIR_SHAKEN_KEY_PATH\" /etc/dsiprouter/gui/settings.py -q || exit 1\n) &&\n{\n    # let user know how to upgrade ms teams license if valid\n    if [[ $(curl -s -d '{\"method\": \"dsiprouter.health_check\", \"jsonrpc\": \"2.0\", \"id\": 1}' 'http://localhost:5060/api/kamailio' | jq -r '.result' 2>/dev/null) == \"Health Check Succeeded\" ]]; then\n        printwarn 'your msteams license can not be converted automatically'\n        printwarn 'contact dOpenSource to migrate your license: https://dopensource.com/'\n    fi\n} &&\nprintdbg 'successfully generated new settings file' ||\n{\n    printerr 'failed generating new settings file'\n    exit 1\n}\n\nif [[ \"$LOAD_SETTINGS_FROM\" == \"db\" ]]; then\n    printdbg 'updating dsip_settings table, the GUI will be restarted multiple times...'\n    setConfigAttrib 'LOAD_SETTINGS_FROM' 'file' /etc/dsiprouter/gui/settings.py &&\n    systemctl restart dsiprouter &&\n    setConfigAttrib 'LOAD_SETTINGS_FROM' 'db' /etc/dsiprouter/gui/settings.py &&\n    systemctl restart dsiprouter ||\n    {\n        printerr 'failed updating dsip_settings DB table'\n        exit 1\n    }\nelse\n    printdbg 'the dsip_settings table will be updated when the GUI service is restarted..'\nfi\n\nprintdbg 'generating documentation for the GUI'\n(\n    cd ${DSIP_PROJECT_DIR}/docs\n    make html >/dev/null 2>&1\n)\n\nprintdbg 'generating documentation for the CLI'\ncp -f ${DSIP_PROJECT_DIR}/resources/man/dsiprouter.1 /usr/share/man/man1/\ngzip -f /usr/share/man/man1/dsiprouter.1\nmandb\ncp -f ${DSIP_PROJECT_DIR}/dsiprouter/dsip_completion.sh /etc/bash_completion.d/dsiprouter\n\nprintdbg 'upgrading systemd service configurations'\n# move kamailio defaults file to the new name\nmv -f /etc/default/kamailio /etc/default/kamailio.conf\n# update the default memory allocation if needed\n# currently the minimum system RAM is 256MB + 64MB + 512MB = 832MB\nif (( $(awk '/MemTotal/ { printf \"%d \\n\", $2/1024 }' /proc/meminfo) < 832 )); then\n    printwarn 'system has less than 832MB RAM, not updating kamailio default memory allocations'\nelse\n    PKG_MEM=$(grep -m 1 -oP 'PKG_MEMORY\\=\\K.*' /etc/default/kamailio.conf)\n    if (( $PKG_MEM < 64 )); then\n        perl -i -pe 's%(PKG_MEM\\=).*%${1}64%' /etc/default/kamailio.conf\n    fi\n    SHM_MEM=$(grep -m 1 -oP 'SHM_MEMORY\\=\\K.*' /etc/default/kamailio.conf)\n    if (( $SHM_MEM < 512 )); then\n        perl -i -pe 's%(SHM_MEMORY\\=).*%${1}512%' /etc/default/kamailio.conf\n    fi\nfi\n\n    case \"$DISTRO\" in\n        debian|ubuntu)\n            cat << 'EOF' >/etc/systemd/system/dnsmasq.service\n[Unit]\nDescription=dnsmasq - A lightweight DHCP and caching DNS server\nRequires=basic.target network.target\nAfter=network.target network-online.target basic.target\nWants=nss-lookup.target\nBefore=nss-lookup.target\nDefaultDependencies=no\n\n[Service]\nType=forking\nPIDFile=/run/dnsmasq/dnsmasq.pid\nEnvironment='RUN_DIR=/run/dnsmasq'\nEnvironment='IGNORE_RESOLVCONF=yes'\n# make sure everything is setup correctly before starting\nExecStartPre=!-/usr/bin/dsiprouter chown -dnsmasq\nExecStartPre=/usr/sbin/dnsmasq --test\n# We run dnsmasq via the /etc/init.d/dnsmasq script which acts as a\n# wrapper picking up extra configuration files and then execs dnsmasq\n# itself, when called with the \"systemd-exec\" function.\nExecStart=/etc/init.d/dnsmasq systemd-exec\n# The systemd-*-resolvconf functions configure (and deconfigure)\n# resolvconf to work with the dnsmasq DNS server. They're called like\n# this to get correct error handling (ie don't start-resolvconf if the\n# dnsmasq daemon fails to start.\nExecStartPost=/etc/init.d/dnsmasq systemd-start-resolvconf\nExecStop=/etc/init.d/dnsmasq systemd-stop-resolvconf\nExecReload=/bin/kill -HUP $MAINPID\n\n[Install]\nWantedBy=multi-user.target\nEOF\n            ;;\n        almalinux|rocky)\n            cat << 'EOF' >/etc/systemd/system/dnsmasq.service\n[Unit]\nDescription=dnsmasq - A lightweight DHCP and caching DNS server\nRequires=basic.target network.target\nAfter=network.target network-online.target basic.target\nBefore=multi-user.target\nDefaultDependencies=no\n\n[Service]\nType=simple\nPIDFile=/run/dnsmasq/dnsmasq.pid\nEnvironment='RUN_DIR=/run/dnsmasq'\n# make sure everything is setup correctly before starting\nExecStartPre=!-/usr/bin/dsiprouter chown -dnsmasq\nExecStartPre=/usr/sbin/dnsmasq --test\nExecStart=/usr/sbin/dnsmasq -k\nExecReload=/bin/kill -HUP $MAINPID\n\n[Install]\nWantedBy=multi-user.target\nEOF\n            ;;\n        amzn|rhel)\n            cat << 'EOF' >/etc/systemd/system/dnsmasq.service\n[Unit]\nDescription=dnsmasq - A lightweight DHCP and caching DNS server\nRequires=basic.target network.target\nAfter=network.target network-online.target basic.target\nBefore=multi-user.target\nDefaultDependencies=no\n\n[Service]\nType=simple\nPermissionsStartOnly=true\nPIDFile=/run/dnsmasq/dnsmasq.pid\nEnvironment='RUN_DIR=/run/dnsmasq'\n# make sure everything is setup correctly before starting\nExecStartPre=/usr/bin/dsiprouter chown -dnsmasq\nExecStartPre=/usr/sbin/dnsmasq --test\nExecStart=/usr/sbin/dnsmasq -k\nExecReload=/bin/kill -HUP $MAINPID\n\n[Install]\nWantedBy=multi-user.target\nEOF\n            ;;\n    esac\n\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]]; then\n    SVC_FILE=$(grep -oP \"kamailio-v[0-9]+\\.service\" ${DSIP_PROJECT_DIR}/kamailio/${DISTRO}/${DISTRO_MAJOR_VER}.sh)\n    cp -f ${DSIP_PROJECT_DIR}/kamailio/systemd/$SVC_FILE /etc/systemd/system/kamailio.service\nfi\n\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.nginxinstalled\" ]]; then\n    SVC_FILE=$(grep -oP \"nginx-v[0-9]+\\.service\" ${DSIP_PROJECT_DIR}/nginx/${DISTRO}/${DISTRO_MAJOR_VER}.sh)\n    cp -f ${DSIP_PROJECT_DIR}/nginx/systemd/$SVC_FILE /etc/systemd/system/nginx.service\nfi\n\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]]; then\n    SVC_FILE=$(grep -oP \"dsiprouter-v[0-9]+\\.service\" ${DSIP_PROJECT_DIR}/dsiprouter/${DISTRO}/${DISTRO_MAJOR_VER}.sh)\n    cp -f ${DSIP_PROJECT_DIR}/dsiprouter/systemd/$SVC_FILE /lib/systemd/system/dsiprouter.service\nfi\n\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\" ]]; then\n    SVC_FILE=$(grep -m 1 -oP \"rtpengine-v[0-9]+\\.service\" ${DSIP_PROJECT_DIR}/rtpengine/${DISTRO}/install.sh)\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/systemd/$SVC_FILE /etc/systemd/system/rtpengine.service\nfi\n\nDSIP_SYSTEM_CONFIG_DIR=\"/etc/dsiprouter\"\nDSIP_CERTS_DIR=\"${DSIP_SYSTEM_CONFIG_DIR}/certs\"\ncp -f ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.service /etc/systemd/system/nginx-watcher.service\nperl -p \\\n    -e \"s%PathChanged\\=.*%PathChanged=${DSIP_CERTS_DIR}/%;\" \\\n    ${DSIP_PROJECT_DIR}/nginx/systemd/nginx-watcher.path >/etc/systemd/system/nginx-watcher.path\nchmod 644 /etc/systemd/system/nginx-watcher.service\nchmod 644 /etc/systemd/system/nginx-watcher.path\n\nsystemctl daemon-reload\n\n# generate mysql service if needed\nreconfigureMysqlSystemdService\n\nprintdbg 'upgrading kamailio configs'\ndsiprouter configurekam\n\nprintdbg 'upgrading rtpengine configs'\ndsiprouter updatertpconfig\n\nprintdbg 'upgrading dnsmasq configs'\ndsiprouter updatednsconfig\n\nprintdbg 'updating file permissions'\ndsiprouter chown\n\nprintdbg 'restarting services'\nsystemctl restart dnsmasq\nsystemctl restart kamailio\nsystemctl restart nginx\nsystemctl restart dsiprouter\nsystemctl restart rtpengine\n\nexit 0\n"
  },
  {
    "path": "resources/upgrade/v0.721/settings.json",
    "content": "{\n  \"version\": \"0.721\",\n  \"depends\": \"0.70\",\n  \"install_location\": \"/opt/dsiprouter\",\n  \"dsiprouter\": [\n    \"migrate.sh\"\n  ]\n}\n"
  },
  {
    "path": "resources/upgrade/v0.73/scripts/bootstrap.sh",
    "content": "#!/usr/bin/env bash\n\nexport BOOTSTRAPPING_UPGRADE=1\nexport BOOTSTRAP_DIR='/tmp/dsiprouter'\nTAG_NAME='v0.73-rel'\nREPO_URL='https://github.com/dOpensource/dsiprouter.git'\n[[ -e \"$BOOTSTRAP_DIR\" ]] && rm -rf \"$BOOTSTRAP_DIR\"\ngit clone --depth 1 -c advice.detachedHead=false -b \"$TAG_NAME\" \"$REPO_URL\" \"$BOOTSTRAP_DIR\"\n${BOOTSTRAP_DIR}/dsiprouter.sh upgrade -rel v0.73\nRET=$?\nrm -rf ${BOOTSTRAP_DIR}\nexit $RET\n"
  },
  {
    "path": "resources/upgrade/v0.73/scripts/migrate.sh",
    "content": "#!/usr/bin/env bash\n\n# where the new project files were downloaded\nNEW_PROJECT_DIR=${NEW_PROJECT_DIR:-/tmp/dsiprouter}\n# whether or not this was called from the GUI\nRUN_FROM_GUI=${RUN_FROM_GUI:-0}\n# the backup directory set by dsiprouter.sh\nCURR_BACKUP_DIR=${CURR_BACKUP_DIR:-\"/var/backups/dsiprouter/$(date '+%s')\"}\n\n# set project dir where previous repo was located\nexport DSIP_PROJECT_DIR='/opt/dsiprouter'\n# import dsip_lib utility / shared functions (we are using old functions on purpose here)\n# WARNING: from here on we are explicitly using the OLD definitions of the dsip_lib funcs\n. ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\n\nprintdbg 'retrieving system info'\nexport DISTRO=$(getDistroName)\nexport DISTRO_VER=$(getDistroVer)\nexport DISTRO_MAJOR_VER=$(cut -d '.' -f 1 <<<\"$DISTRO_VER\")\nexport DISTRO_MINOR_VER=$(cut -s -d '.' -f 2 <<<\"$DISTRO_VER\")\n\nprintdbg 'validating OS support'\nif [[ \"$(getDistroName)\" == 'debian' && \"$(getDistroVer)\" == \"9\" ]]; then\n    printerr 'debian stretch is not supported in this version of dSIPRouter'\n    echo 'upgrade your system to a supported version of debian first'\n    echo 'for more information see: https://dsiprouter.readthedocs.io/en/latest/upgrading.html'\n    exit 1\nfi\n\nprintdbg 'retrieving current system settings'\n# NOTE: some magic is being done here to reset specific settings next install\nexport PYTHON_CMD=python3\nDSIP_SYSTEM_CONFIG_DIR='/etc/dsiprouter'\nDSIP_CONFIG_FILE=${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py\nDSIP_KAMAILIO_CONFIG_FILE=\"${DSIP_SYSTEM_CONFIG_DIR}/kamailio/kamailio.cfg\"\nexport ROOT_DB_PASS=$(decryptConfigAttrib 'ROOT_DB_PASS' ${DSIP_CONFIG_FILE})\nexport ROOT_DB_HOST=$(getConfigAttrib 'ROOT_DB_HOST' ${DSIP_CONFIG_FILE})\nexport ROOT_DB_PORT=$(getConfigAttrib 'ROOT_DB_PORT' ${DSIP_CONFIG_FILE})\nexport ROOT_DB_NAME=$(getConfigAttrib 'ROOT_DB_NAME' ${DSIP_CONFIG_FILE})\nexport KAM_DB_NAME=$(getConfigAttrib 'KAM_DB_NAME' ${DSIP_CONFIG_FILE})\nexport SET_KAM_DB_HOST=$(getConfigAttrib 'KAM_DB_HOST' ${DSIP_CONFIG_FILE})\nexport KAM_DB_HOST=\"$SET_KAM_DB_HOST\"\nexport KAM_DB_USER=$(getConfigAttrib 'KAM_DB_USER' ${DSIP_CONFIG_FILE})\nexport SET_KAM_DB_PASS=$(decryptConfigAttrib 'KAM_DB_PASS' ${DSIP_CONFIG_FILE})\nexport KAM_DB_PASS=\"$SET_KAM_DB_PASS\"\nexport SET_DSIP_API_TOKEN=$(decryptConfigAttrib 'DSIP_API_TOKEN' ${DSIP_CONFIG_FILE})\nexport DSIP_IPC_PASS=\"$SET_DSIP_API_TOKEN\"\nexport SET_DSIP_MAIL_PASS=$(decryptConfigAttrib 'MAIL_PASSWORD' ${DSIP_CONFIG_FILE})\nexport MAIL_PASSWORD=\"$DSIP_MAIL_PASS\"\nexport SET_DSIP_IPC_TOKEN=$(decryptConfigAttrib 'DSIP_IPC_PASS' ${DSIP_CONFIG_FILE})\nexport DSIP_IPC_PASS=\"$SET_DSIP_IPC_TOKEN\"\n\nprintdbg 'preparing for migration'\nREINSTALL_DNSMASQ=0\nREINSTALL_KAMAILIO=0\nREINSTALL_DSIPROUTER=0\nREINSTALL_RTPENGINE=0\nINSTALL_OPTS=()\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dnsmasqinstalled\" ]]; then\n    REINSTALL_DNSMASQ=1\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]]; then\n    REINSTALL_KAMAILIO=1\n    INSTALL_OPTS+=(-kam)\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]]; then\n    REINSTALL_DSIPROUTER=1\n    INSTALL_OPTS+=(-dsip)\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\" ]]; then\n    REINSTALL_RTPENGINE=1\n    INSTALL_OPTS+=(-rtp)\nfi\nmkdir -p $CURR_BACKUP_DIR\n\n# if the state files for the services to upgrade were there before\n# and we fail, put them back so the system can recover\nresetConfigsHandler() {\n    printwarn 'upgrade failed, resetting system to previous state'\n\n    if (( $REINSTALL_KAMAILIO == 1 )); then\n        systemctl unmask kamailio.service\n    fi\n    if (( $REINSTALL_DSIPROUTER == 1 )); then\n        systemctl unmask dsiprouter.service\n    fi\n    if (( $REINSTALL_RTPENGINE == 1 )); then\n        systemctl unmask rtpengine.service\n    fi\n\n    cp -af ${CURR_BACKUP_DIR}/opt/. /etc/\n    cp -af ${CURR_BACKUP_DIR}/opt/. /lib/\n    cp -af ${CURR_BACKUP_DIR}/opt/. /opt/\n    cp -af ${CURR_BACKUP_DIR}/opt/. /var/\n    systemctl daemon-reload\n\n    if (( ${KAM_DB_DROPPED:-0} == 1 )); then\n        withRootDBConn mysql <${CURR_BACKUP_DIR}/db.sql\n        withRootDBConn mysql <${CURR_BACKUP_DIR}/user.sql\n    fi\n\n    if (( $RUN_FROM_GUI == 0 )); then\n        if (( $REINSTALL_DNSMASQ == 1 )); then\n            systemctl restart dnsmasq\n        fi\n        if (( $REINSTALL_KAMAILIO == 1 )); then\n            systemctl restart kamailio\n        fi\n        if (( $REINSTALL_DSIPROUTER == 1 )); then\n            systemctl restart dsiprouter\n        fi\n        if (( $REINSTALL_RTPENGINE == 1 )); then\n            systemctl restart rtpengine\n        fi\n    fi\n\n    trap - EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n}\ntrap 'resetConfigsHandler $?' EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n\n# always reinstalled\nrm -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiproutercliinstalled\" 2>/dev/null\nrm -f \"${DSIP_SYSTEM_CONFIG_DIR}/.requirementsinstalled\" 2>/dev/null\nrm -rf ${SRC_DIR}/kamailio ${SRC_DIR}/rtpengine\n\n# conditionally reinstalled\nprintdbg 'masking services'\nif (( $REINSTALL_DNSMASQ == 1 )); then\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.dnsmasqinstalled\n\n    # dnsmasq is not masked, we want it to restart during the install process\n    if [[ -f /etc/systemd/system/dnsmasq.service ]]; then\n        mv -f /etc/systemd/system/dnsmasq.service /lib/systemd/system/dnsmasq.service\n        systemctl daemon-reload\n    fi\nfi\nif (( $REINSTALL_KAMAILIO == 1 )); then\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\n\n    if [[ -f /etc/systemd/system/kamailio.service ]]; then\n        mv -f /etc/systemd/system/kamailio.service /lib/systemd/system/kamailio.service\n        systemctl daemon-reload\n    fi\n\n    rm -f \"$DSIP_KAMAILIO_CONFIG_FILE\"\n\n    systemctl mask kamailio.service\nfi\nif (( $REINSTALL_DSIPROUTER == 1 )); then\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\n\n    systemctl mask dsiprouter.service\nfi\nif (( $REINSTALL_RTPENGINE == 1 )); then\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\n\n    if [[ -f /etc/systemd/system/rtpengine.service ]]; then\n        mv -f /etc/systemd/system/rtpengine.service /lib/systemd/system/rtpengine.service\n        systemctl daemon-reload\n    fi\n\n    systemctl mask rtpengine.service\nfi\n\nprintdbg 'migrating dSIPRouter project files'\ncp -rf ${NEW_PROJECT_DIR}/. ${DSIP_PROJECT_DIR}/\nrm -rf ${NEW_PROJECT_DIR}\ncd ${DSIP_PROJECT_DIR}/\n\nif (( $REINSTALL_DSIPROUTER == 1 )); then\n    printdbg 'updating new dSIPRouter settings'\n    cd ${DSIP_PROJECT_DIR}/gui && (\npython3 <<'EOF'\nimport os\nimport settings as default_settings\nfrom importlib.util import module_from_spec, spec_from_file_location\nfrom shared import objToDict, updateConfig\ndefault_settings_dict = objToDict(default_settings)\nspec = spec_from_file_location('current_settings', '/etc/dsiprouter/gui/settings.py')\ncurrent_settings = module_from_spec(spec)\nspec.loader.exec_module(current_settings)\ncurrent_settings_dict = objToDict(current_settings)\ndefault_settings_dict.update(current_settings_dict)\nos.system(f'cp -f {default_settings.__file__} {current_settings.__file__}')\nupdateConfig(current_settings, default_settings_dict)\nEOF\n    ) || {\n        printerr 'failed updating dSIPRouter settings'\n        exit 1\n    }\nfi\n\n# source the new dsip_lib functions\n# WARNING: from here on we are explicitly using the NEW definitions of the dsip_lib funcs\n# NOTE: resetConfigsHandler() above will still use the new definitions (lazy loading)\n. ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\n\nif (( $REINSTALL_KAMAILIO == 1 )); then\n    printdbg 'backing up kamailio database'\n    dumpDB \"$KAM_DB_NAME\" >${CURR_BACKUP_DIR}/db.sql\n    dumpDBUser \"$KAM_DB_USER@$KAM_DB_NAME\" >${CURR_BACKUP_DIR}/user.sql\n\n    withRootDBConn mysql -e \"USE $KAM_DB_NAME; DROP TABLE IF EXISTS dsip_settings;\"\n    withRootDBConn mysqldump --single-transaction --no-create-info --skip-triggers --replace \"$KAM_DB_NAME\" >${CURR_BACKUP_DIR}/data.sql\n    if [[ ! -f ${CURR_BACKUP_DIR}/data.sql ]]; then\n        printerr 'failed backing up kamailio database data'\n        exit 1\n    fi\n    printdbg 'dropping kamailio database to install new schema'\n    withRootDBConn mysql -e \"DROP DATABASE IF EXISTS $KAM_DB_NAME;\"\n    withRootDBConn mysql -e \"DROP USER IF EXISTS '$KAM_DB_USER'@'%'; DROP USER IF EXISTS '$KAM_DB_USER'@'localhost';\"\n    KAM_DB_DROPPED=1\nfi\n\nprintdbg 'upgrading services'\n${DSIP_PROJECT_DIR}/dsiprouter.sh install ${INSTALL_OPTS[@]}\n\nif (( $? != 0 )); then\n    printerr 'failed upgrading services'\n    exit 1\nfi\n\nif (( $REINSTALL_KAMAILIO == 1 )); then\n    printdbg 'restoring kamailio database data'\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql <${CURR_BACKUP_DIR}/data.sql || {\n        printerr 'failed restoring kamailio database data'\n        exit 1\n    }\nfi\n\nif (( $REINSTALL_DSIPROUTER == 1 )); then\n    printdbg 'updating dSIPRouter version'\n    setConfigAttrib 'VERSION' '0.73' ${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py -q || {\n        printerr 'failed updating dSIPRouter version'\n        exit 1\n    }\nfi\n\nprintdbg 'unmasking services'\nif (( $REINSTALL_KAMAILIO == 1 )); then\n    systemctl unmask kamailio.service\nfi\nif (( $REINSTALL_DSIPROUTER == 1 )); then\n    systemctl unmask dsiprouter.service\nfi\nif (( $REINSTALL_RTPENGINE == 1 )); then\n    systemctl unmask rtpengine.service\nfi\n\nif (( $RUN_FROM_GUI == 0 )); then\n    printdbg 'restarting services'\n    if (( $REINSTALL_KAMAILIO == 1 )); then\n        systemctl restart kamailio\n        if ! systemctl is-active -q kamailio; then\n            printerr 'could not start kamailio service'\n            exit 1\n        fi\n    fi\n    if (( $REINSTALL_DSIPROUTER == 1 )); then\n        systemctl restart dsiprouter\n        if ! systemctl is-active -q dsiprouter; then\n            printerr 'could not start dsiprouter service'\n            exit 1\n        fi\n    fi\n    if (( $REINSTALL_RTPENGINE == 1 )); then\n        systemctl restart rtpengine\n        if ! systemctl is-active -q rtpengine; then\n            printerr 'could not start rtpengine service'\n            exit 1\n        fi\n    fi\nelse\n    printwarn 'running from the GUI, some services require restarting'\nfi\n\n# make sure the resetConfigsHandler() is nerfed now that we are successful\ntrap - EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n\npprint 'upgrade completed successfully'\n\nexit 0\n"
  },
  {
    "path": "resources/upgrade/v0.73/settings.json",
    "content": "{\n  \"version\": \"0.73\",\n  \"depends\": [\"0.72\", \"0.721\"],\n  \"install_location\": \"/opt/dsiprouter\",\n  \"dsiprouter\": [\n    \"migrate.sh\"\n  ]\n}\n"
  },
  {
    "path": "resources/upgrade/v0.74/scripts/migrate.sh",
    "content": "#!/usr/bin/env bash\n\n# where the new project files were downloaded\nNEW_PROJECT_DIR=${NEW_PROJECT_DIR:-/tmp/dsiprouter}\n# whether or not this was called from the GUI\nRUN_FROM_GUI=${RUN_FROM_GUI:-0}\n# the backup directory set by dsiprouter.sh\nCURR_BACKUP_DIR=${CURR_BACKUP_DIR:-\"/var/backups/dsiprouter/$(date '+%s')\"}\n\n# set project dir where previous repo was located\nexport DSIP_PROJECT_DIR='/opt/dsiprouter'\n# import dsip_lib utility / shared functions (we are using old functions on purpose here)\n# WARNING: from here on we are explicitly using the OLD definitions of the dsip_lib funcs\n. ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\n\nprintdbg 'retrieving system info'\nexport DISTRO=$(getDistroName)\nexport DISTRO_VER=$(getDistroVer)\nexport DISTRO_MAJOR_VER=$(cut -d '.' -f 1 <<<\"$DISTRO_VER\")\nexport DISTRO_MINOR_VER=$(cut -s -d '.' -f 2 <<<\"$DISTRO_VER\")\n\nprintdbg 'validating OS support'\nif [[ \"$(getDistroName)\" == 'debian' && \"$(getDistroVer)\" == \"9\" ]]; then\n    printerr 'debian stretch is not supported in this version of dSIPRouter'\n    echo 'upgrade your system to a supported version of debian first'\n    echo 'for more information see: https://dsiprouter.readthedocs.io/en/latest/upgrading.html'\n    exit 1\nfi\n\nprintdbg 'retrieving current system settings'\n# NOTE: some magic is being done here to reset specific settings next install\nexport PYTHON_CMD=python3\nDSIP_SYSTEM_CONFIG_DIR='/etc/dsiprouter'\nDSIP_CONFIG_FILE=${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py\nDSIP_KAMAILIO_CONFIG_FILE=\"${DSIP_SYSTEM_CONFIG_DIR}/kamailio/kamailio.cfg\"\nexport ROOT_DB_PASS=$(decryptConfigAttrib 'ROOT_DB_PASS' ${DSIP_CONFIG_FILE})\nexport ROOT_DB_HOST=$(getConfigAttrib 'ROOT_DB_HOST' ${DSIP_CONFIG_FILE})\nexport ROOT_DB_PORT=$(getConfigAttrib 'ROOT_DB_PORT' ${DSIP_CONFIG_FILE})\nexport ROOT_DB_NAME=$(getConfigAttrib 'ROOT_DB_NAME' ${DSIP_CONFIG_FILE})\nexport KAM_DB_NAME=$(getConfigAttrib 'KAM_DB_NAME' ${DSIP_CONFIG_FILE})\nexport SET_KAM_DB_HOST=$(getConfigAttrib 'KAM_DB_HOST' ${DSIP_CONFIG_FILE})\nexport KAM_DB_HOST=\"$SET_KAM_DB_HOST\"\nexport KAM_DB_USER=$(getConfigAttrib 'KAM_DB_USER' ${DSIP_CONFIG_FILE})\nexport SET_KAM_DB_PASS=$(decryptConfigAttrib 'KAM_DB_PASS' ${DSIP_CONFIG_FILE})\nexport KAM_DB_PASS=\"$SET_KAM_DB_PASS\"\nexport SET_DSIP_API_TOKEN=$(decryptConfigAttrib 'DSIP_API_TOKEN' ${DSIP_CONFIG_FILE})\nexport DSIP_API_TOKEN=\"$SET_DSIP_API_TOKEN\"\nexport SET_DSIP_MAIL_PASS=$(decryptConfigAttrib 'MAIL_PASSWORD' ${DSIP_CONFIG_FILE})\nexport MAIL_PASSWORD=\"$DSIP_MAIL_PASS\"\nexport SET_DSIP_IPC_TOKEN=$(decryptConfigAttrib 'DSIP_IPC_PASS' ${DSIP_CONFIG_FILE})\nexport DSIP_IPC_PASS=\"$SET_DSIP_IPC_TOKEN\"\n\nprintdbg 'preparing for migration'\nREINSTALL_DNSMASQ=0\nREINSTALL_NGINX=0\nREINSTALL_KAMAILIO=0\nREINSTALL_DSIPROUTER=0\nREINSTALL_RTPENGINE=0\nINSTALL_OPTS=()\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dnsmasqinstalled\" ]]; then\n    REINSTALL_DNSMASQ=1\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.nginxinstalled\" ]]; then\n    REINSTALL_NGINX=1\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]]; then\n    REINSTALL_KAMAILIO=1\n    INSTALL_OPTS+=(-kam)\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]]; then\n    REINSTALL_DSIPROUTER=1\n    INSTALL_OPTS+=(-dsip)\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\" ]]; then\n    REINSTALL_RTPENGINE=1\n    INSTALL_OPTS+=(-rtp)\nfi\nmkdir -p $CURR_BACKUP_DIR\n\n# if the state files for the services to upgrade were there before\n# and we fail, put them back so the system can recover\nresetConfigsHandler() {\n    printwarn 'upgrade failed, resetting system to previous state'\n\n    if (( $REINSTALL_KAMAILIO == 1 )); then\n        systemctl unmask kamailio.service\n    fi\n    if (( $REINSTALL_DSIPROUTER == 1 )); then\n        systemctl unmask dsiprouter.service\n    fi\n    if (( $REINSTALL_RTPENGINE == 1 )); then\n        systemctl unmask rtpengine.service\n    fi\n\n    cp -af ${CURR_BACKUP_DIR}/etc/. /etc/\n    cp -af ${CURR_BACKUP_DIR}/lib/. /lib/\n    cp -af ${CURR_BACKUP_DIR}/opt/. /opt/\n    cp -af ${CURR_BACKUP_DIR}/var/. /var/\n    systemctl daemon-reload\n\n    if (( $REINSTALL_KAMAILIO == 1 )); then\n        # automatically created in dsiprouter.sh when installKamailio() runs\n        [[ -e ${CURR_BACKUP_DIR}/db.sql ]] &&\n        withRootDBConn mysql <${CURR_BACKUP_DIR}/db.sql &&\n        withRootDBConn mysql <${CURR_BACKUP_DIR}/user.sql\n    fi\n\n    # not included in the restrt() function from dsiprouter.sh\n    # so we always restart to get config changes\n    if (( $REINSTALL_DNSMASQ == 1 )); then\n        systemctl restart dnsmasq\n    fi\n\n    if (( $RUN_FROM_GUI == 0 )); then\n        if (( $REINSTALL_NGINX == 1 )); then\n            systemctl restart nginx\n        fi\n        if (( $REINSTALL_KAMAILIO == 1 )); then\n            systemctl restart kamailio\n        fi\n        if (( $REINSTALL_DSIPROUTER == 1 )); then\n            systemctl restart dsiprouter\n        fi\n        if (( $REINSTALL_RTPENGINE == 1 )); then\n            systemctl restart rtpengine\n        fi\n    fi\n\n    trap - EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n}\ntrap 'resetConfigsHandler $?' EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n\n# conditionally reinstalled\nprintdbg 'masking services'\nif (( $REINSTALL_DNSMASQ == 1 )); then\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.dnsmasqinstalled\n\n    # dnsmasq is not masked, we want it to restart during the install process\nfi\nif (( $REINSTALL_NGINX == 1 )); then\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.nginxinstalled\n\n    if [[ -f /etc/systemd/system/nginx.service ]]; then\n        mv -f /etc/systemd/system/nginx.service /lib/systemd/system/nginx.service\n        systemctl daemon-reload\n    fi\n\n    systemctl mask nginx.service\nfi\nif (( $REINSTALL_KAMAILIO == 1 )); then\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\n    rm -rf ${SRC_DIR}/kamailio\n    rm -f \"$DSIP_KAMAILIO_CONFIG_FILE\"\n\n    systemctl mask kamailio.service\nfi\nif (( $REINSTALL_DSIPROUTER == 1 )); then\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\n\n    systemctl mask dsiprouter.service\nfi\nif (( $REINSTALL_RTPENGINE == 1 )); then\n    rm -rf ${SRC_DIR}/rtpengine\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\n\n    systemctl mask rtpengine.service\nfi\n\nprintdbg 'migrating dSIPRouter project files'\ncp -rf ${NEW_PROJECT_DIR}/. ${DSIP_PROJECT_DIR}/\nrm -rf ${NEW_PROJECT_DIR}\ncd ${DSIP_PROJECT_DIR}/\n\n# source the new dsip_lib functions\n# WARNING: from here on we are explicitly using the NEW definitions of the dsip_lib funcs\n# NOTE: resetConfigsHandler() above will still use the new definitions (lazy loading)\n. ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\n\nprintdbg 'upgrading services'\n${DSIP_PROJECT_DIR}/dsiprouter.sh install ${INSTALL_OPTS[@]}\n\nif (( $? != 0 )); then\n    printerr 'failed upgrading services'\n    exit 1\nfi\n\nif (( $REINSTALL_KAMAILIO == 1 )); then\n    printdbg 'restoring kamailio database'\n    withRootDBConn mysql <${CURR_BACKUP_DIR}/db.sql &&\n    withRootDBConn mysql <${CURR_BACKUP_DIR}/user.sql || {\n        printerr 'failed restoring kamailio database'\n        exit 1\n    }\nfi\n\nif (( $REINSTALL_DSIPROUTER == 1 )); then\n    printdbg 'updating dSIPRouter version'\n    setConfigAttrib 'VERSION' '0.74' ${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py -q || {\n        printerr 'failed updating dSIPRouter version'\n        exit 1\n    }\nfi\n\nprintdbg 'unmasking services'\nif (( $REINSTALL_NGINX == 1 )); then\n    systemctl unmask nginx.service\nfi\nif (( $REINSTALL_KAMAILIO == 1 )); then\n    systemctl unmask kamailio.service\nfi\nif (( $REINSTALL_DSIPROUTER == 1 )); then\n    systemctl unmask dsiprouter.service\nfi\nif (( $REINSTALL_RTPENGINE == 1 )); then\n    systemctl unmask rtpengine.service\nfi\n\nif (( $RUN_FROM_GUI == 0 )); then\n    printdbg 'restarting services'\n    if (( $REINSTALL_KAMAILIO == 1 )); then\n        systemctl restart kamailio\n        if ! systemctl is-active -q kamailio; then\n            printerr 'could not start kamailio service'\n            exit 1\n        fi\n    fi\n    if (( $REINSTALL_DSIPROUTER == 1 )); then\n        systemctl restart dsiprouter\n        if ! systemctl is-active -q dsiprouter; then\n            printerr 'could not start dsiprouter service'\n            exit 1\n        fi\n    fi\n    if (( $REINSTALL_RTPENGINE == 1 )); then\n        systemctl restart rtpengine\n        if ! systemctl is-active -q rtpengine; then\n            printerr 'could not start rtpengine service'\n            exit 1\n        fi\n    fi\nelse\n    printwarn 'running from the GUI, some services require restarting'\nfi\n\n# make sure the resetConfigsHandler() is nerfed now that we are successful\ntrap - EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n\npprint 'upgrade completed successfully'\n\nexit 0\n"
  },
  {
    "path": "resources/upgrade/v0.74/settings.json",
    "content": "{\n  \"version\": \"0.74\",\n  \"depends\": [\"0.73\"],\n  \"install_location\": \"/opt/dsiprouter\",\n  \"dsiprouter\": [\n    \"migrate.sh\"\n  ]\n}\n"
  },
  {
    "path": "resources/upgrade/v0.75/clear_defaults.sql",
    "content": "SET FOREIGN_KEY_CHECKS=0;\nTRUNCATE TABLE `address`;\nTRUNCATE TABLE `dr_gateways`;\nTRUNCATE TABLE `dr_gw_lists`;\nTRUNCATE TABLE `dr_rules`;\nSET FOREIGN_KEY_CHECKS=1;"
  },
  {
    "path": "resources/upgrade/v0.75/migrate_data.sql",
    "content": "UPDATE `dr_gateways` SET `attrs` = IF(\n  (CHAR_LENGTH(attrs) - CHAR_LENGTH(REPLACE(attrs, ',', ''))) = 2,\n  CONCAT(SUBSTRING_INDEX(`attrs`, ',', 3), ',proxy,proxy'),\n  CONCAT(SUBSTRING_INDEX(`attrs`, ',', 2), ',,proxy,proxy')\n);\n\nUPDATE `dispatcher` SET `attrs` = IF(\n  `attrs` REGEXP 'weight=([0-9]+)', CONCAT('signalling=proxy;media=proxy;rweight=',\n  CAST((REGEXP_REPLACE(`attrs`, '.*weight=([0-9]+).*', '\\\\1') / 100) AS CHAR)),\n  'signalling=proxy;media=proxy;rweight=0'\n);\n\nINSERT INTO `dsip_call_settings` (`gwgroupid`, `limit`)\nSELECT CAST(`gwgroupid` AS UNSIGNED), CAST(`limit` AS UNSIGNED)\nFROM `dsip_calllimit`;\nDROP TABLE `dsip_calllimit`;"
  },
  {
    "path": "resources/upgrade/v0.75/pre_import_data.sql",
    "content": "CREATE TABLE `dsip_calllimit` (\n    `gwgroupid` varchar(64) NOT NULL,\n    `limit` varchar(64) NOT NULL DEFAULT '0',\n    `status` tinyint(1) NOT NULL DEFAULT 1,\n    `key_type` varchar(64) NOT NULL DEFAULT '0',\n    `value_type` varchar(64) NOT NULL DEFAULT '0',\n    PRIMARY KEY (`gwgroupid`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;"
  },
  {
    "path": "resources/upgrade/v0.75/scripts/bootstrap.sh",
    "content": "#!/usr/bin/env bash\n\ncd /opt/dsiprouter\n. dsiprouter/dsip_lib.sh\nLC_CHECK=$(decryptConfigAttrib 'DSIP_CORE_LICENSE' /etc/dsiprouter/gui/settings.py | dd if=/dev/stdin of=/dev/stdout bs=1 count=32 2>/dev/null)\n(( ${#LC_CHECK} == 32 )) || {\necho \"a core licene is required to use the auto upgrade feature\"\necho \"without a license the upgrade will fail\"\necho \"you can purchase a license here: https://dopensource.com/product/dsiprouter-core/\"\nexit 1\n}\n\nexport BOOTSTRAPPING_UPGRADE=1\nexport BOOTSTRAP_DIR='/tmp/dsiprouter'\nTAG_NAME='v0.75-rel'\nREPO_URL='https://github.com/dOpensource/dsiprouter.git'\n[[ -e \"$BOOTSTRAP_DIR\" ]] && rm -rf \"$BOOTSTRAP_DIR\"\n\ngit clone --depth 1 -c advice.detachedHead=false -b \"$TAG_NAME\" \"$REPO_URL\" \"$BOOTSTRAP_DIR\" &&\nmkdir -p /opt/dsiprouter/resources/upgrade/v0.75/ &&\ncp -rf /tmp/dsiprouter/resources/upgrade/v0.75/. /opt/dsiprouter/resources/upgrade/v0.75/ &&\ndsiprouter upgrade -rel v0.75"
  },
  {
    "path": "resources/upgrade/v0.75/scripts/migrate.sh",
    "content": "#!/usr/bin/env bash\n\n(( ${DEBUG:-0} == 1 )) && set -x\n\n# where the new project files were downloaded\nNEW_PROJECT_DIR=${NEW_PROJECT_DIR:-/tmp/dsiprouter}\n# whether or not this was called from the GUI\nRUN_FROM_GUI=${RUN_FROM_GUI:-0}\n# the backup directory set by dsiprouter.sh\nCURR_BACKUP_DIR=${CURR_BACKUP_DIR:-\"/var/backups/dsiprouter/$(date '+%s')\"}\n\n# set project dir where previous repo was located\nexport DSIP_PROJECT_DIR='/opt/dsiprouter'\n# import dsip_lib utility / shared functions (we are using old functions on purpose here)\n# WARNING: from here on we are explicitly using the OLD definitions of the dsip_lib funcs\n. ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\n\n# make sure the updates are downloaded and in the correct location\n[[ ! -e \"$NEW_PROJECT_DIR\" ]] && {\n    printerr 'could not find repo to upgrade from'\n    echo \"expected updated repo to be here: $NEW_PROJECT_DIR\"\n    exit 1\n}\n\nprintdbg 'retrieving system info'\nexport DISTRO=$(getDistroName)\nexport DISTRO_VER=$(getDistroVer)\nexport DISTRO_MAJOR_VER=$(cut -d '.' -f 1 <<<\"$DISTRO_VER\")\nexport DISTRO_MINOR_VER=$(cut -s -d '.' -f 2 <<<\"$DISTRO_VER\")\n\nprintdbg 'validating OS support'\nif [[ \"$(getDistroName)\" == 'debian' && \"$(getDistroVer)\" == \"9\" ]]; then\n    printerr 'debian stretch is not supported in this version of dSIPRouter'\n    echo 'upgrade your system to a supported version of debian first'\n    echo 'for more information see: https://dsiprouter.readthedocs.io/en/latest/upgrading.html'\n    exit 1\nfi\n\nprintdbg 'retrieving current system settings'\n# NOTE: some magic is being done here to reset specific settings next install\nexport PYTHON_CMD=python3\nDSIP_SYSTEM_CONFIG_DIR='/etc/dsiprouter'\nDSIP_LIB_DIR='/var/lib/dsiprouter'\nDSIP_CONFIG_FILE=${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py\nDSIP_KAMAILIO_CONFIG_FILE=\"${DSIP_SYSTEM_CONFIG_DIR}/kamailio/kamailio.cfg\"\nSYSTEM_KAMAILIO_CONFIG_DIR='/etc/kamailio'\nSYSTEM_RTPENGINE_CONFIG_DIR='/etc/rtpengine'\nexport ROOT_DB_PASS=$(decryptConfigAttrib 'ROOT_DB_PASS' ${DSIP_CONFIG_FILE})\nexport ROOT_DB_HOST=$(getConfigAttrib 'ROOT_DB_HOST' ${DSIP_CONFIG_FILE})\nexport ROOT_DB_PORT=$(getConfigAttrib 'ROOT_DB_PORT' ${DSIP_CONFIG_FILE})\nexport ROOT_DB_NAME=$(getConfigAttrib 'ROOT_DB_NAME' ${DSIP_CONFIG_FILE})\nexport KAM_DB_NAME=$(getConfigAttrib 'KAM_DB_NAME' ${DSIP_CONFIG_FILE})\nexport SET_KAM_DB_HOST=$(getConfigAttrib 'KAM_DB_HOST' ${DSIP_CONFIG_FILE})\nexport KAM_DB_HOST=\"$SET_KAM_DB_HOST\"\nexport KAM_DB_USER=$(getConfigAttrib 'KAM_DB_USER' ${DSIP_CONFIG_FILE})\nexport SET_KAM_DB_PASS=$(decryptConfigAttrib 'KAM_DB_PASS' ${DSIP_CONFIG_FILE})\nexport KAM_DB_PASS=\"$SET_KAM_DB_PASS\"\nexport SET_DSIP_API_TOKEN=$(decryptConfigAttrib 'DSIP_API_TOKEN' ${DSIP_CONFIG_FILE})\nexport DSIP_API_TOKEN=\"$SET_DSIP_API_TOKEN\"\nexport SET_DSIP_MAIL_PASS=$(decryptConfigAttrib 'MAIL_PASSWORD' ${DSIP_CONFIG_FILE})\nexport MAIL_PASSWORD=\"$DSIP_MAIL_PASS\"\nexport SET_DSIP_IPC_TOKEN=$(decryptConfigAttrib 'DSIP_IPC_PASS' ${DSIP_CONFIG_FILE})\nexport DSIP_IPC_PASS=\"$SET_DSIP_IPC_TOKEN\"\nDSIP_CORE_LICENSE=$(decryptConfigAttrib 'DSIP_CORE_LICENSE' ${DSIP_CONFIG_FILE} | dd if=/dev/stdin of=/dev/stdout bs=1 count=32 2>/dev/null)\nDSIP_STIRSHAKEN_LICENSE=$(decryptConfigAttrib 'DSIP_STIRSHAKEN_LICENSE' ${DSIP_CONFIG_FILE} | dd if=/dev/stdin of=/dev/stdout bs=1 count=32 2>/dev/null)\nDSIP_TRANSNEXUS_LICENSE=$(decryptConfigAttrib 'DSIP_TRANSNEXUS_LICENSE' ${DSIP_CONFIG_FILE} | dd if=/dev/stdin of=/dev/stdout bs=1 count=32 2>/dev/null)\nDSIP_MSTEAMS_LICENSE=$(decryptConfigAttrib 'DSIP_MSTEAMS_LICENSE' ${DSIP_CONFIG_FILE} | dd if=/dev/stdin of=/dev/stdout bs=1 count=32 2>/dev/null)\n\nprintdbg 'validating system system configuration'\nif [[ -z \"$DSIP_CORE_LICENSE\" ]]; then\n    printerr 'A DSIP_CORE license is required to use the auto upgrade feature'\n    echo 'Consider supporting the hard working engineers maintaining this software if you would like to use this feature'\n    exit 1\nfi\n\n\nprintdbg 'preparing for migration'\nREINSTALL_DNSMASQ=0\nREINSTALL_NGINX=0\nREINSTALL_KAMAILIO=0\nREINSTALL_DSIPROUTER=0\nREINSTALL_RTPENGINE=0\nINSTALL_OPTS=()\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dnsmasqinstalled\" ]]; then\n    REINSTALL_DNSMASQ=1\n    INSTALL_OPTS+=(-dns)\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.nginxinstalled\" ]]; then\n    REINSTALL_NGINX=1\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]]; then\n    REINSTALL_KAMAILIO=1\n    INSTALL_OPTS+=(-kam)\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]]; then\n    REINSTALL_DSIPROUTER=1\n    INSTALL_OPTS+=(-dsip)\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\" ]]; then\n    REINSTALL_RTPENGINE=1\n    INSTALL_OPTS+=(-rtp)\nfi\n\nprintdbg 'backing up configs just in case the upgrade fails'\nmkdir -p $CURR_BACKUP_DIR\n# TODO: make the destination paths use our static variables as well\nmkdir -p ${CURR_BACKUP_DIR}/{opt/dsiprouter,var/lib/dsiprouter,etc/dsiprouter,etc/kamailio,etc/rtpengine,etc/systemd/system,lib/systemd/system,etc/default}\n#    mkdir -p ${CURR_BACKUP_DIR}/{var/lib/mysql,${HOME}}\ncp -af ${DSIP_PROJECT_DIR}/. ${CURR_BACKUP_DIR}/opt/dsiprouter/\ncp -af ${DSIP_LIB_DIR}/. ${CURR_BACKUP_DIR}/var/lib/dsiprouter/\ncp -af ${SYSTEM_KAMAILIO_CONFIG_DIR}/. ${CURR_BACKUP_DIR}/etc/kamailio/\ncp -af ${DSIP_SYSTEM_CONFIG_DIR}/. ${CURR_BACKUP_DIR}/etc/dsiprouter/\ncp -af ${SYSTEM_RTPENGINE_CONFIG_DIR}/. ${CURR_BACKUP_DIR}/etc/rtpengine/\n#    cp -af /var/lib/mysql/. ${CURR_BACKUP_DIR}/var/lib/mysql/\n#    cp -af /etc/my.cnf ${CURR_BACKUP_DIR}/etc/ 2>/dev/null\n#    cp -af /etc/mysql/. ${CURR_BACKUP_DIR}/etc/mysql/\n#    cp -af ${HOME}/.my.cnf ${CURR_BACKUP_DIR}/${HOME}/ 2>/dev/null\ncp -af /etc/dnsmasq.conf ${CURR_BACKUP_DIR}/etc/\ncp -af /etc/systemd/system/{nginx,dsiprouter,dnsmasq,kamailio,rtpengine,dsip-init,mariadb}.service ${CURR_BACKUP_DIR}/etc/systemd/system/ 2>/dev/null\ncp -af /lib/systemd/system/{nginx,dsiprouter,dnsmasq,kamailio,rtpengine,dsip-init,mariadb}.service ${CURR_BACKUP_DIR}/lib/systemd/system/ 2>/dev/null\ncp -af /etc/default/{kamailio,rtpengine}* ${CURR_BACKUP_DIR}/etc/default/\nprintdbg \"files were backed up here: ${CURR_BACKUP_DIR}/\"\n\n# shim any functions that would be missing from older versions\ndeclare -F withRootDBConn >/dev/null || {\n    function withRootDBConn() {\n        local TMP CMD\n        local CONN_OPTS=()\n\n        case \"$1\" in\n            --db=*)\n                TMP=$(cut -d '=' -f 2- <<<\"$1\")\n                [[ -n \"$TMP\" ]] && CONN_OPTS+=( \"--database=${TMP}\" )\n                shift\n                CMD=\"$1\"\n                shift\n                ;;\n            *)\n                CMD=\"$1\"\n                shift\n                if [[ \"$CMD\" == \"mysql\" ]]; then\n                    [[ -n \"$ROOT_DB_NAME\" ]] && CONN_OPTS+=( \"--database=${ROOT_DB_NAME}\" )\n                fi\n                ;;\n        esac\n\n        [[ -n \"$ROOT_DB_HOST\" ]] && CONN_OPTS+=( \"--host=${ROOT_DB_HOST}\" )\n        [[ -n \"$ROOT_DB_PORT\" ]] && CONN_OPTS+=( \"--port=${ROOT_DB_PORT}\" )\n        [[ -n \"$ROOT_DB_USER\" ]] && CONN_OPTS+=( \"--user=${ROOT_DB_USER}\" )\n        [[ -n \"$ROOT_DB_PASS\" ]] && CONN_OPTS+=( \"--password=${ROOT_DB_PASS}\" )\n\n        if [[ -p /dev/stdin ]]; then\n            ${CMD} \"${CONN_OPTS[@]}\" \"$@\" </dev/stdin\n        else\n            ${CMD} \"${CONN_OPTS[@]}\" \"$@\"\n        fi\n        return $?\n    }\n}\n\n# removes the old licenses from the database dump so we can re-add them in the new format\nfilterLicenseFromDataDump() {\n    perl -pe 's%(INSERT .*?INTO `dsip_settings` VALUES \\()(.*?)[^,]*?,[^,]*?,[^,]*?,[^,]*?(\\)\\;)%\\1\\2'\"'BQAAAAA='\"'\\3%g' </dev/stdin\n}\n\n# if the state files for the services to upgrade were there before\n# and we fail, put them back so the system can recover\nresetConfigsHandler() {\n    printwarn 'upgrade failed, resetting system to previous state'\n\n    if (( $REINSTALL_NGINX == 1 )); then\n        systemctl unmask nginx.service\n    fi\n    if (( $REINSTALL_KAMAILIO == 1 )); then\n        systemctl unmask kamailio.service\n    fi\n    if (( $REINSTALL_DSIPROUTER == 1 )); then\n        systemctl unmask dsiprouter.service\n    fi\n    if (( $REINSTALL_RTPENGINE == 1 )); then\n        systemctl unmask rtpengine.service\n    fi\n\n    cp -af ${CURR_BACKUP_DIR}/etc/. /etc/\n    cp -af ${CURR_BACKUP_DIR}/lib/. /lib/\n    cp -af ${CURR_BACKUP_DIR}/opt/. /opt/\n    cp -af ${CURR_BACKUP_DIR}/var/. /var/\n    systemctl daemon-reload\n\n    if (( $REINSTALL_KAMAILIO == 1 )); then\n        # automatically created in dsiprouter.sh when installKamailio() runs\n        [[ -e ${CURR_BACKUP_DIR}/db.sql ]] && withRootDBConn mysql <${CURR_BACKUP_DIR}/db.sql\n        [[ -e ${CURR_BACKUP_DIR}/user.sql ]] && withRootDBConn mysql <${CURR_BACKUP_DIR}/user.sql\n    fi\n\n    # not included in the restart() function from dsiprouter.sh\n    # so we always restart to get config changes\n    if (( $REINSTALL_DNSMASQ == 1 )); then\n        systemctl restart dnsmasq\n    fi\n\n    if (( $RUN_FROM_GUI == 0 )); then\n        if (( $REINSTALL_NGINX == 1 )); then\n            systemctl restart nginx\n        fi\n        if (( $REINSTALL_KAMAILIO == 1 )); then\n            systemctl restart kamailio\n        fi\n        if (( $REINSTALL_DSIPROUTER == 1 )); then\n            systemctl restart dsiprouter\n        fi\n        if (( $REINSTALL_RTPENGINE == 1 )); then\n            systemctl restart rtpengine\n        fi\n    fi\n\n    trap - EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n    exit 1\n}\ntrap 'resetConfigsHandler $?' EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n\n# conditionally reinstalled\nprintdbg 'masking services'\nif (( $REINSTALL_DNSMASQ == 1 )); then\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.dnsmasqinstalled\n\n    # dnsmasq is not masked, we want it to restart during the install process\nfi\nif (( $REINSTALL_NGINX == 1 )); then\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.nginxinstalled\n\n    if [[ -f /etc/systemd/system/nginx.service ]]; then\n        rm -f /etc/systemd/system/nginx.service\n    fi\n\n    systemctl mask nginx.service\nfi\nif (( $REINSTALL_KAMAILIO == 1 )); then\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\n    rm -rf ${SRC_DIR}/kamailio\n    rm -f \"$DSIP_KAMAILIO_CONFIG_FILE\"\n\n    if [[ -f /etc/systemd/system/kamailio.service ]]; then\n        rm -f /etc/systemd/system/kamailio.service\n    fi\n\n    systemctl mask kamailio.service\nfi\nif (( $REINSTALL_DSIPROUTER == 1 )); then\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\n\n    systemctl mask dsiprouter.service\nfi\nif (( $REINSTALL_RTPENGINE == 1 )); then\n    rm -rf ${SRC_DIR}/rtpengine\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\n\n    if [[ -f /etc/systemd/system/rtpengine.service ]]; then\n        rm -f /etc/systemd/system/rtpengine.service\n    fi\n\n    systemctl mask rtpengine.service\nfi\n\nprintdbg 'storing kamailio database data'\n(\n    withRootDBConn mysqldump --single-transaction --skip-opt --skip-triggers --no-create-db --no-create-info \\\n        --insert-ignore --hex-blob --skip-comments --databases \"$KAM_DB_NAME\"\n) >${CURR_BACKUP_DIR}/data.sql\n\nprintdbg 'migrating dSIPRouter project files'\ncp -rf ${NEW_PROJECT_DIR}/. ${DSIP_PROJECT_DIR}/\ncd ${DSIP_PROJECT_DIR}/\n\n# source the new dsip_lib functions\n# WARNING: from here on we are explicitly using the NEW definitions of the dsip_lib funcs\n# NOTE: resetConfigsHandler() above will still use the new definitions (lazy loading)\nexport PYTHON_CMD=\"${DSIP_PROJECT_DIR}/venv/bin/python\"\n. ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\n\nprintdbg 'upgrading services'\n# we clear environment here to make sure we get new static settings on install\nenv -i CURR_BACKUP_DIR=\"$CURR_BACKUP_DIR\" HOME=\"$HOME\" LANG=\"$LANG\" LANGUAGE=\"$LANGUAGE\" LC_ALL=\"$LC_ALL\" PATH=\"$PATH\" PWD=\"$PWD\" \\\n    ${DSIP_PROJECT_DIR}/dsiprouter.sh install ${INSTALL_OPTS[@]}\n\nif (( $? != 0 )); then\n    printerr 'failed upgrading services'\n    exit 1\nfi\n\nif (( $REINSTALL_KAMAILIO == 1 )); then\n    printdbg 'migrating kamailio database'\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql <${CURR_BACKUP_DIR}/user.sql &&\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql <${DSIP_PROJECT_DIR}/resources/upgrade/v0.75/clear_defaults.sql &&\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql <${DSIP_PROJECT_DIR}/resources/upgrade/v0.75/pre_import_data.sql &&\n    filterLicenseFromDataDump <${CURR_BACKUP_DIR}/data.sql | withRootDBConn --db=\"$KAM_DB_NAME\" mysql &&\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql <${DSIP_PROJECT_DIR}/resources/upgrade/v0.75/migrate_data.sql || {\n        printerr 'failed migrating kamailio database'\n        exit 1\n    }\nfi\n\nif (( $REINSTALL_DSIPROUTER == 1 )); then\n    printdbg 'migrating dSIPRouter settings'\n    (\n        # magic bash hacking\n        function exit() { :; }\n        export -f exit\n        source ${CURR_BACKUP_DIR}/opt/dsiprouter/dsiprouter.sh\n        unset -f exit\n        setStaticScriptSettings\n        setDynamicScriptSettings\n        # more magic environment munging\n        dsiprouter configuredsip\n    ) &&\n    setConfigAttrib 'VERSION' '0.75' ${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py -q &&\n    ${PYTHON_CMD} <<EOPY || { printerr 'Failed migrating dSIPRouter settings'; exit 1; }\nimport sys\nsys.path = [*['${DSIP_SYSTEM_CONFIG_DIR}/gui', '${DSIP_PROJECT_DIR}/gui'], *sys.path[1:]]\nfrom database import updateDsipSettingsTable\nfrom shared import updateConfig\nfrom modules.api.licensemanager.classes import WoocommerceLicense\nimport settings\n\nkeys = [\n    \"$DSIP_CORE_LICENSE\",\n    \"$DSIP_STIRSHAKEN_LICENSE\",\n    \"$DSIP_TRANSNEXUS_LICENSE\",\n    \"$DSIP_MSTEAMS_LICENSE\"\n]\nfor k in keys:\n    if len(k) == 0:\n        continue\n\n    lc = WoocommerceLicense(license_key=lc_key)\n    settings.DSIP_LICENSE_STORE[str(lc.id)] = lc.encrypt()\nelse:\n    if keys[0] == '':\n        sys.exit(1)\n    if not keys[0].active:\n        sys.exit(1)\nif settings.LOAD_SETTINGS_FROM == 'db':\n    updateDsipSettingsTable({'DSIP_LICENSE_STORE': settings.DSIP_LICENSE_STORE})\nupdateConfig(settings, {'DSIP_LICENSE_STORE': settings.DSIP_LICENSE_STORE}, hot_reload=True)\nEOPY\n\nprintdbg 'unmasking services'\nif (( $REINSTALL_NGINX == 1 )); then\n    systemctl unmask nginx.service\nfi\nif (( $REINSTALL_KAMAILIO == 1 )); then\n    systemctl unmask kamailio.service\nfi\nif (( $REINSTALL_DSIPROUTER == 1 )); then\n    systemctl unmask dsiprouter.service\nfi\nif (( $REINSTALL_RTPENGINE == 1 )); then\n    systemctl unmask rtpengine.service\nfi\n\nif (( $RUN_FROM_GUI == 0 )); then\n    printdbg 'restarting services'\n    if (( $REINSTALL_NGINX == 1 )); then\n        systemctl restart nginx\n        if ! systemctl is-active -q nginx; then\n            printerr 'could not start nginx service'\n            exit 1\n        fi\n    fi\n    if (( $REINSTALL_KAMAILIO == 1 )); then\n        systemctl restart kamailio\n        if ! systemctl is-active -q kamailio; then\n            printerr 'could not start kamailio service'\n            exit 1\n        fi\n    fi\n    if (( $REINSTALL_DSIPROUTER == 1 )); then\n        systemctl restart dsiprouter\n        if ! systemctl is-active -q dsiprouter; then\n            printerr 'could not start dsiprouter service'\n            exit 1\n        fi\n    fi\n    if (( $REINSTALL_RTPENGINE == 1 )); then\n        systemctl restart rtpengine\n        if ! systemctl is-active -q rtpengine; then\n            printerr 'could not start rtpengine service'\n            exit 1\n        fi\n    fi\nelse\n    printwarn 'running from the GUI, some services require restarting'\nfi\n\n# make sure the resetConfigsHandler() is nerfed now that we are successful\ntrap - EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n\npprint 'upgrade completed successfully'\n\nexit 0\n"
  },
  {
    "path": "resources/upgrade/v0.75/settings.json",
    "content": "{\n  \"version\": \"0.75\",\n  \"depends\": [\"0.72\", \"0.721\", \"0.73\", \"0.74\"],\n  \"install_location\": \"/opt/dsiprouter\",\n  \"dsiprouter\": [\n    \"migrate.sh\"\n  ]\n}\n"
  },
  {
    "path": "resources/upgrade/v0.76/scripts/migrate.sh",
    "content": "#!/usr/bin/env bash\n\n(( ${DEBUG:-0} == 1 )) && set -x\n\n# where the new project files were downloaded\nNEW_PROJECT_DIR=${NEW_PROJECT_DIR:-/tmp/dsiprouter}\n# project dir where previous repo was located\nOLD_PROJECT_DIR=${DSIP_PROJECT_DIR:-/opt/dsiprouter}\n# the backup directory set by dsiprouter.sh\nCURR_BACKUP_DIR=${CURR_BACKUP_DIR:-\"/var/backups/dsiprouter/$(date '+%s')\"}\n# system config files for dsiprouter\nDSIP_SYSTEM_CONFIG_DIR='/etc/dsiprouter'\n\n# import dsip_lib utility / shared functions (no changes to func definitions in this revision)\n. ${OLD_PROJECT_DIR}/dsiprouter/dsip_lib.sh\n\n# make sure the updates are downloaded and in the correct location\n[[ ! -e \"$NEW_PROJECT_DIR\" ]] && {\n    printerr 'could not find repo to upgrade from'\n    echo \"expected updated repo to be here: $NEW_PROJECT_DIR\"\n    exit 1\n}\n\nprintdbg 'preparing new repo migration'\ncp -af ${OLD_PROJECT_DIR}/venv/. ${NEW_PROJECT_DIR}/venv/\n\n\nprintdbg 'validating system configuration'\nif ! env DSIP_PROJECT_DIR=\"$NEW_PROJECT_DIR\" \\\nDSIP_SYSTEM_CONFIG_DIR=\"$DSIP_SYSTEM_CONFIG_DIR\" \\\n${NEW_PROJECT_DIR}/dsiprouter.sh licensemanager -check tag=DSIP_CORE; then\n    printerr 'A DSIP_CORE license is required to use the auto upgrade feature'\n    echo 'Consider supporting the hard working engineers maintaining this software if you would like to use this feature'\n    exit 1\nfi\n\nprintdbg 'backing up configs just in case the upgrade fails'\nmkdir -p \"$CURR_BACKUP_DIR\"\n# TODO: make the destination paths use our static variables as well\nmkdir -p ${CURR_BACKUP_DIR}/{opt/dsiprouter,etc/dsiprouter,etc/kamailio}\ncp -af ${OLD_PROJECT_DIR}/. ${CURR_BACKUP_DIR}/opt/dsiprouter/\ncp -af ${SYSTEM_KAMAILIO_CONFIG_DIR}/. ${CURR_BACKUP_DIR}/etc/kamailio/\ncp -af ${DSIP_SYSTEM_CONFIG_DIR}/. ${CURR_BACKUP_DIR}/etc/dsiprouter/\nprintdbg \"files were backed up here: ${CURR_BACKUP_DIR}/\"\n\nupdateDsiprouterCli() {\n    FROM_PROJECT_DIR=\"$1\"\n    cp -f ${FROM_PROJECT_DIR}/dsiprouter/dsip_completion.sh /etc/bash_completion.d/dsiprouter\n    cp -f ${FROM_PROJECT_DIR}/resources/man/dsiprouter.1 /usr/share/man/man1/ &&\n    gzip -f /usr/share/man/man1/dsiprouter.1 &&\n    mandb\n}\n\n# if the state files for the services to upgrade were there before\n# and we fail, put them back so the system can recover\nresetConfigsHandler() {\n    printwarn 'upgrade failed, resetting system to previous state'\n\n    cp -af ${CURR_BACKUP_DIR}/etc/. /etc/\n    cp -af ${CURR_BACKUP_DIR}/opt/. /opt/\n\n    updateDsiprouterCli \"$OLD_PROJECT_DIR\"\n\n    dsiprouter configurekam\n\n    trap - EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n    exit 1\n}\ntrap 'resetConfigsHandler $?' EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n\nprintdbg 'migrating dSIPRouter project files'\ncp -rf ${NEW_PROJECT_DIR}/. ${OLD_PROJECT_DIR}/\nupdateDsiprouterCli \"$NEW_PROJECT_DIR\"\ndsiprouter configurekam\n\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]]; then\n    printwarn 'kamailio service requires restarting'\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]]; then\n    printwarn 'dsiprouter service requires restarting'\nfi\n\n# make sure the resetConfigsHandler() is nerfed now that we are successful\ntrap - EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n\npprint 'upgrade completed successfully'\n\nexit 0\n"
  },
  {
    "path": "resources/upgrade/v0.76/settings.json",
    "content": "{\n  \"version\": \"0.76\",\n  \"depends\": [\"0.75\"],\n  \"install_location\": \"/opt/dsiprouter\",\n  \"dsiprouter\": [\n    \"migrate.sh\"\n  ]\n}\n"
  },
  {
    "path": "resources/upgrade/v0.77/clear_defaults.sql",
    "content": "SET FOREIGN_KEY_CHECKS=0;\nTRUNCATE TABLE `address`;\nTRUNCATE TABLE `dr_gateways`;\nTRUNCATE TABLE `dr_gw_lists`;\nTRUNCATE TABLE `dr_rules`;\nSET FOREIGN_KEY_CHECKS=1;"
  },
  {
    "path": "resources/upgrade/v0.77/scripts/migrate.sh",
    "content": "#!/usr/bin/env bash\n\n(( ${DEBUG:-0} == 1 )) && set -x\n\n# where the new project files were downloaded\nNEW_PROJECT_DIR=${NEW_PROJECT_DIR:-/tmp/dsiprouter}\n# project dir where previous repo was located\nOLD_PROJECT_DIR=${DSIP_PROJECT_DIR:-/opt/dsiprouter}\n# the backup directory set by dsiprouter.sh\nCURR_BACKUP_DIR=${CURR_BACKUP_DIR:-\"/var/backups/dsiprouter/$(date '+%s')\"}\n# system config files for dsiprouter\nDSIP_SYSTEM_CONFIG_DIR='/etc/dsiprouter'\n\n# import dsip_lib utility / shared functions (no changes to func definitions in this revision)\n. ${OLD_PROJECT_DIR}/dsiprouter/dsip_lib.sh\n\n# make sure the updates are downloaded and in the correct location\n[[ ! -e \"$NEW_PROJECT_DIR\" ]] && {\n    printerr 'could not find repo to upgrade from'\n    echo \"expected updated repo to be here: $NEW_PROJECT_DIR\"\n    exit 1\n}\n\nprintdbg 'validating system configuration'\nif ! dsiprouter licensemanager -check tag=DSIP_CORE; then\n    printerr 'A DSIP_CORE license is required to use the auto upgrade feature'\n    echo 'Consider supporting the hard working engineers maintaining this software if you would like to use this feature'\n    exit 1\nfi\n\nDISTRO=$(getDistroName)\nDISTRO_VER=$(getDistroVer)\nDISTRO_MAJOR_VER=$(cut -d '.' -f 1 <<<\"$DISTRO_VER\")\nDISTRO_MINOR_VER=$(cut -s -d '.' -f 2 <<<\"$DISTRO_VER\")\ncase \"$DISTRO\" in\ndebian)\n    case \"$DISTRO_VER\" in\n    12|11|10)\n        NEW_KAM_VERSION=${NEW_KAM_VERSION:-\"5.8.3\"}\n        NEW_RTPENGINE_VER=${NEW_RTPENGINE_VER:-\"mr11.5.1.11\"}\n        ;;\n    9)\n        NEW_KAM_VERSION=${NEW_KAM_VERSION:-\"5.5.7\"}\n        NEW_RTPENGINE_VER=${NEW_RTPENGINE_VER:-\"mr9.5.5.1\"}\n        ;;\n    esac\n    ;;\ncentos)\n    case \"$DISTRO_VER\" in\n    8|9)\n        NEW_KAM_VERSION=${NEW_KAM_VERSION:-\"5.8.3\"}\n        NEW_RTPENGINE_VER=${NEW_RTPENGINE_VER:-\"mr11.5.1.11\"}\n        ;;\n    7)\n        NEW_KAM_VERSION=${NEW_KAM_VERSION:-\"5.7.6\"}\n        NEW_RTPENGINE_VER=${NEW_RTPENGINE_VER:-\"mr11.5.1.11\"}\n        ;;\n    esac\n    ;;\namzn)\n    case \"$DISTRO_VER\" in\n    2)\n        NEW_KAM_VERSION=${NEW_KAM_VERSION:-\"5.7.6\"}\n        NEW_RTPENGINE_VER=${NEW_RTPENGINE_VER:-\"mr9.5.5.1\"}\n        ;;\n    esac\n    ;;\nubuntu)\n    case \"$DISTRO_VER\" in\n    24.04)\n        NEW_KAM_VERSION=${NEW_KAM_VERSION:-\"5.8.4\"}\n        NEW_RTPENGINE_VER=${NEW_RTPENGINE_VER:-\"mr11.5.1.11\"}\n        ;;\n    22.04)\n        NEW_KAM_VERSION=${NEW_KAM_VERSION:-\"5.8.3\"}\n        NEW_RTPENGINE_VER=${NEW_RTPENGINE_VER:-\"mr11.5.1.11\"}\n        ;;\n    20.04)\n        NEW_KAM_VERSION=${NEW_KAM_VERSION:-\"5.8.3\"}\n        NEW_RTPENGINE_VER=${NEW_RTPENGINE_VER:-\"mr9.5.5.1\"}\n        ;;\n    esac\n    ;;\nrhel)\n    case \"$DISTRO_MAJOR_VER\" in\n    9)\n        NEW_KAM_VERSION=${NEW_KAM_VERSION:-\"5.8.3\"}\n        NEW_RTPENGINE_VER=${NEW_RTPENGINE_VER:-\"mr11.5.1.11\"}\n        ;;\n    8)\n        NEW_KAM_VERSION=${NEW_KAM_VERSION:-\"5.8.3\"}\n        NEW_RTPENGINE_VER=${NEW_RTPENGINE_VER:-\"mr9.5.5.1\"}\n        ;;\n    esac\n    ;;\nalmalinux)\n    case \"$DISTRO_MAJOR_VER\" in\n    9)\n        NEW_KAM_VERSION=${NEW_KAM_VERSION:-\"5.8.3\"}\n        NEW_RTPENGINE_VER=${NEW_RTPENGINE_VER:-\"mr11.5.1.11\"}\n        ;;\n    8)\n        NEW_KAM_VERSION=${NEW_KAM_VERSION:-\"5.8.3\"}\n        NEW_RTPENGINE_VER=${NEW_RTPENGINE_VER:-\"mr11.5.1.11\"}\n        ;;\n    esac\n    ;;\nrocky)\n    case \"$DISTRO_MAJOR_VER\" in\n    9)\n        NEW_KAM_VERSION=${NEW_KAM_VERSION:-\"5.8.3\"}\n        NEW_RTPENGINE_VER=${NEW_RTPENGINE_VER:-\"mr11.5.1.11\"}\n        ;;\n    8)\n        NEW_KAM_VERSION=${NEW_KAM_VERSION:-\"5.8.3\"}\n        NEW_RTPENGINE_VER=${NEW_RTPENGINE_VER:-\"mr11.5.1.11\"}\n        ;;\n    esac\n    ;;\nesac\nif [[ -z \"${NEW_KAM_VERSION}${NEW_RTPENGINE_VER}\" ]]; then\n    printerr 'unsupported OS version'\n    exit 1\nfi\n\nprintdbg 'retrieving current system settings'\n# NOTE: some magic is being done here to reset specific settings next install\nexport PYTHON_CMD=${OLD_PROJECT_DIR}/venv/bin/python\nDSIP_SYSTEM_CONFIG_DIR='/etc/dsiprouter'\nDSIP_LIB_DIR='/var/lib/dsiprouter'\nDSIP_CONFIG_FILE=${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py\nDSIP_KAMAILIO_CONFIG_FILE=\"${DSIP_SYSTEM_CONFIG_DIR}/kamailio/kamailio.cfg\"\nSYSTEM_KAMAILIO_CONFIG_DIR='/etc/kamailio'\nSYSTEM_RTPENGINE_CONFIG_DIR='/etc/rtpengine'\nexport SET_ROOT_DB_USER=$(getConfigAttrib 'ROOT_DB_USER' ${DSIP_CONFIG_FILE})\nexport ROOT_DB_USER=\"$SET_ROOT_DB_USER\"\nexport SET_ROOT_DB_PASS=$(decryptConfigAttrib 'ROOT_DB_PASS' ${DSIP_CONFIG_FILE})\nexport ROOT_DB_PASS=\"$SET_ROOT_DB_PASS\"\nexport ROOT_DB_HOST=$(getConfigAttrib 'ROOT_DB_HOST' ${DSIP_CONFIG_FILE})\nexport ROOT_DB_PORT=$(getConfigAttrib 'ROOT_DB_PORT' ${DSIP_CONFIG_FILE})\nexport ROOT_DB_NAME=$(getConfigAttrib 'ROOT_DB_NAME' ${DSIP_CONFIG_FILE})\nexport KAM_DB_NAME=$(getConfigAttrib 'KAM_DB_NAME' ${DSIP_CONFIG_FILE})\nexport SET_KAM_DB_HOST=$(getConfigAttrib 'KAM_DB_HOST' ${DSIP_CONFIG_FILE})\nexport KAM_DB_HOST=\"$SET_KAM_DB_HOST\"\nexport SET_KAM_DB_USER=$(getConfigAttrib 'KAM_DB_USER' ${DSIP_CONFIG_FILE})\nexport KAM_DB_USER=\"$SET_KAM_DB_USER\"\nexport SET_KAM_DB_PASS=$(decryptConfigAttrib 'KAM_DB_PASS' ${DSIP_CONFIG_FILE})\nexport KAM_DB_PASS=\"$SET_KAM_DB_PASS\"\nexport SET_DSIP_API_TOKEN=$(decryptConfigAttrib 'DSIP_API_TOKEN' ${DSIP_CONFIG_FILE})\nexport DSIP_API_TOKEN=\"$SET_DSIP_API_TOKEN\"\nexport SET_DSIP_MAIL_PASS=$(decryptConfigAttrib 'MAIL_PASSWORD' ${DSIP_CONFIG_FILE})\nexport MAIL_PASSWORD=\"$DSIP_MAIL_PASS\"\nexport SET_DSIP_IPC_TOKEN=$(decryptConfigAttrib 'DSIP_IPC_PASS' ${DSIP_CONFIG_FILE})\nexport DSIP_IPC_PASS=\"$SET_DSIP_IPC_TOKEN\"\nexport DSIP_LICENSE_STORE=$(getConfigAttrib 'DSIP_LICENSE_STORE' ${DSIP_CONFIG_FILE})\n\n\nprintdbg 'preparing for migration'\nREINSTALL_KAMAILIO=0\nREINSTALL_DSIPROUTER=0\nREINSTALL_RTPENGINE=0\nINSTALL_OPTS=()\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]]; then\n    REINSTALL_KAMAILIO=1\n    INSTALL_OPTS+=(-kam)\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]]; then\n    REINSTALL_DSIPROUTER=1\n    INSTALL_OPTS+=(-dsip)\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\" ]]; then\n    OLD_RTPENGINE_VER=$(rtpengine -v 2>&1 | awk '{print $2}' | cut -d '~' -f 2)\n    if [[ \"$OLD_RTPENGINE_VER\" != \"$NEW_RTPENGINE_VER\" ]]; then\n        REINSTALL_RTPENGINE=1\n        INSTALL_OPTS+=(-rtp)\n    fi\nfi\n\nprintdbg 'backing up configs just in case the upgrade fails'\nmkdir -p \"$CURR_BACKUP_DIR\"\nmkdir -p ${CURR_BACKUP_DIR}/{opt/dsiprouter,var/lib/dsiprouter,etc/dsiprouter,etc/kamailio,etc/rtpengine,etc/systemd/system,lib/systemd/system,etc/default}\ncp -afP ${OLD_PROJECT_DIR}/. ${CURR_BACKUP_DIR}/opt/dsiprouter/\ncp -afP ${DSIP_LIB_DIR}/. ${CURR_BACKUP_DIR}/var/lib/dsiprouter/\ncp -afP ${SYSTEM_KAMAILIO_CONFIG_DIR}/. ${CURR_BACKUP_DIR}/etc/kamailio/\ncp -afP ${DSIP_SYSTEM_CONFIG_DIR}/. ${CURR_BACKUP_DIR}/etc/dsiprouter/\ncp -afP ${SYSTEM_RTPENGINE_CONFIG_DIR}/. ${CURR_BACKUP_DIR}/etc/rtpengine/\ncp -afP /etc/systemd/system/{dsiprouter,kamailio,rtpengine,dsip-init,mariadb}.service ${CURR_BACKUP_DIR}/etc/systemd/system/ 2>/dev/null\ncp -afP /lib/systemd/system/{dsiprouter,kamailio,rtpengine,dsip-init,mariadb}.service ${CURR_BACKUP_DIR}/lib/systemd/system/ 2>/dev/null\ncp -afP /etc/default/{kamailio,rtpengine}* ${CURR_BACKUP_DIR}/etc/default/\nprintdbg \"files were backed up here: ${CURR_BACKUP_DIR}/\"\n\n# if the state files for the services to upgrade were there before\n# and we fail, put them back so the system can recover\nresetConfigsHandler() {\n    printwarn 'upgrade failed, resetting system to previous state'\n\n    if (( $REINSTALL_KAMAILIO == 1 )); then\n        systemctl unmask kamailio.service\n    fi\n    if (( $REINSTALL_DSIPROUTER == 1 )); then\n        systemctl unmask dsiprouter.service\n    fi\n    if (( $REINSTALL_RTPENGINE == 1 )); then\n        systemctl unmask rtpengine.service\n    fi\n\n    cp -afP ${CURR_BACKUP_DIR}/etc/. /etc/\n    cp -afP ${CURR_BACKUP_DIR}/lib/. /lib/\n    cp -afP ${CURR_BACKUP_DIR}/opt/. /opt/\n    cp -afP ${CURR_BACKUP_DIR}/var/. /var/\n    systemctl daemon-reload\n\n    if (( $REINSTALL_KAMAILIO == 1 )); then\n        # automatically created in dsiprouter.sh when installKamailio() runs\n        [[ -e ${CURR_BACKUP_DIR}/db.sql ]] && withRootDBConn mysql <${CURR_BACKUP_DIR}/db.sql\n        [[ -e ${CURR_BACKUP_DIR}/user.sql ]] && withRootDBConn mysql <${CURR_BACKUP_DIR}/user.sql\n    fi\n\n    trap - EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n    exit 1\n}\ntrap 'resetConfigsHandler $?' EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n\n# conditionally reinstalled\nprintdbg 'preparing service updates'\nif (( $REINSTALL_KAMAILIO == 1 )); then\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\n    rm -rf ${SRC_DIR}/kamailio\n    rm -f \"$DSIP_KAMAILIO_CONFIG_FILE\"\n\n    systemctl mask kamailio.service\nfi\nif (( $REINSTALL_DSIPROUTER == 1 )); then\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.dsiproutercliinstalled\n\n    systemctl mask dsiprouter.service\nfi\nif (( $REINSTALL_RTPENGINE == 1 )); then\n    rm -rf ${SRC_DIR}/rtpengine\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\n\n    systemctl mask rtpengine.service\nfi\n\nprintdbg 'storing kamailio database data'\n(\n    withRootDBConn mysqldump --single-transaction --skip-opt --skip-triggers --no-create-db --no-create-info \\\n        --replace --complete-insert --hex-blob --skip-comments --databases \"$KAM_DB_NAME\"\n) >${CURR_BACKUP_DIR}/data.sql\n\nif (( $REINSTALL_DSIPROUTER == 1 )); then\n    printdbg 'migrating dSIPRouter project files'\n    cp -rf ${NEW_PROJECT_DIR}/. ${OLD_PROJECT_DIR}/\n    export DSIP_PROJECT_DIR=${OLD_PROJECT_DIR}\n\n    printdbg 'migrating dSIPRouter settings'\n    (\n        # magic bash hacking\n        function exit() { :; }\n        export -f exit\n        source ${DSIP_PROJECT_DIR}/dsiprouter.sh &>/dev/null\n        unset -f exit\n        setStaticScriptSettings\n        setDynamicScriptSettings\n        # more magic environment munging\n        dsiprouter configuredsip\n        # the rest of the settings are configured during reinstall\n    ) || {\n        printerr 'Failed migrating dSIPRouter settings'\n        exit 1\n    }\n\n    rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.reposconfigured\n    rm -f /lib/systemd/system/dsip-init.service\nfi\n\n# source the new dsip_lib functions\n# WARNING: from here on we are explicitly using the NEW definitions of the dsip_lib funcs\n# NOTE: resetConfigsHandler() above will still use the new definitions (lazy loading)\nexport PYTHON_CMD=\"${DSIP_PROJECT_DIR}/venv/bin/python\"\n. ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\n\nprintdbg 'upgrading services'\n# we clear environment here to make sure we get new static settings on install\nenv -i CURR_BACKUP_DIR=\"$CURR_BACKUP_DIR\" HOME=\"$HOME\" LANG=\"$LANG\" LANGUAGE=\"$LANGUAGE\" LC_ALL=\"$LC_ALL\" PATH=\"$PATH\" PWD=\"$PWD\" \\\n    ${DSIP_PROJECT_DIR}/dsiprouter.sh install ${INSTALL_OPTS[@]}\n\nif (( $? != 0 )); then\n    printerr 'failed upgrading services'\n    exit 1\nfi\n\nif (( $REINSTALL_KAMAILIO == 1 )); then\n    printdbg 'migrating kamailio database'\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql <${CURR_BACKUP_DIR}/user.sql &&\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql <${DSIP_PROJECT_DIR}/resources/upgrade/v0.77/clear_defaults.sql &&\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql <${CURR_BACKUP_DIR}/data.sql || {\n        printerr 'failed migrating kamailio database'\n        exit 1\n    }\nfi\n\nprintdbg 'unmasking services'\nif (( $REINSTALL_KAMAILIO == 1 )); then\n    systemctl unmask kamailio.service\nfi\nif (( $REINSTALL_DSIPROUTER == 1 )); then\n    systemctl unmask dsiprouter.service\nfi\nif (( $REINSTALL_RTPENGINE == 1 )); then\n    systemctl unmask rtpengine.service\nfi\n\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]]; then\n    printwarn 'kamailio service requires restarting'\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]]; then\n    printwarn 'dsiprouter service requires restarting'\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.rtpengineinstalled\" ]]; then\n    printwarn 'rtpengine service requires restarting'\nfi\n\n# make sure the resetConfigsHandler() is nerfed now that we are successful\ntrap - EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n\npprint 'upgrade completed successfully'\n\nexit 0\n"
  },
  {
    "path": "resources/upgrade/v0.77/settings.json",
    "content": "{\n  \"version\": \"0.77\",\n  \"depends\": [\"0.76\"],\n  \"install_location\": \"/opt/dsiprouter\",\n  \"dsiprouter\": [\n    \"migrate.sh\"\n  ]\n}\n"
  },
  {
    "path": "resources/upgrade/v0.78/dsip-fwd-new.sql",
    "content": "DROP TABLE IF EXISTS dsip_prefix_mapping;\nDROP VIEW IF EXISTS dsip_prefix_mapping;\nCREATE VIEW dsip_prefix_mapping AS\n  SELECT\n    prefix,\n    CAST(ruleid AS char) AS ruleid,\n    CAST(priority AS char) AS priority,\n    '0' AS key_type,\n    '0' AS value_type\n  FROM dr_rules\n  WHERE groupid='9000';"
  },
  {
    "path": "resources/upgrade/v0.78/dsip-fwd-old.sql",
    "content": "DROP TABLE IF EXISTS dsip_prefix_mapping;\nDROP VIEW IF EXISTS dsip_prefix_mapping;\nCREATE VIEW dsip_prefix_mapping AS\n  SELECT\n    prefix,\n    CAST(ruleid AS char) AS ruleid,\n    CAST(priority AS char) AS priority,\n    '0' AS key_type,\n    '0' AS value_type\n  FROM dr_rules;"
  },
  {
    "path": "resources/upgrade/v0.78/scripts/migrate.sh",
    "content": "#!/usr/bin/env bash\n\n(( ${DEBUG:-0} == 1 )) && set -x\n\n# where the new project files were downloaded\nNEW_PROJECT_DIR=${NEW_PROJECT_DIR:-/tmp/dsiprouter}\n# project dir where previous repo was located\nOLD_PROJECT_DIR=${DSIP_PROJECT_DIR:-/opt/dsiprouter}\n# the backup directory set by dsiprouter.sh\nCURR_BACKUP_DIR=${CURR_BACKUP_DIR:-\"/var/backups/dsiprouter/$(date '+%s')\"}\n# system config files for dsiprouter\nDSIP_SYSTEM_CONFIG_DIR='/etc/dsiprouter'\n\n# import dsip_lib utility / shared functions (no changes to func definitions in this revision)\n. ${OLD_PROJECT_DIR}/dsiprouter/dsip_lib.sh\n\n# make sure the updates are downloaded and in the correct location\n[[ ! -e \"$NEW_PROJECT_DIR\" ]] && {\n    printerr 'could not find repo to upgrade from'\n    echo \"expected updated repo to be here: $NEW_PROJECT_DIR\"\n    exit 1\n}\n\nprintdbg 'validating system configuration'\nif ! dsiprouter licensemanager -check tag=DSIP_CORE; then\n    printerr 'A DSIP_CORE license is required to use the auto upgrade feature'\n    echo 'Consider supporting the hard working engineers maintaining this software if you would like to use this feature'\n    exit 1\nfi\n\nprintdbg 'retrieving current system settings'\nexport PYTHON_CMD=${OLD_PROJECT_DIR}/venv/bin/python\nDSIP_SYSTEM_CONFIG_DIR='/etc/dsiprouter'\nDSIP_CONFIG_FILE=${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py\nSYSTEM_KAMAILIO_CONFIG_DIR='/etc/kamailio'\nexport ROOT_DB_USER=$(getConfigAttrib 'ROOT_DB_USER' ${DSIP_CONFIG_FILE})\nexport ROOT_DB_PASS=$(decryptConfigAttrib 'ROOT_DB_PASS' ${DSIP_CONFIG_FILE})\nexport ROOT_DB_HOST=$(getConfigAttrib 'ROOT_DB_HOST' ${DSIP_CONFIG_FILE})\nexport ROOT_DB_PORT=$(getConfigAttrib 'ROOT_DB_PORT' ${DSIP_CONFIG_FILE})\nexport KAM_DB_NAME=$(getConfigAttrib 'KAM_DB_NAME' ${DSIP_CONFIG_FILE})\n\nprintdbg 'preparing for migration'\nexport DSIP_PROJECT_DIR=${OLD_PROJECT_DIR}\nUPDATE_KAMAILIO=0\nUPDATE_DSIPROUTER=0\nREQUIRED_RELOADS=()\nRELOAD_CMD=(dsiprouter restart)\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled\" ]]; then\n    UPDATE_KAMAILIO=1\n    REQUIRED_RELOADS+=(kamailio)\n    RELOAD_CMD+=(-kam)\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.dsiprouterinstalled\" ]]; then\n    UPDATE_DSIPROUTER=1\n    REQUIRED_RELOADS+=(dsiprouter)\n    RELOAD_CMD+=(-dsip)\nfi\nif [[ -f \"${DSIP_SYSTEM_CONFIG_DIR}/.nginxinstalled\" ]]; then\n    UPDATE_NGINX=1\nfi\n\nprintdbg 'backing up configs just in case the upgrade fails'\nmkdir -p \"$CURR_BACKUP_DIR\"\nmkdir -p ${CURR_BACKUP_DIR}/{opt/dsiprouter,etc/dsiprouter,etc/kamailio,etc/nginx}\ncp -afP ${OLD_PROJECT_DIR}/. ${CURR_BACKUP_DIR}/opt/dsiprouter/\ncp -afP ${SYSTEM_KAMAILIO_CONFIG_DIR}/. ${CURR_BACKUP_DIR}/etc/kamailio/\ncp -afP /etc/nginx/. ${CURR_BACKUP_DIR}/etc/nginx/\nprintdbg \"files were backed up here: ${CURR_BACKUP_DIR}/\"\n\n# revert the changes we made on failure\nresetConfigsHandler() {\n    printwarn 'upgrade failed, resetting system to previous state'\n\n    cp -afP ${CURR_BACKUP_DIR}/etc/. /etc/\n    cp -afP ${CURR_BACKUP_DIR}/opt/. /opt/\n\n    if (( $UPDATE_KAMAILIO == 1 )); then\n        # DSIP_PROJECT_DIR may be the old or new project based on when this fails\n        [[ -e \"${DSIP_PROJECT_DIR}/resources/upgrade/v0.78/dsip-fwd-old.sql\" ]] && {\n            withRootDBConn --db=\"$KAM_DB_NAME\" mysql <${DSIP_PROJECT_DIR}/resources/upgrade/v0.78/dsip-fwd-old.sql\n            kamcmd htable.reload prefix_to_route\n        }\n    fi\n\n    trap - EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n    exit 1\n}\ntrap 'resetConfigsHandler $?' EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n\n# we want the project files updated first\nif (( $UPDATE_DSIPROUTER == 1 )); then\n    printdbg 'migrating dSIPRouter project files'\n    cp -rf ${NEW_PROJECT_DIR}/. ${OLD_PROJECT_DIR}/\n    export DSIP_PROJECT_DIR=${NEW_PROJECT_DIR}\n\n    printdbg 'migrating dSIPRouter settings'\n    setConfigAttrib 'VERSION' '0.78' ${DSIP_CONFIG_FILE} -q || {\n        printerr 'Failed migrating dSIPRouter settings'\n        exit 1\n    }\n\n    printdbg 'regenerating dSIPRouter documentation'\n    (\n        cd ${DSIP_PROJECT_DIR}/docs &&\n        make -j $(nproc) html\n    ) || {\n        printerr 'Failed generating documentation'\n        exit 1\n    }\n\n    printdbg 'syncing fusionpbx domains'\n    sudo -u dsiprouter ${PYTHON_CMD} ${DSIP_PROJECT_DIR}/gui/dsiprouter_cron.py fusionpbx sync || {\n        printerr 'Failed syncing fusionpbx domains'\n        exit 1\n    }\nfi\n\n# NOTE: no change in dsip_lib.sh so we do not need to source the new one\n\nif (( $UPDATE_KAMAILIO == 1 )); then\n    printdbg 'updating kamailio configuration'\n    dsiprouter configurekam\n    kamailio -c >/dev/null || {\n        printerr 'Failed migrating kamailio configuration to newer version'\n        exit 1\n    }\n    withRootDBConn --db=\"$KAM_DB_NAME\" mysql <${DSIP_PROJECT_DIR}/resources/upgrade/v0.78/dsip-fwd-new.sql || {\n        printerr 'Failed migrating kamailio database'\n        exit 1\n    }\nfi\n\nif (( $UPDATE_NGINX == 1 )); then\n    printdbg 'updating nginx configuration'\n    dsiprouter chown -nginx\nfi\n\nif (( $UPDATE_KAMAILIO == 1 || $UPDATE_DSIPROUTER == 1 )); then\n    printwarn 'The following services require restarting:'\n    for SVC in ${REQUIRED_RELOADS[@]}; do\n        echo \"- $SVC\"\n    done\n    echo ''\n    echo \"To reload these services do $(printwarn -n ONE) of the following:\"\n    echo '- press the \"Reload\" button in the GUI and then click \"Reload dSIPRouter\"'\n    echo '- run this command in the CLI \"'${RELOAD_CMD[@]}'\"'\n    echo ''\nfi\n\n# make sure the resetConfigsHandler() is nerfed now that we are successful\ntrap - EXIT SIGHUP SIGINT SIGQUIT SIGTERM\n\npprint 'upgrade completed successfully'\n\nexit 0\n"
  },
  {
    "path": "resources/upgrade/v0.78/settings.json",
    "content": "{\n  \"version\": \"0.78\",\n  \"depends\": [\"0.77\"],\n  \"install_location\": \"/opt/dsiprouter\",\n  \"dsiprouter\": [\n    \"migrate.sh\"\n  ]\n}\n"
  },
  {
    "path": "resources/uploadOutRoute.py",
    "content": "import requests\n\n# set per your own configs (get prefixs from gui/util/conversions.py)\nprocessed_prefixs = [\n'011',\n]\nname = 'Test Calling'\ngwlist = '#9'\n\n# set per your own configs\nhost = \"10.10.10.154\"\nusername = 'admin'\npassword = 'admin'\n\n# auth for session cookie\nURL = 'http://{}:5000/login'.format(host)\npayload = {\n    'username': username,\n    'password': password,\n    'nextpage': ''\n}\nheaders = {\n    'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',\n    'Accept-Encoding':'gzip, deflate',\n    'Accept-Language':'en-US,en;q=0.9',\n    'Cache-Control':'max-age=0',\n    'Connection':'keep-alive',\n    'Content-Type':'application/x-www-form-urlencoded',\n    'DNT':'1',\n    'Host':'{}:5000'.format(host),\n    'Origin':'http://{}:5000'.format(host),\n    'Referer':'http://{}:5000/'.format(host),\n    'Upgrade-Insecure-Requests':'1',\n    'User-Agent':'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'\n}\n\nr1 = requests.post(URL, data=payload, headers=headers)\nprint(\"LOGIN: {}\\n\".format(r1.status_code))\n\n\n# add outbound routes\nURL = 'http://{}:5000/outboundroutes'.format(host)\npayloads = [{\n    'ruleid': '',\n    'from_prefix': '',\n    'timerec': '',\n    'priority': '',\n    'prefix': prefix,\n    'name': name,\n    'gwlist': gwlist\n} for prefix in processed_prefixs]\nheaders = {\n    'Host':'{}:5000'.format(host),\n    'Connection':'keep-alive',\n    'Cache-Control':'max-age=0',\n    'Origin':'http://{}:5000'.format(host),\n    'Upgrade-Insecure-Requests':'1',\n    'DNT':'1',\n    'Content-Type':'application/x-www-form-urlencoded',\n    'User-Agent':'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36',\n    'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',\n    'Referer':'http://{}:5000/outboundroutes'.format(host),\n    'Accept-Encoding':'gzip, deflate',\n    'Accept-Language':'en-US,en;q=0.9'\n}\n\nfor payload in payloads:\n    r = requests.post(URL, data=payload, headers=headers, cookies=r1.cookies)\n    print(\"OUTBOUNDROUTES: {}\".format(r.status_code))\n\nexit(0)\n"
  },
  {
    "path": "rtpengine/almalinux/install.sh",
    "content": "#!/usr/bin/env bash\n\n# TODO: update based off latest changes in rtpengine/amzn/install.sh\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\n# search for RPM using almalinux vault repos\n# not guaranteed to find an RPM, returns 1 if not found\n# arguments:\n#   $1 == rpm to search for\n# options:\n#   -af <archictecture>\n#   -dm <distro major version>\nfunction vaultSearch() {\n    local RPM_SEARCH DISTRO_MAJVER ARCH_FILTER REPO SEARCH_RESULTS VERSIONS_TO_SEARCH SEARCH_URL REPOS_TO_SEARCH\n\n    while (( $# > 0 )); do\n        # last arg is user and database\n        if (( $# == 1 )); then\n            RPM_SEARCH=\"$1\"\n            shift\n            break\n        fi\n\n        case \"$1\" in\n            -af)\n                shift\n                ARCH_FILTER=\"$1\"\n                shift\n                ;;\n            -dm)\n                shift\n                DISTRO_MAJVER=\"$1\"\n                shift\n                ;;\n        esac\n    done\n\n    VERSIONS_TO_SEARCH=($(\n        curl -s https://raw.repo.almalinux.org/vault/ |\n        perl -e \"\\$distro_majver='$DISTRO_MAJVER'; @matches=();\" -0777 -e '\n            $html = do { local $/; <STDIN> };\n            @matches = ($html =~ m%(?<=\\<a href=[\"'\"'\"'])(${distro_majver}\\.[0-9a-zA-Z-]+)/(?=[\"'\"'\"']\\>)%g);\n            foreach my $match (@matches) { print \"${match}\\n\"; }\n        ' 2>/dev/null\n    ))\n\n    for VAULT_VER in ${VERSIONS_TO_SEARCH[@]}; do\n        REPOS_TO_SEARCH=($(\n            curl -s https://repo.almalinux.org/vault/${VAULT_VER}/metadata/${ARCH_FILTER}/composeinfo.json |\n            jq -re '.payload.variants | keys[]'\n        ))\n        (( $? == 0 )) || continue\n        for REPO in ${REPOS_TO_SEARCH[@]}; do\n            SEARCH_URL=\"https://repo.almalinux.org/vault/${VAULT_VER}/${REPO}/${ARCH_FILTER}/os/Packages/${RPM_SEARCH}.rpm\"\n            if (( $(curl -s -I -w \"%{http_code}\" -o /dev/null \"$SEARCH_URL\") == 200 )); then\n                echo \"$SEARCH_URL\"\n                return 0\n            fi\n        done\n    done\n\n    return 1\n}\n\n# try installing in the following order:\n# 1: headers from repos\n# 2: headers from vault repos\nfunction installKernelDevHeaders {\n    local DISTRO_MAJVER=\"$DISTRO_MAJVER\"\n    local OS_ARCH=\"$OS_ARCH\"\n    local OS_KERNEL=\"$OS_KERNEL\"\n    local KERN_DEV KERN_HDR\n\n    dnf install -y kernel-devel-${OS_KERNEL} kernel-headers-${OS_KERNEL} || {\n        KERN_DEV=$(vaultSearch -af $OS_ARCH -dm $DISTRO_MAJVER \"kernel-devel-${OS_KERNEL}\") || return 1\n        KERN_HDR=$(vaultSearch -af $OS_ARCH -dm $DISTRO_MAJVER \"kernel-headers-${OS_KERNEL}\") || return 1\n\n        dnf install -y \"$KERN_DEV\" &&\n        dnf install -y \"$KERN_HDR\"\n    }\n}\n\n# compile and install rtpengine from RPM's\nfunction install {\n    local RTPENGINE_RPM_VER BUILD_KERN_VERSIONS\n    local REBOOT_REQUIRED=0\n    local OS_ARCH=$(uname -m)\n    local OS_KERNEL=$(uname -r)\n    local RHEL_BASE_VER=$(rpm -E %{rhel})\n    local DISTRO_VER=$(source /etc/os-release; echo \"$VERSION_ID\")\n    local DISTRO_MAJVER=$(cut -d '.' -f 1 <<<\"$DISTRO_VER\")\n    local NPROC=$(nproc)\n\n    # Install required libraries\n    dnf install -y distribution-gpg-keys &&\n    dnf install -y epel-release &&\n    dnf install -y almalinux-release-devel &&\n    if (( ${DISTRO_MAJVER} == 9 )); then\n        dnf config-manager -y --set-enabled crb &&\n        dnf install -y http://rpm.dsiprouter.org/dsiprouter-repo.noarch.rpm\n    elif (( ${DISTRO_MAJVER} == 8 )); then\n        dnf config-manager -y --set-enabled powertools &&\n        rpmkeys --import /usr/share/distribution-gpg-keys/rpmfusion/RPM-GPG-KEY-rpmfusion-free-el-${RHEL_BASE_VER} &&\n        dnf --setopt=localpkg_gpgcheck=1 install -y https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-${RHEL_BASE_VER}.noarch.rpm\n    fi &&\n    dnf install -y jq curl gcc glib2 glib2-devel zlib zlib-devel openssl openssl-devel pcre pcre-devel libcurl libcurl-devel \\\n        xmlrpc-c xmlrpc-c-devel libpcap libpcap-devel hiredis hiredis-devel json-glib json-glib-devel libevent libevent-devel \\\n        iptables iptables-devel xmlrpc-c-devel gperf redhat-rpm-config rpm-build pkgconfig spandsp-devel pandoc \\\n        freetype-devel fontconfig-devel libxml2-devel nc dkms logrotate rsyslog perl perl-IPC-Cmd bc libwebsockets-devel \\\n        gperf gperftools gperftools-devel gperftools-libs gzip mariadb-devel perl-Config-Tiny spandsp librabbitmq librabbitmq-devel \\\n        ffmpeg ffmpeg-devel libjpeg-turbo-devel mosquitto-devel opus-devel iptables-legacy-devel gcc-toolset-14 &&\n    installKernelDevHeaders\n\n    if (( $? != 0 )); then\n        printerr \"Problem with installing the required libraries for RTPEngine\"\n        return 1\n    fi\n\n    BUILD_KERN_VERSIONS=$(joinwith '' ',' '' $(rpm -q kernel-headers | sed 's/kernel-headers-//g'))\n\n    # rtpengine >= mr11.3.1.1 requires curl >= 7.43.0\n    if versionCompare \"$(tr -d '[a-zA-Z]' <<<\"$RTPENGINE_VER\")\" gteq \"11.3.1.1\"; then\n        if versionCompare \"$(curl -V | head -1 | awk '{print $2}')\" lt \"7.43.0\"; then\n            printdbg 'curl version is not recent enough.. compiling curl 7.8.0'\n            if [[ ! -d ${SRC_DIR}/curl ]]; then\n                (\n                    cd ${SRC_DIR} &&\n                    curl -sL https://curl.haxx.se/download/curl-7.80.0.tar.gz 2>/dev/null |\n                    tar -xzf - --transform 's%curl-7.80.0%curl%';\n                )\n            fi\n            (\n                cd ${SRC_DIR}/curl &&\n                ./configure --prefix=/usr --libdir=/usr/lib64 --with-ssl &&\n                make -j $NPROC &&\n                make -j $NPROC install &&\n                ldconfig\n            )\n            if (( $? != 0 )); then\n                printerr 'Failed to compile curl'\n                return 1\n            fi\n        fi\n    fi\n\n    # reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/rtpengine ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/rtpengine)\" != \"${RTPENGINE_VER}\" ]]; then\n            rm -rf ${SRC_DIR}/rtpengine\n            git clone --depth 1 -c advice.detachedHead=false -b ${RTPENGINE_VER} https://github.com/sipwise/rtpengine.git ${SRC_DIR}/rtpengine\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${RTPENGINE_VER} https://github.com/sipwise/rtpengine.git ${SRC_DIR}/rtpengine\n    fi\n\n    # apply our patches\n    (\n        cd ${SRC_DIR}/rtpengine &&\n        patch -p1 -N <${DSIP_PROJECT_DIR}/rtpengine/el-${RTPENGINE_VER}.patch\n    )\n    if (( $? > 1 )); then\n        printerr 'Failed patching RTPEngine files prior to build'\n        return 1\n    fi\n\n    RTPENGINE_RPM_VER=$(grep -oP 'Version:.+?\\K[\\w\\.\\~\\+]+' ${SRC_DIR}/rtpengine/el/rtpengine.spec)\n    RPM_BUILD_ROOT=\"${HOME}/rpmbuild\"\n    rm -rf ${RPM_BUILD_ROOT} 2>/dev/null\n    mkdir -p ${RPM_BUILD_ROOT}/SOURCES &&\n    (\n        source scl_source enable gcc-toolset-14 &&\n        cd ${SRC_DIR} &&\n        tar -czf ${RPM_BUILD_ROOT}/SOURCES/ngcp-rtpengine-${RTPENGINE_RPM_VER}.tar.gz \\\n            --transform=\"s%^rtpengine%ngcp-rtpengine-$RTPENGINE_RPM_VER%g\" rtpengine/ &&\n        echo \"%__make $(which make) -j $NPROC\" >~/.rpmmacros &&\n        # fix for BUG: \"exec_prefix: command not found\"\n        function exec_prefix() { echo -n '/usr'; } && export -f exec_prefix &&\n        # build the RPM's\n        rpmbuild -ba --define \"kversion $BUILD_KERN_VERSIONS\" ${SRC_DIR}/rtpengine/el/rtpengine.spec &&\n        rm -f ~/.rpmmacros && unset -f exec_prefix &&\n        systemctl mask ngcp-rtpengine-daemon.service\n\n        # install the RPM's\n        dnf install -y ${RPM_BUILD_ROOT}/RPMS/${OS_ARCH}/ngcp-rtpengine-${RTPENGINE_RPM_VER}*.rpm \\\n            ${RPM_BUILD_ROOT}/RPMS/noarch/ngcp-rtpengine-dkms-${RTPENGINE_RPM_VER}*.rpm \\\n            ${RPM_BUILD_ROOT}/RPMS/${OS_ARCH}/ngcp-rtpengine-kernel-${RTPENGINE_RPM_VER}*.rpm\n    )\n\n    if (( $? != 0 )); then\n        printerr \"Problems occurred compiling rtpengine\"\n        return 1\n    fi\n\n    # warn user if kernel module not loaded yet\n    if (( $REBOOT_REQUIRED == 1 )); then\n        printwarn \"A reboot is required to load the RTPEngine kernel module\"\n    fi\n\n    # ensure config dirs exist\n    mkdir -p /run/rtpengine ${SYSTEM_RTPENGINE_CONFIG_DIR}\n    chown -R rtpengine:rtpengine /run/rtpengine\n\n    # setup rtpengine defaults file\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/configs/default.conf /etc/default/rtpengine.conf\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # give rtpengine permissions in selinux\n    semanage port -a -t rtp_media_port_t -p udp ${RTP_PORT_MIN}-${RTP_PORT_MAX} ||\n    semanage port -m -t rtp_media_port_t -p udp ${RTP_PORT_MIN}-${RTP_PORT_MAX}\n\n    # Setup Firewall rules for RTPEngine\n    firewall-cmd --zone=public --add-port=${RTP_PORT_MIN}-${RTP_PORT_MAX}/udp --permanent\n    firewall-cmd --reload\n\n    # Setup RTPEngine Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rtpengine.conf /etc/rsyslog.d/rtpengine.conf\n    touch /var/log/rtpengine.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/rtpengine /etc/logrotate.d/rtpengine\n\n    # Setup tmp files\n    echo \"d /run/rtpengine/rtpengine.pid  0755 rtpengine rtpengine - -\" > /etc/tmpfiles.d/rtpengine.conf\n\n    # Reconfigure systemd service files\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/systemd/rtpengine-v3.service /lib/systemd/system/rtpengine.service\n    chmod 644 /lib/systemd/system/rtpengine.service\n    systemctl daemon-reload\n    systemctl enable rtpengine\n\n    # preliminary check that rtpengine actually installed\n    if cmdExists rtpengine; then\n        return 0\n    else\n        return 1\n    fi\n}\n\n# Remove RTPEngine\nfunction uninstall {\n    systemctl stop rtpengine\n    systemctl disable rtpengine\n    rm -f /{etc,lib}/systemd/system/rtpengine.service 2>/dev/null\n    systemctl daemon-reload\n\n    yum remove -y ngcp-rtpengine\\*\n\n    rm -f /usr/bin/rtpengine\n    rm -f /etc/rsyslog.d/rtpengine.conf\n    rm -f /etc/logrotate.d/rtpengine\n\n    # remove our selinux changes\n    semanage port -D -t rtp_media_port_t -p udp\n\n    # remove our firewall changes\n    firewall-cmd --zone=public --remove-port=${RTP_PORT_MIN}-${RTP_PORT_MAX}/udp --permanent\n    firewall-cmd --reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "rtpengine/amzn/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\n# search for RPM using external APIs mirrors and archives\n# not guaranteed to find an RPM, outputs empty string if search fails\n# arguments:\n#   $1 == rpm to search for\n# options:\n#   -a <arch filter>\n#   --arch=<arch filter>\n#   -d <distro filter>\n#   --distro=<distro filter>\n#   -f <grep filter>\n#   --filter=<grep filter>\n#function rpmSearch() {\n#    local RPM_SEARCH=\"\" DISTRO_FILTER=\"\" ARCH_FILTER=\"\" GREP_FILTER=\"\" SEARCH_RESULTS=\"\"\n#\n#    while (( $# > 0 )); do\n#        # last arg is user and database\n#        if (( $# == 1 )); then\n#            RPM_SEARCH=\"$1\"\n#            shift\n#            break\n#        fi\n#\n#        case \"$1\" in\n#            -a)\n#                shift\n#                ARCH_FILTER=\"$1\"\n#                shift\n#                ;;\n#            --arch=*)\n#                ARCH_FILTER=\"$(echo \"$1\" | cut -d '=' -f 2)\"\n#                shift\n#                ;;\n#            -d)\n#                shift\n#                DISTRO_FILTER=\"$1\"\n#                shift\n#                ;;\n#            --distro=*)\n#                DISTRO_FILTER=\"$(echo \"$1\" | cut -d '=' -f 2)\"\n#                shift\n#                ;;\n#            -f)\n#                shift\n#                GREP_FILTER=\"$1\"\n#                shift\n#                ;;\n#            --filter=*)\n#                GREP_FILTER=\"$(echo \"$1\" | cut -d '=' -f 2)\"\n#                shift\n#                ;;\n#        esac\n#    done\n#\n#    # if grep filter not set it defaults to rpm search\n#    if [[ -z \"$GREP_FILTER\" ]]; then\n#        GREP_FILTER=\"${RPM_SEARCH}\"\n#    fi\n#\n#    # grab the results of the search using an API on rpmfind.net\n#    SEARCH_RESULTS=$(\n#        curl -sL \"https://www.rpmfind.net/linux/rpm2html/search.php?query=${RPM_SEARCH}&system=${DISTRO_FILTER}&arch=${ARCH_FILTER}\" 2>/dev/null |\n#            perl -e \"\\$rpmfind_base_url='https://rpmfind.net'; \\$rpm_search='${RPM_SEARCH}'; @matches=(); \" -0777 -e \\\n#                '$html = do { local $/; <STDIN> };\n#                @matches = ($html =~ m%(?<=\\<a href=[\"'\"'\"'])([-a-zA-Z0-9\\@\\:\\%\\._\\+~#=/]*${rpm_search}[-a-zA-Z0-9\\@\\:\\%\\._\\+\\~\\#\\=]*\\.rpm)(?=[\"'\"'\"']\\>)%g);\n#                foreach my $match (@matches) { print \"${rpmfind_base_url}${match}\\n\"; }' 2>/dev/null |\n#            grep -m 1 \"${GREP_FILTER}\"\n#    )\n#\n#    if [[ -n \"$SEARCH_RESULTS\" ]]; then\n#        echo \"$SEARCH_RESULTS\"\n#    fi\n#}\n\n# compile and install rtpengine from RPM's\nfunction install {\n    local RTPENGINE_RPM_VER BUILD_KERN_VERSIONS TMP\n    local OS_ARCH=$(uname -m)\n    local OS_KERNEL=$(uname -r)\n    local NPROC=$(nproc)\n\n    # Install required libraries\n    amazon-linux-extras enable -y GraphicsMagick1.3 >/dev/null\n    amazon-linux-extras enable -y redis6 >/dev/null\n    amazon-linux-extras install -y epel >/dev/null\n    amazon-linux-extras enable mariadb10.5 >/dev/null\n    yum groupinstall --setopt=group_package_types=mandatory,default -y 'Development Tools'\n    yum install -y gcc-10 gcc10-c++ glib2 glib2-devel zlib zlib-devel pcre2 pcre2-devel libcurl libcurl-devel libjpeg-turbo-devel \\\n        xmlrpc-c xmlrpc-c-devel libpcap libpcap-devel hiredis hiredis-devel json-glib json-glib-devel libevent \\\n        libevent-devel iptables iptables-devel xmlrpc-c-devel gperf redhat-rpm-config rpm-build rpmrebuild cmake3 \\\n        pkgconfig freetype-devel fontconfig-devel libxml2-devel nc dkms logrotate rsyslog perl perl-IPC-Cmd libtiff-devel \\\n        bc libwebsockets-devel gperf gperftools gperftools-devel gperftools-libs gzip perl-Config-Tiny libbluray-devel \\\n        libjpeg-turbo-devel mosquitto-devel glib2-devel xmlrpc-c-devel hiredis-devel libpcap-devel libevent-devel \\\n        json-glib-devel gperf nasm yasm yasm-devel autoconf automake bzip2 bzip2-devel libtool make mercurial libtiff-devel \\\n        mariadb-libs mariadb-devel pandoc\n\n    if (( $? != 0 )); then\n        printerr \"Could not install the required libraries for RTPEngine\"\n        return 1\n    fi\n\n    yum install -y kernel-devel-${OS_KERNEL} kernel-headers-${OS_KERNEL} || {\n        printwarn 'could not install kernel headers for current kernel'\n        echo 'upgrading kernel and installing new headers'\n        printwarn 'you will need to reboot the machine for changes to take effect'\n        yum install -y kernel-devel kernel-headers\n    }\n\n    if (( $? != 0 )); then\n        printerr \"Could not install kernel headers\"\n        return 1\n    fi\n\n    # change to a later C/C++ toolchain\n    for FILE in $(ls /usr/bin/gcc10-*); do\n        TMP=$(cut -d '-' -f 2- <<<\"$FILE\")\n        ln -sf \"$FILE\" \"/usr/local/bin/$TMP\"\n    done\n    ln -sf $(which cmake3) /usr/local/bin/cmake\n    # make sure PATH has been updated\n    source /etc/profile\n\n    BUILD_KERN_VERSIONS=$(joinwith '' ',' '' $(rpm -q kernel-headers | sed 's/kernel-headers-//g'))\n\n    ## compile and install openssl v1.1.1 (workaround for amazon linux repo conflicts)\n    ## we must overwrite system packages (openssl/openssl-devel) otherwise python's openssl package is not supported\n    if [[ \"$(openssl version 2>/dev/null | awk '{print $2}')\" != \"1.1.1w\" ]]; then\n        if [[ ! -d ${SRC_DIR}/openssl ]]; then\n            ( cd ${SRC_DIR} &&\n            curl -sL https://www.openssl.org/source/openssl-1.1.1w.tar.gz 2>/dev/null |\n            tar -xzf - --transform 's%openssl-1.1.1w%openssl%'; )\n        fi\n        (\n            cd ${SRC_DIR}/openssl &&\n            ./Configure --prefix=/usr linux-$(uname -m) &&\n            make -j $NPROC &&\n            make -j $NPROC install\n        ) || {\n            printerr 'Failed to compile openssl'\n            return 1\n        }\n    fi\n\n    ## compile and install libxh264\n    if [[ ! -d ${SRC_DIR}/libxh264 ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://code.videolan.org/videolan/x264 ${SRC_DIR}/libxh264\n    fi\n    (\n        cd ${SRC_DIR}/libxh264 &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 --enable-static &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libxh264'\n        return 1\n    }\n\n    ## compile and install libx265\n    if [[ ! -d ${SRC_DIR}/libx265 ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/videolan/x265 ${SRC_DIR}/libx265\n    fi\n    (\n        cd ${SRC_DIR}/libx265/build/linux &&\n        rm -rf ${SRC_DIR}/libx265/.git &&\n        cmake -G \"Unix Makefiles\" -DCMAKE_INSTALL_PREFIX=/usr -DLIB_INSTALL_DIR=/usr/lib64 \\\n            -DENABLE_SHARED=TRUE ../../source &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libx265'\n        return 1\n    }\n\n    ## compile and install libfdkaac\n    if [[ ! -d ${SRC_DIR}/libfdkaac ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/mstorsjo/fdk-aac ${SRC_DIR}/libfdkaac\n    fi\n    (\n        cd ${SRC_DIR}/libfdkaac &&\n        autoreconf -i &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 --disable-shared &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libfdkaac'\n        return 1\n    }\n\n    ## compile and install libmp3lame\n    if [[ ! -d ${SRC_DIR}/libmp3lame ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/gypified/libmp3lame.git ${SRC_DIR}/libmp3lame\n    fi\n    (\n        cd ${SRC_DIR}/libmp3lame &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 --disable-shared --enable-nasm &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libmp3lame'\n        return 1\n    }\n\n    ## compile and install libopus\n    if [[ ! -d ${SRC_DIR}/libopus ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://gitlab.xiph.org/xiph/opus.git ${SRC_DIR}/libopus ||\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/xiph/opus.git ${SRC_DIR}/libopus\n    fi\n    (\n        cd ${SRC_DIR}/libopus &&\n        autoreconf -i &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 --disable-shared &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libopus'\n        return 1\n    }\n\n    ## compile and install libvpx\n    if [[ ! -d ${SRC_DIR}/libvpx ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://chromium.googlesource.com/webm/libvpx.git ${SRC_DIR}/libvpx\n    fi\n    (\n        cd ${SRC_DIR}/libvpx &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 --disable-examples --disable-unit-tests --enable-vp9-highbitdepth --as=yasm &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libvpx'\n        return 1\n    }\n\n    ## compile and install ffmpeg\n    if [[ ! -d ${SRC_DIR}/ffmpeg ]]; then\n        git clone --depth 1 -c advice.detachedHead=false -b n7.1.1 https://git.ffmpeg.org/ffmpeg.git ${SRC_DIR}/ffmpeg\n    fi\n    (\n        cd ${SRC_DIR}/ffmpeg &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 --pkg-config-flags=\"--static\" --extra-libs=-lpthread --extra-libs=-lm \\\n            --enable-gpl --enable-libfdk-aac --enable-libfreetype --enable-libmp3lame --enable-libopus --enable-libvpx \\\n            --enable-libx264 --enable-libx265 --enable-nonfree &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install ffmpeg'\n        return 1\n    }\n\n    ## compile and install librabbitmq\n    if [[ ! -d ${SRC_DIR}/librabbitmq ]]; then\n        git clone --depth 1 -c advice.detachedHead=false -b v0.11.0 https://github.com/alanxz/rabbitmq-c.git ${SRC_DIR}/librabbitmq\n    fi\n    (\n        cd ${SRC_DIR}/librabbitmq &&\n        mkdir -p build &&\n        cd build/ &&\n        cmake -G \"Unix Makefiles\" -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=lib64 \\\n            -DBUILD_EXAMPLES=FALSE -DBUILD_TESTS=FALSE .. &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install librabbitmq'\n        return 1\n    }\n\n    ## compile and install libspandsp\n    if [[ ! -d ${SRC_DIR}/libspandsp ]]; then\n        git clone --depth 1 -c advice.detachedHead=false https://github.com/freeswitch/spandsp.git ${SRC_DIR}/libspandsp\n    fi\n    (\n        cd ${SRC_DIR}/libspandsp &&\n        ./bootstrap.sh &&\n        ./configure --prefix=/usr --libdir=/usr/lib64 &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libspandsp'\n        return 1\n    }\n\n    ## compile and install libwebsockets\n    if [[ ! -d ${SRC_DIR}/libwebsockets ]]; then\n        git clone --depth 1 -c advice.detachedHead=false -b v4.3.3 https://github.com/warmcat/libwebsockets.git ${SRC_DIR}/libwebsockets\n    fi\n    (\n        CMAKE_ARGS='-DCMAKE_INSTALL_PREFIX=/usr -DLIB_SUFFIX=64 -DLWS_WITH_HTTP2=1'\n        if [[ -e \"${SRC_DIR}/openssl\" ]]; then\n            CMAKE_ARGS=\"$CMAKE_ARGS -DLWS_OPENSSL_INCLUDE_DIRS=${SRC_DIR}/openssl/include\"\n            CMAKE_ARGS=\"$CMAKE_ARGS -DLWS_OPENSSL_LIBRARIES=${SRC_DIR}/openssl/libssl.so;${SRC_DIR}/openssl/libcrypto.so\"\n        fi\n\n        cd ${SRC_DIR}/libwebsockets &&\n        { rm -rf build/ 2>/dev/null || :; } &&\n        mkdir -p build &&\n        cd build/ &&\n        cmake $CMAKE_ARGS .. &&\n        make -j $NPROC &&\n        make -j $NPROC install\n    ) || {\n        printerr 'Failed to compile and install libwebsockets'\n        return 1\n    }\n\n    ## compile and install RTPEngine as an RPM package\n\n    # create rtpengine user and group\n    # sometimes locks aren't properly removed (this seems to happen often on VM's)\n    rm -f /etc/passwd.lock /etc/shadow.lock /etc/group.lock /etc/gshadow.lock &>/dev/null\n    userdel rtpengine &>/dev/null; groupdel rtpengine &>/dev/null\n    useradd --system --user-group --shell /bin/false --comment \"RTPengine RTP Proxy\" rtpengine\n\n    # reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/rtpengine ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/rtpengine)\" != \"${RTPENGINE_VER}\" ]]; then\n            rm -rf ${SRC_DIR}/rtpengine\n            git clone --depth 1 -c advice.detachedHead=false -b ${RTPENGINE_VER} https://github.com/sipwise/rtpengine.git ${SRC_DIR}/rtpengine\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${RTPENGINE_VER} https://github.com/sipwise/rtpengine.git ${SRC_DIR}/rtpengine\n    fi\n\n    # apply our patches\n    (\n        cd ${SRC_DIR}/rtpengine &&\n        patch -p1 -N <${DSIP_PROJECT_DIR}/rtpengine/el-${RTPENGINE_VER}.patch\n    )\n    if (( $? > 1 )); then\n        printerr 'Failed patching RTPEngine files prior to build'\n        return 1\n    fi\n\n    # amazon linux specific: remove the dependencies that were manually compiled\n    sed -i --regexp-extended \\\n        -e '/^BuildRequires:[ \\t]*pkgconfig\\(libwebsockets\\).*/d' \\\n        -e '/^BuildRequires:[ \\t]*pkgconfig\\(spandsp\\).*/d' \\\n        -e '/^BuildRequires:[ \\t]*pkgconfig\\(opus\\).*/d' \\\n        -e '/^BuildRequires:[ \\t]*ffmpeg-devel.*/d' \\\n        -e '/^Requires\\(pre\\):[ \\t]*ffmpeg-libs.*/d' \\\n        -e 's/^(BuildRequires:.*)ffmpeg-devel(.*)/\\1\\2/' \\\n        ${SRC_DIR}/rtpengine/el/rtpengine.spec\n\n    RTPENGINE_RPM_VER=$(grep -oP 'Version:.+?\\K[\\w\\.\\~\\+]+' ${SRC_DIR}/rtpengine/el/rtpengine.spec)\n    RPM_BUILD_ROOT=\"${HOME}/rpmbuild\"\n    rm -rf ${RPM_BUILD_ROOT} 2>/dev/null\n    mkdir -p ${RPM_BUILD_ROOT}/SOURCES &&\n    (\n        cd ${SRC_DIR} &&\n        tar -czf ${RPM_BUILD_ROOT}/SOURCES/ngcp-rtpengine-${RTPENGINE_RPM_VER}.tar.gz \\\n            --transform=\"s%^rtpengine%ngcp-rtpengine-$RTPENGINE_RPM_VER%g\" rtpengine/ &&\n        echo \"%__make $(which make) -j $NPROC\" >~/.rpmmacros &&\n        # fix for BUG: \"exec_prefix: command not found\"\n        function exec_prefix() { echo -n '/usr'; } && export -f exec_prefix &&\n        # build the RPM's\n        rpmbuild -ba --define \"kversion $BUILD_KERN_VERSIONS\" ${SRC_DIR}/rtpengine/el/rtpengine.spec &&\n        # see: https://stackoverflow.com/questions/49263444/missing-libraries-in-my-rpm-but-i-know-they-are-there\n        rpmrebuild --change-spec-requires='sed -re \"/^(Requires:.*)(libspandsp\\.so|libwebsockets\\.so|libx265\\.so).*/d\"' \\\n            -bp ${RPM_BUILD_ROOT}/RPMS/${OS_ARCH}/ngcp-rtpengine-${RTPENGINE_RPM_VER}*.rpm &&\n        rm -f ~/.rpmmacros && unset -f exec_prefix &&\n        systemctl mask ngcp-rtpengine-daemon.service &&\n        # install the RPM's\n        yum localinstall -y ${RPM_BUILD_ROOT}/RPMS/${OS_ARCH}/ngcp-rtpengine-${RTPENGINE_RPM_VER}*.rpm \\\n            ${RPM_BUILD_ROOT}/RPMS/noarch/ngcp-rtpengine-dkms-${RTPENGINE_RPM_VER}*.rpm \\\n            ${RPM_BUILD_ROOT}/RPMS/${OS_ARCH}/ngcp-rtpengine-kernel-${RTPENGINE_RPM_VER}*.rpm\n    )\n\n    if (( $? != 0 )); then\n        printerr \"Problems occurred compiling rtpengine\"\n        return 1\n    fi\n\n    # make sure RTPEngine kernel module configured\n    # skip if the kernel headers were not installed\n    if rpm -qa | grep -q \"kernel-headers-${OS_KERNEL}\"; then\n        if [[ -z \"$(find /lib/modules/${OS_KERNEL}/ -name 'xt_RTPENGINE.ko' 2>/dev/null)\" ]]; then\n            printerr \"Problem installing RTPEngine kernel module\"\n            return 1\n        fi\n    fi\n\n    # ensure config dirs exist\n    mkdir -p /run/rtpengine ${SYSTEM_RTPENGINE_CONFIG_DIR}\n    chown -R rtpengine:rtpengine /run/rtpengine\n\n    # setup rtpengine defaults file\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/configs/default.conf /etc/default/rtpengine.conf\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    if (( $? != 0 )); then\n        # fix for bug: https://bugzilla.redhat.com/show_bug.cgi?id=1575845\n        systemctl restart dbus\n        systemctl restart firewalld\n        # fix for ensuing bug: https://bugzilla.redhat.com/show_bug.cgi?id=1372925\n        systemctl restart systemd-logind\n    fi\n\n    # Setup Firewall rules for RTPEngine\n    firewall-cmd --zone=public --add-port=${RTP_PORT_MIN}-${RTP_PORT_MAX}/udp --permanent\n    firewall-cmd --reload\n\n    # Setup RTPEngine Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rtpengine.conf /etc/rsyslog.d/rtpengine.conf\n    touch /var/log/rtpengine.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/rtpengine /etc/logrotate.d/rtpengine\n\n    # Setup tmp files\n    echo \"d /var/run/rtpengine.pid  0755 rtpengine rtpengine - -\" > /etc/tmpfiles.d/rtpengine.conf\n\n    # Reconfigure systemd service files\n    rm -f /lib/systemd/system/rtpengine.service 2>/dev/null\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/systemd/rtpengine-v1.service /lib/systemd/system/rtpengine.service\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/rtpengine-{start-pre,stop-post} /usr/sbin/\n    chmod +x /usr/sbin/rtpengine-{start-pre,stop-post} /usr/bin/rtpengine\n\n    # Reload systemd configs\n    systemctl daemon-reload\n    # Enable the RTPEngine to start during boot\n    systemctl enable rtpengine\n\n    # preliminary check that rtpengine actually installed\n    if cmdExists rtpengine; then\n        return 0\n    else\n        return 1\n    fi\n}\n\n# Remove RTPEngine\nfunction uninstall {\n    systemctl stop rtpengine\n    systemctl disable rtpengine\n    rm -f /{etc,lib}/systemd/system/rtpengine.service 2>/dev/null\n    systemctl daemon-reload\n\n    yum remove -y ngcp-rtpengine\\*\n\n    rm -f /usr/sbin/rtpengine* /usr/bin/rtpengine /etc/rsyslog.d/rtpengine.conf \\\n        /etc/logrotate.d/rtpengine ${SRC_DIR}/rtpengine\n\n    for LIB in libxh264 libx265 libfdkaac libmp3lame libopus libvpx ffmpeg librabbitmq libspandsp libwebsockets; do\n    (\n        cd ${SRC_DIR}/${LIB} &&\n        make uninstall &&\n        rm -rf ${SRC_DIR}/${LIB}\n    )\n    done\n\n    # remove our firewall changes\n    firewall-cmd --zone=public --remove-port=${RTP_PORT_MIN}-${RTP_PORT_MAX}/udp --permanent\n    firewall-cmd --reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "rtpengine/amzn/rtpengine.spec",
    "content": "Name:\t\tngcp-rtpengine\nVersion:\t9.5.5.1+0~mr9.5.5.1\nRelease:\t1%{?dist}\nSummary:\tThe Sipwise NGCP rtpengine\nGroup:\t\tSystem Environment/Daemons\nLicense:\tGPLv3\nURL:\t\thttps://github.com/sipwise/rtpengine\nSource0:\thttps://github.com/sipwise/rtpengine/archive/mr%{version}/%{name}-%{version}.tar.gz\nConflicts:\t%{name}-kernel < %{version}-%{release}\n\n%global with_transcoding 1\n%{?_unitdir:%define has_systemd_dirs 1}\n\nBuildRequires:\tgcc make pkgconfig redhat-rpm-config\nBuildRequires:\tglib2-devel libcurl-devel openssl-devel pcre-devel\nBuildRequires:\txmlrpc-c-devel zlib-devel hiredis-devel\nBuildRequires:\tlibpcap-devel libevent-devel json-glib-devel\nBuildRequires:\tgperf perl-IPC-Cmd\nBuildRequires:\tperl-podlators\nRequires(pre):\tshadow-utils\n\nRequires:\tnc\n# Remain compat with other installations\nProvides:\tngcp-rtpengine = %{version}-%{release}\n\n%description\nThe Sipwise NGCP rtpengine is a proxy for RTP traffic and other UDP based\nmedia traffic. It's meant to be used with the Kamailio SIP proxy and forms a\ndrop-in replacement for any of the other available RTP and media proxies.\n\n%if 0%{?rhel} < 7\n%define iptables_ipv6 1\n%endif\n%package kernel\nSummary:\tNGCP rtpengine in-kernel packet forwarding\nGroup:\t\tSystem Environment/Daemons\nBuildRequires:\tgcc make redhat-rpm-config iptables-devel\nRequires:\tiptables %{?iptables_ipv6:iptables-ipv6}\nRequires:\t%{name}%{?_isa} = %{version}-%{release}\nRequires: \t%{name}-dkms = %{version}-%{release}\n\n%description kernel\n%{summary}.\n\n\n%package dkms\nSummary:\tKernel module for NGCP rtpengine in-kernel packet forwarding\nGroup:\t\tSystem Environment/Daemons\nBuildArch:\tnoarch\nBuildRequires:\tredhat-rpm-config\nRequires:\tgcc make\n# Define requires according to the installed kernel.\n%{?rhel:Requires: kernel-devel}\n%{?fedora:Requires: kernel-devel}\n%{?suse_version:Requires: kernel-source}\nRequires(post):\tdkms\nRequires(preun): dkms\n\n%description dkms\n%{summary}.\n\n%if 0%{?rhel} >= 8\n%define mysql_devel_pkg mariadb-devel\n%else\n%define mysql_devel_pkg mysql-devel\n%endif\n\n%if 0%{?with_transcoding} > 0\n%package recording\nSummary:        NGCP rtpengine recording daemon packet\nGroup:          System Environment/Daemons\nBuildRequires:  gcc make redhat-rpm-config %{mysql_devel_pkg}\n\n%description recording\n%{summary}.\n\n%endif\n\n%define binname rtpengine\n\n\n%prep\n%setup -q -n %{name}-%{version}\n\n\n%build\n%if 0%{?with_transcoding} > 0\ncd daemon\nRTPENGINE_VERSION=\"\\\"%{version}-%{release}\\\"\" make\ncd ../iptables-extension\nRTPENGINE_VERSION=\"\\\"%{version}-%{release}\\\"\" make\ncd ../recording-daemon\nRTPENGINE_VERSION=\"\\\"%{version}-%{release}\\\"\" make\ncd ..\n%else\ncd daemon\nRTPENGINE_VERSION=\"\\\"%{version}-%{release}\\\"\" make with_transcoding=no\ncd ../iptables-extension\nRTPENGINE_VERSION=\"\\\"%{version}-%{release}\\\"\" make with_transcoding=no\ncd ..\n%endif\n\n%install\n# Install the userspace daemon\ninstall -D -p -m755 daemon/%{binname} %{buildroot}%{_bindir}/%{binname}\n# Install CLI (command line interface)\ninstall -D -p -m755 utils/%{binname}-ctl %{buildroot}%{_bindir}/%{binname}-ctl\n# Install recording daemon\n%if 0%{?with_transcoding} > 0\ninstall -D -p -m755 recording-daemon/%{binname}-recording %{buildroot}%{_bindir}/%{binname}-recording\n%endif\n\n## Install the init.d script and configuration file\n%if 0%{?has_systemd_dirs}\ninstall -D -p -m644 el/%{binname}.service \\\n\t%{buildroot}%{_unitdir}/%{binname}.service\n%else\ninstall -D -p -m755 el/%{binname}.init \\\n\t%{buildroot}%{_initrddir}/%{name}\n%endif\n%if 0%{?with_transcoding} > 0\n%if 0%{?has_systemd_dirs}\ninstall -D -p -m644 el/%{binname}-recording.service \\\n\t%{buildroot}%{_unitdir}/%{binname}-recording.service\n%else\ninstall -D -p -m755 el/%{binname}-recording.init \\\n        %{buildroot}%{_initrddir}/%{name}-recording\n%endif\n%endif\ninstall -D -p -m644 el/%{binname}.sysconfig \\\n\t%{buildroot}%{_sysconfdir}/sysconfig/%{binname}\n%if 0%{?with_transcoding} > 0\ninstall -D -p -m644 el/%{binname}-recording.sysconfig \\\n\t%{buildroot}%{_sysconfdir}/sysconfig/%{binname}-recording\n%endif\nmkdir -p %{buildroot}%{_sharedstatedir}/%{name}\nmkdir -p %{buildroot}%{_var}/spool/%{binname}\n\n# Install config files\ninstall -D -p -m644 etc/%{binname}.sample.conf \\\n\t%{buildroot}%{_sysconfdir}/%{binname}/%{binname}.conf\n%if 0%{?with_transcoding} > 0\ninstall -D -p -m644 etc/%{binname}-recording.sample.conf \\\n\t%{buildroot}%{_sysconfdir}/%{binname}/%{binname}-recording.conf\n%endif\n\n# Install the iptables plugin\ninstall -D -p -m755 iptables-extension/libxt_RTPENGINE.so \\\n\t%{buildroot}/%{_lib}/xtables/libxt_RTPENGINE.so\n\n## DKMS module source install\ninstall -D -p -m644 kernel-module/Makefile \\\n\t %{buildroot}%{_usrsrc}/%{name}-%{version}-%{release}/Makefile\ninstall -D -p -m644 kernel-module/xt_RTPENGINE.c \\\n\t %{buildroot}%{_usrsrc}/%{name}-%{version}-%{release}/xt_RTPENGINE.c\ninstall -D -p -m644 kernel-module/xt_RTPENGINE.h \\\n\t %{buildroot}%{_usrsrc}/%{name}-%{version}-%{release}/xt_RTPENGINE.h\nmkdir -p %{buildroot}%{_usrsrc}/%{name}-%{version}-%{release}\ninstall -D -p -m644 kernel-module/rtpengine_config.h \\\n\t %{buildroot}%{_usrsrc}/%{name}-%{version}-%{release}/rtpengine_config.h\ninstall -D -p -m644 debian/dkms.conf.in %{buildroot}%{_usrsrc}/%{name}-%{version}-%{release}/dkms.conf\nsed -i -e \"s/__VERSION__/%{version}-%{release}/g\" %{buildroot}%{_usrsrc}/%{name}-%{version}-%{release}/dkms.conf\n\n# For RHEL 7, load the compiled kernel module on boot.\n%if 0%{?rhel} == 7\n  install -D -p -m644 kernel-module/xt_RTPENGINE.modules.load.d \\\n           %{buildroot}%{_sysconfdir}/modules-load.d/xt_RTPENGINE.conf\n%endif\n\n%pre\ngetent group %{name} >/dev/null || /usr/sbin/groupadd -r %{name}\ngetent passwd %{name} >/dev/null || /usr/sbin/useradd -r -g %{name} \\\n\t-s /sbin/nologin -c \"%{name} daemon\" -d %{_sharedstatedir}/%{name} %{name}\n\n\n%post\nif [ $1 -eq 1 ]; then\n%if 0%{?has_systemd_dirs}\n        systemctl daemon-reload\n%else\n        /sbin/chkconfig --add %{name} || :\n%endif\nfi\n\n\n%post dkms\n# Add to DKMS registry, build, and install module\n# The kernel version can be overridden with \"--define kversion foo\" on rpmbuild,\n# e.g. --define \"kversion 2.6.32-696.23.1.el6.x86_64\"\n%{!?kversion: %define kversion %{nil}}\n\n%if \"%{kversion}\" != \"\"\n  dkms add -m %{name} -v %{version}-%{release} --rpm_safe_upgrade &&\n  dkms build -m %{name} -v %{version}-%{release} -k %{kversion} --rpm_safe_upgrade &&\n  dkms install -m %{name} -v %{version}-%{release} -k %{kversion} --rpm_safe_upgrade --force\n%else\n  dkms add -m %{name} -v %{version}-%{release} --rpm_safe_upgrade &&\n  dkms build -m %{name} -v %{version}-%{release} --rpm_safe_upgrade &&\n  dkms install -m %{name} -v %{version}-%{release} --rpm_safe_upgrade --force\n%endif\ntrue\n\n\n%preun\nif [ $1 = 0 ] ; then\n%if 0%{?has_systemd_dirs}\n        systemctl stop %{binname}.service\n        systemctl disable %{binname}.service\n\n%else\n        /sbin/service %{name} stop >/dev/null 2>&1\n        /sbin/chkconfig --del %{name}\n%endif\nfi\n\n%preun dkms\n# Remove from DKMS registry\ndkms remove -m %{name} -v %{version}-%{release} --rpm_safe_upgrade --all\ntrue\n\n\n%files\n# Userspace daemon\n%{_bindir}/%{binname}\n# CLI (command line interface)\n%{_bindir}/%{binname}-ctl\n# init.d script and configuration file\n%if 0%{?has_systemd_dirs}\n%{_unitdir}/%{binname}.service\n%else\n%{_initrddir}/%{name}\n%endif\n%config(noreplace) %{_sysconfdir}/sysconfig/%{binname}\n%attr(0750,%{name},%{name}) %dir %{_sharedstatedir}/%{name}\n# default config\n%{_sysconfdir}/%{binname}/%{binname}.conf\n# Documentation\n%doc LICENSE README.md el/README.el.md debian/changelog debian/copyright\n\n\n%files kernel\n/%{_lib}/xtables/libxt_RTPENGINE.so\n\n\n%files dkms\n%{_usrsrc}/%{name}-%{version}-%{release}/\n%if 0%{?rhel} == 7\n  %{_sysconfdir}/modules-load.d/xt_RTPENGINE.conf\n%endif\n\n\n%if 0%{?with_transcoding} > 0\n%files recording\n# Recording daemon\n%{_bindir}/%{binname}-recording\n# Init script\n%if 0%{?has_systemd_dirs}\n%{_unitdir}/%{binname}-recording.service\n%else\n%{_initrddir}/%{name}-recording\n%endif\n# Sysconfig\n%config(noreplace) %{_sysconfdir}/sysconfig/%{binname}-recording\n# Default config\n%{_sysconfdir}/%{binname}/%{binname}-recording.conf\n# spool directory\n%attr(0750,%{name},%{name}) %dir %{_var}/spool/%{binname}\n%endif\n\n%changelog\n* Thu Oct 26 2023 Tyler Moore <tmoore@dopensource.com>\n  - update paths to match latest versions\n  - remove amzn2 compiled libraries from checks\n  - remove/replace \"archname\" variable\n* Tue Jul 10 2018 netaskd <netaskd@gmail.com> - 6.4.0.0-1\n  - update to ngcp-rtpengine version 6.4.0.0\n  - add packet recording\n* Thu Nov 24 2016 Marcel Weinberg <marcel@ng-voice.com>\n  - Updated to ngcp-rtpengine version 4.5.0 and CentOS 7.2\n  - created a new variable \"binname\" to use rtpengine as name for the binaries\n    (still using ngcp-rtpenginge as name of the package and daemon - aligned to the .deb packages)\n  - fixed dependencies\n* Mon Nov 11 2013 Peter Dunkley <peter.dunkley@crocodilertc.net>\n  - Updated version to 2.3.2\n  - Set license to GPLv3\n* Thu Aug 15 2013 Peter Dunkley <peter.dunkley@crocodilertc.net>\n  - init.d scripts and configuration file\n* Wed Aug 14 2013 Peter Dunkley <peter.dunkley@crocodilertc.net>\n  - First version of .spec file\n  - Builds and installs userspace daemon (but no init.d scripts etc yet)\n  - Builds and installs the iptables plugin\n  - DKMS package for the kernel module\n\n"
  },
  {
    "path": "rtpengine/centos/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\n## search for RPM using external APIs mirrors and archives\n## not guaranteed to find an RPM, outputs empty string if search fails\n## arguments:\n##   $1 == rpm to search for\n## options:\n##   -d <distro filter>\n##   --distro=<distro filter>\n##   -f <grep filter>\n##   --filter=<grep filter>\n## TODO: add support for searching https://linuxsoft.cern.ch as well\n#function rpmSearch() {\n#    local RPM_SEARCH=\"\" DISTRO_FILTER=\"$DISTRO\" GREP_FILTER=\"\" SEARCH_RESULTS=\"\"\n#\n#    while (( $# > 0 )); do\n#        # last arg is user and database\n#        if (( $# == 1 )); then\n#            RPM_SEARCH=\"$1\"\n#            shift\n#            break\n#        fi\n#\n#        case \"$1\" in\n#            -d)\n#                shift\n#                DISTRO_FILTER=\"$1\"\n#                shift\n#                ;;\n#            --distro=*)\n#                DISTRO_FILTER=\"$(echo \"$1\" | cut -d '=' -f 2)\"\n#                shift\n#                ;;\n#            -f)\n#                shift\n#                GREP_FILTER=\"$1\"\n#                shift\n#                ;;\n#            --filter=*)\n#                GREP_FILTER=\"$(echo \"$1\" | cut -d '=' -f 2)\"\n#                shift\n#                ;;\n#        esac\n#    done\n#\n#    # if grep filter not set it defaults to rpm search\n#    if [[ -z \"$GREP_FILTER\" ]]; then\n#        GREP_FILTER=\"${RPM_SEARCH}\"\n#    fi\n#\n#    # grab the results of the search using an API on rpmfind.net\n#    SEARCH_RESULTS=$(\n#        curl -sL \"https://www.rpmfind.net/linux/rpm2html/search.php?query=${RPM_SEARCH}&system=${DISTRO_FILTER}&arch=${OS_ARCH}\" 2>/dev/null |\n#            perl -e \"\\$rpmfind_base_url='https://rpmfind.net'; \\$rpm_search='${RPM_SEARCH}'; @matches=(); \" -0777 -e \\\n#                '$html = do { local $/; <STDIN> };\n#                @matches = ($html =~ m%(?<=\\<a href=[\"'\"'\"'])([-a-zA-Z0-9\\@\\:\\%\\._\\+~#=/]*${rpm_search}[-a-zA-Z0-9\\@\\:\\%\\._\\+\\~\\#\\=]*\\.rpm)(?=[\"'\"'\"']\\>)%g);\n#                foreach my $match (@matches) { print \"${rpmfind_base_url}${match}\\n\"; }' 2>/dev/null |\n#            grep -m 1 \"${GREP_FILTER}\"\n#    )\n#\n#    # if empty try searching the official archives on vault.centos.org\n#    if [[ -z \"$SEARCH_RESULTS\" ]]; then\n#        SEARCH_RESULTS=$(\n#            curl --keepalive-time 5 --compressed -sL https://vault.centos.org/filelist.gz 2>/dev/null |\n#                gunzip -c |\n#                tac |\n#                grep -oP \".*${OS_ARCH}.*${RPM_SEARCH}.*\\.rpm\" |\n#                grep -m 1 \"${GREP_FILTER}\" |\n#                perl -pe 's%^\\./(.*\\.rpm)$%https://vault.centos.org/\\1%'\n#        )\n#    fi\n#\n#    if [[ -n \"$SEARCH_RESULTS\" ]]; then\n#        echo \"$SEARCH_RESULTS\"\n#    fi\n#}\n\n# compile and install rtpengine from RPM's\nfunction install {\n    local RTPENGINE_RPM_VER TMP BUILD_KERN_VERSIONS\n    local REBOOT_REQUIRED=0\n    local OS_ARCH=$(uname -m)\n    local OS_KERNEL=$(uname -r)\n    local DISTRO_VER=$(source /etc/os-release; echo \"$VERSION_ID\")\n    local DISTRO_MAJVER=$(cut -d '.' -f 1 <<<\"$DISTRO_VER\")\n    local RHEL_BASE_VER=$(rpm -E %{rhel})\n    local NPROC=$(nproc)\n\n    # Install required libraries\n    if (( ${DISTRO_MAJVER} == 9 )); then\n        dnf install -y epel-release &&\n        dnf install -y epel-next-release &&\n        dnf config-manager -y --set-enabled crb &&\n        dnf install -y http://rpm.dsiprouter.org/dsiprouter-repo.noarch.rpm &&\n        dnf install -y gcc glib2 glib2-devel zlib zlib-devel openssl openssl-devel pcre pcre-devel curl libcurl libcurl-devel \\\n            xmlrpc-c libpcap libpcap-devel hiredis hiredis-devel json-glib json-glib-devel libevent libevent-devel \\\n            iptables iptables-devel gperf nc dkms perl perl-IPC-Cmd spandsp spandsp-devel logrotate rsyslog mosquitto-devel \\\n            redhat-rpm-config rpm-build pkgconfig perl-Config-Tiny gperftools-libs gperftools gperftools-devel gzip mariadb-devel \\\n            libwebsockets-devel iptables-legacy-devel pandoc ladspa libuv-devel xmlrpc-c-devel opus-devel ffmpeg ffmpeg-devel\n    elif (( ${DISTRO_MAJVER} == 8 )); then\n        dnf install -y epel-release &&\n        dnf install -y epel-next-release &&\n        dnf config-manager --enable powertools &&\n        dnf install -y https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-${RHEL_BASE_VER}.noarch.rpm &&\n        dnf install -y https://mirrors.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-${RHEL_BASE_VER}.noarch.rpm &&\n        dnf install -y ffmpeg ffmpeg-devel &&\n        dnf install -y gcc glib2 glib2-devel zlib zlib-devel openssl openssl-devel pcre pcre-devel curl libcurl libcurl-devel \\\n            xmlrpc-c libpcap libpcap-devel hiredis hiredis-devel json-glib json-glib-devel libevent libevent-devel \\\n            iptables iptables-devel gperf nc dkms perl perl-IPC-Cmd spandsp spandsp-devel logrotate rsyslog mosquitto-devel \\\n            redhat-rpm-config rpm-build pkgconfig perl-Config-Tiny gperftools-libs gperftools gperftools-devel gzip \\\n            libwebsockets-devel opus-devel xmlrpc-c-devel gcc-toolset-13 pandoc mariadb-devel mariadb-libs &&\n        source scl_source enable gcc-toolset-13\n    else\n        yum-config-manager --enable centos-sclo-rh >/dev/null &&\n        yum install -y epel-release &&\n        yum install -y https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-${RHEL_BASE_VER}.noarch.rpm &&\n        yum install -y https://mirrors.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-${RHEL_BASE_VER}.noarch.rpm &&\n        yum install -y ffmpeg ffmpeg-devel &&\n        yum install -y gcc glib2 glib2-devel zlib zlib-devel openssl openssl-devel pcre2 pcre2-devel curl libcurl libcurl-devel \\\n            xmlrpc-c xmlrpc-c-devel libpcap libpcap-devel hiredis hiredis-devel json-glib json-glib-devel libevent libevent-devel \\\n            iptables iptables-devel xmlrpc-c-devel gperf redhat-lsb nc dkms perl perl-IPC-Cmd spandsp spandsp-devel logrotate rsyslog \\\n            redhat-rpm-config rpm-build pkgconfig perl-Config-Tiny gperftools-libs gperftools gperftools-devel gzip libwebsockets-devel \\\n            mosquitto-devel opus-devel devtoolset-11 pandoc mariadb-devel mariadb-libs &&\n        source scl_source enable devtoolset-11\n    fi\n\n    if (( $? != 0 )); then\n        printerr \"Could not install the required libraries for RTPEngine\"\n        return 1\n    fi\n\n    if (( ${DISTRO_MAJVER} >= 8 )); then\n        dnf install -y kernel-devel-${OS_KERNEL} kernel-headers-${OS_KERNEL} || {\n            REBOOT_REQUIRED=1\n            printwarn 'could not install kernel headers for current kernel'\n            echo 'upgrading kernel and installing new headers'\n            printwarn 'you will need to reboot the machine for changes to take effect'\n            dnf install -y kernel-devel kernel-headers\n        }\n    else\n        yum install -y kernel-devel-${OS_KERNEL} kernel-headers-${OS_KERNEL} || {\n            REBOOT_REQUIRED=1\n            printwarn 'could not install kernel headers for current kernel'\n            echo 'upgrading kernel and installing new headers'\n            printwarn 'you will need to reboot the machine for changes to take effect'\n            yum install -y kernel-devel kernel-headers\n        }\n    fi\n\n    if (( $? != 0 )); then\n        printerr \"Could not install kernel headers\"\n        return 1\n    fi\n\n    BUILD_KERN_VERSIONS=$(joinwith '' ',' '' $(rpm -q kernel-headers | sed 's/kernel-headers-//g'))\n\n    # rtpengine >= mr11.3.1.1 requires curl >= 7.43.0\n    if versionCompare \"$(tr -d '[a-zA-Z]' <<<\"$RTPENGINE_VER\")\" gteq \"11.3.1.1\"; then\n        if versionCompare \"$(curl -V | head -1 | awk '{print $2}')\" lt \"7.43.0\"; then\n            printdbg 'curl version is not recent enough.. compiling curl 7.8.0'\n            if [[ ! -d ${SRC_DIR}/curl ]]; then\n                (\n                    cd ${SRC_DIR} &&\n                    curl -sL https://curl.haxx.se/download/curl-7.80.0.tar.gz 2>/dev/null |\n                    tar -xzf - --transform 's%curl-7.80.0%curl%';\n                )\n            fi\n            (\n                cd ${SRC_DIR}/curl &&\n                ./configure --prefix=/usr --libdir=/usr/lib64 --with-ssl &&\n                make -j $NPROC &&\n                make -j $NPROC install &&\n                ldconfig\n            )\n            if (( $? != 0 )); then\n                printerr 'Failed to compile curl'\n                return 1\n            fi\n        fi\n    fi\n\n    # reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/rtpengine ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/rtpengine)\" != \"${RTPENGINE_VER}\" ]]; then\n            rm -rf ${SRC_DIR}/rtpengine\n            git clone --depth 1 -c advice.detachedHead=false -b ${RTPENGINE_VER} https://github.com/sipwise/rtpengine.git ${SRC_DIR}/rtpengine\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${RTPENGINE_VER} https://github.com/sipwise/rtpengine.git ${SRC_DIR}/rtpengine\n    fi\n\n    # apply our patches\n    (\n        cd ${SRC_DIR}/rtpengine &&\n        patch -p1 -N <${DSIP_PROJECT_DIR}/rtpengine/el-${RTPENGINE_VER}.patch\n    )\n    if (( $? > 1 )); then\n        printerr 'Failed patching RTPEngine files prior to build'\n        return 1\n    fi\n\n    RTPENGINE_RPM_VER=$(grep -oP 'Version:.+?\\K[\\w\\.\\~\\+]+' ${SRC_DIR}/rtpengine/el/rtpengine.spec)\n    RPM_BUILD_ROOT=\"${HOME}/rpmbuild\"\n    rm -rf ${RPM_BUILD_ROOT} 2>/dev/null\n    mkdir -p ${RPM_BUILD_ROOT}/SOURCES &&\n    (\n        cd ${SRC_DIR} &&\n        tar -czf ${RPM_BUILD_ROOT}/SOURCES/ngcp-rtpengine-${RTPENGINE_RPM_VER}.tar.gz \\\n            --transform=\"s%^rtpengine%ngcp-rtpengine-$RTPENGINE_RPM_VER%g\" rtpengine/ &&\n        echo \"%__make $(which make) -j $NPROC\" >~/.rpmmacros &&\n        # fix for BUG: \"exec_prefix: command not found\"\n        function exec_prefix() { echo -n '/usr'; } && export -f exec_prefix &&\n        # build the RPM's\n        rpmbuild -ba --define \"kversion $BUILD_KERN_VERSIONS\" ${SRC_DIR}/rtpengine/el/rtpengine.spec &&\n        rm -f ~/.rpmmacros && unset -f exec_prefix &&\n        systemctl mask ngcp-rtpengine-daemon.service\n\n        # install the RPM's\n        if (( ${DISTRO_MAJVER} >= 8 )); then\n            dnf install -y ${RPM_BUILD_ROOT}/RPMS/${OS_ARCH}/ngcp-rtpengine-${RTPENGINE_RPM_VER}*.rpm \\\n                ${RPM_BUILD_ROOT}/RPMS/noarch/ngcp-rtpengine-dkms-${RTPENGINE_RPM_VER}*.rpm \\\n                ${RPM_BUILD_ROOT}/RPMS/${OS_ARCH}/ngcp-rtpengine-kernel-${RTPENGINE_RPM_VER}*.rpm\n        else\n            yum localinstall -y ${RPM_BUILD_ROOT}/RPMS/${OS_ARCH}/ngcp-rtpengine-${RTPENGINE_RPM_VER}*.rpm \\\n                ${RPM_BUILD_ROOT}/RPMS/noarch/ngcp-rtpengine-dkms-${RTPENGINE_RPM_VER}*.rpm \\\n                ${RPM_BUILD_ROOT}/RPMS/${OS_ARCH}/ngcp-rtpengine-kernel-${RTPENGINE_RPM_VER}*.rpm\n        fi\n    )\n\n    if (( $? != 0 )); then\n        printerr \"Problems occurred compiling rtpengine\"\n        return 1\n    fi\n\n    # warn user if kernel module not loaded yet\n    if (( $REBOOT_REQUIRED == 1 )); then\n        printwarn \"A reboot is required to load the RTPEngine kernel module\"\n    fi\n\n    # ensure config dirs exist\n    mkdir -p /run/rtpengine ${SYSTEM_RTPENGINE_CONFIG_DIR}\n    chown -R rtpengine:rtpengine /run/rtpengine\n\n    # setup rtpengine defaults file\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/configs/default.conf /etc/default/rtpengine.conf\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    if (( $? != 0 )) && (( ${DISTRO_MAJVER} == 7 )); then\n        # fix for bug: https://bugzilla.redhat.com/show_bug.cgi?id=1575845\n        systemctl restart dbus\n        systemctl restart firewalld\n        # fix for ensuing bug: https://bugzilla.redhat.com/show_bug.cgi?id=1372925\n        systemctl restart systemd-logind\n    fi\n\n    # give rtpengine permissions in selinux\n    semanage port -a -t rtp_media_port_t -p udp ${RTP_PORT_MIN}-${RTP_PORT_MAX} ||\n    semanage port -m -t rtp_media_port_t -p udp ${RTP_PORT_MIN}-${RTP_PORT_MAX}\n\n    # Setup Firewall rules for RTPEngine\n    firewall-cmd --zone=public --add-port=${RTP_PORT_MIN}-${RTP_PORT_MAX}/udp --permanent\n    firewall-cmd --reload\n\n    # Setup RTPEngine Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rtpengine.conf /etc/rsyslog.d/rtpengine.conf\n    touch /var/log/rtpengine.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/rtpengine /etc/logrotate.d/rtpengine\n\n    # Setup tmp files\n    echo \"d /run/rtpengine/rtpengine.pid  0755 rtpengine rtpengine - -\" > /etc/tmpfiles.d/rtpengine.conf\n\n    # Reconfigure systemd service files\n    if (( ${DISTRO_MAJVER} > 7 )); then\n        cp -f ${DSIP_PROJECT_DIR}/rtpengine/systemd/rtpengine-v3.service /lib/systemd/system/rtpengine.service\n    else\n        cp -f ${DSIP_PROJECT_DIR}/rtpengine/systemd/rtpengine-v2.service /lib/systemd/system/rtpengine.service\n    fi\n    chmod 644 /lib/systemd/system/rtpengine.service\n    systemctl daemon-reload\n    systemctl enable rtpengine\n\n    # preliminary check that rtpengine actually installed\n    if cmdExists rtpengine; then\n        return 0\n    else\n        return 1\n    fi\n}\n\n# Remove RTPEngine\nfunction uninstall {\n    systemctl stop rtpengine\n    systemctl disable rtpengine\n    rm -f /{etc,lib}/systemd/system/rtpengine.service 2>/dev/null\n    systemctl daemon-reload\n\n    yum remove -y ngcp-rtpengine\\*\n\n    rm -f /usr/bin/rtpengine\n    rm -f /etc/rsyslog.d/rtpengine.conf\n    rm -f /etc/logrotate.d/rtpengine\n\n    # remove our selinux changes\n    semanage port -D -t rtp_media_port_t -p udp\n\n    # remove our firewall changes\n    firewall-cmd --zone=public --remove-port=${RTP_PORT_MIN}-${RTP_PORT_MAX}/udp --permanent\n    firewall-cmd --reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "rtpengine/configs/default.conf",
    "content": "CONFIG_FILE=/etc/rtpengine/rtpengine.conf\nCONFIG_SECTION=rtpengine\nPID_FILE=/run/rtpengine/rtpengine.pid\nMANAGE_IPTABLES=yes\nSET_USER=rtpengine\nSET_GROUP=rtpengine\n"
  },
  {
    "path": "rtpengine/configs/rtpengine.conf",
    "content": "[rtpengine]\ntable = 0\nno-fallback = false\ninterface = 127.0.0.1\nlisten-ng = 127.0.0.1:7722\nport-min = 10000\nport-max = 20000\n#num-threads = 8\ntimeout = 60\nsilent-timeout = 3600\noffer-timeout = 300\n#final-timeout = 7200\n#tos = 184\n#control-tos = 184\n#delete-delay = 30\n#final-timeout = 10800\n#homer = 123.234.345.456:9060\n#homer-protocol = udp\n#homer-id = 1\n#sip-source = false\n#dtls-passive = false\nlog-level = 4\nlog-stderr = false\nlog-facility = local1\nlog-facility-cdr = local1\nlog-facility-rtcp = local1\n"
  },
  {
    "path": "rtpengine/deb-mr11.5.1.11.patch",
    "content": "Subject: [PATCH] change defaults config path\n---\nIndex: debian/ngcp-rtpengine-iptables-setup\nIDEA additional info:\nSubsystem: com.intellij.openapi.diff.impl.patch.CharsetEP\n<+>UTF-8\n===================================================================\ndiff --git a/debian/ngcp-rtpengine-iptables-setup b/debian/ngcp-rtpengine-iptables-setup\n--- a/debian/ngcp-rtpengine-iptables-setup\t(revision a6631401498937d0c03e63931e601c40fcbfa2e7)\n+++ b/debian/ngcp-rtpengine-iptables-setup\t(date 1717809128862)\n@@ -4,7 +4,7 @@\n MODNAME=xt_RTPENGINE\n MANAGE_IPTABLES=yes\n \n-DEFAULTS=/etc/default/ngcp-rtpengine-daemon\n+DEFAULTS=/etc/default/rtpengine.conf\n \n # Load startup options if available\n if [ -f \"$DEFAULTS\" ]; then\n"
  },
  {
    "path": "rtpengine/debian/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\n# TODO: add support for searching packages.debian.org\nfunction debSearch() {\n    local DEB_SEARCH=\"$1\" SEARCH_RESULTS=\"\"\n\n    # search debian snapshots for package\n    if [[ $(curl -sLI -w \"%{http_code}\" \"https://snapshot.debian.org/binary/?bin=${DEB_SEARCH}\" -o /dev/null) == \"200\" ]]; then\n        SEARCH_RESULTS=$(curl -sL \"https://snapshot.debian.org/binary/?bin=${DEB_SEARCH}\" 2>/dev/null | grep -oP '<li><a href=\"../../\\K.*(?=\")' | head -1)\n        SEARCH_RESULTS=$(curl -sL \"https://snapshot.debian.org/${SEARCH_RESULTS}\" 2>/dev/null | grep -oP \"<a href=\\\"\\K.*${DEB_SEARCH}.*\\.deb(?=\\\")\" | head -1)\n        if [[ -n \"$SEARCH_RESULTS\" ]]; then\n            echo \"https://snapshot.debian.org${SEARCH_RESULTS}\"\n            return 0\n        fi\n    fi\n\n    return 1\n}\n\nfunction aptInstallKernelHeadersFromURI() {\n    local RET=0\n    local KERN_HDR_URI=\"$1\" KERN_HDR_DEB=$(basename \"$1\")\n    local KERN_HDR_COMMON_URI=\"\" KERN_HDR_COMMON_DEB=\"\"\n\n    (\n        # download the .deb file\n        cd /tmp/\n        curl -sLO --retry 3 \"$KERN_HDR_URI\"\n\n        # install dependent common headers\n        KERN_HDR_COMMON_URI=$(\n            debSearch $(\n                dpkg --info \"$KERN_HDR_DEB\" 2>/dev/null |\n                grep 'Depends:' |\n                cut -d ':' -f 2 |\n                tr ',' '\\n' |\n                grep -oP 'linux-headers-.*-common'\n            )\n        ) &&\n        KERN_HDR_COMMON_DEB=$(basename \"$KERN_HDR_COMMON_URI\") &&\n        curl -sLO --retry 3 \"$KERN_HDR_COMMON_URI\" && {\n            apt-get install -y ./${KERN_HDR_COMMON_DEB}\n            RET=$((RET + $?))\n            apt-get install -y -f\n            rm -f \"$KERN_HDR_COMMON_DEB\"\n        }\n\n        # install the kernel headers\n        apt-get install -y ./${KERN_HDR_DEB}\n        RET=$((RET + $?))\n        rm -f \"$KERN_HDR_DEB\"\n        exit $RET\n    )\n\n    return $?\n}\n\n# prints $1 if not virtual or the package that provides $1 if virtual\nfunction resolveAptVirtualPkg() {\n    apt-cache search \"^$1\\$\" | awk '{print $1}'\n}\n\n# when run from root of a debian repo finds the package dependencies\nfunction getDebDependencies() {\n    local TMP DISCRETE_PKGS CONDITIONAL_PKGS RESULT_PKGS=()\n\n    TMP=$(\n        dpkg-checkbuilddeps 2>&1 |\n        awk -F 'Unmet build dependencies: ' '{print $2}' |\n        perl -pe 's% \\(.*?\\)%%g'\n    )\n    DISCRETE_PKGS=$(perl -pe 's%[^ ]+ \\| [^ ]+%%g' <<<\"$TMP\")\n    CONDITIONAL_PKGS=$(\n        grep -oP '[^ ]+ \\| [^ ]+' <<<\"$TMP\" | (\n            while IFS= read -r LINE; do\n                PKG=$(resolveAptVirtualPkg $(awk -F ' | ' '{print $1}' <<<\"$LINE\"))\n                if [[ -n \"$(apt-cache search $PKG 2>/dev/null)\" ]]; then\n                    echo \"$PKG\"\n                else\n                    PKG=$(resolveAptVirtualPkg $(awk -F ' | ' '{print $2}' <<<\"$LINE\"))\n                    [[ -n \"$(apt-cache search $PKG 2>/dev/null)\" ]] && echo \"$PKG\"\n                fi\n            done\n        )\n    )\n\n    for PKG in $DISCRETE_PKGS; do\n        RESULT_PKGS+=( $(resolveAptVirtualPkg \"$PKG\") )\n    done\n    for PKG in $CONDITIONAL_PKGS; do\n        RESULT_PKGS+=( \"$PKG\" )\n    done\n\n    echo ${RESULT_PKGS[@]}\n}\n\nfunction install {\n    local MISSING_PKGS\n    local NPROC=$(nproc)\n\n    # Install required packages and remove conflicting packages\n    { dpkg -l ufw &>/dev/null && apt-get remove -y ufw || :; } &&\n    case \"${DISTRO_VER}\" in\n        10)\n            apt-get install -y git logrotate rsyslog dpkg-dev &&\n            apt-get install -y -t bullseye libbcg729-0 libbcg729-dev debhelper dkms libglib2.0-dev libncurses-dev \\\n                zlib1g-dev default-libmysqlclient-dev libmariadb-dev firewalld python3 python3-dev python3-websockets \\\n                perl libbencode-perl libcrypt-openssl-rsa-perl libcrypt-rijndael-perl libdigest-crc-perl libnet-interface-perl \\\n                libsocket6-perl libdigest-hmac-perl libio-multiplex-perl libio-socket-inet6-perl libjson-perl libtest2-suite-perl\n            ;;\n        *)\n            apt-get install -y git logrotate rsyslog firewalld dpkg-dev\n            ;;\n    esac\n\n    if (( $? != 0 )); then\n        printerr \"Problem with installing the required libraries for RTPEngine\"\n        return 1\n    fi\n\n    # try installing kernel dev headers in the following order:\n    # 1: headers from repos\n    # 2: headers from snapshot.debian.org\n    # NOTE: headers should be installed for all kernels on the system\n    #       but we do not want to support ancient kernel dependencies\n    (\n        RET=0\n        for OS_KERNEL in $(ls /lib/modules/ 2>/dev/null); do\n            apt-get install -y linux-headers-${OS_KERNEL} ||\n            aptInstallKernelHeadersFromURI $(debSearch linux-headers-${OS_KERNEL})\n            RET=$((RET+$?))\n        done\n        exit $RET\n    )\n\n    # debian ver <= 10 has package conflicts with some older kernels so allow userspace forwarding\n    if (( $? != 0 && ${DISTRO_VER} > 10 )); then\n        printerr \"Problems occurred installing one or more kernel headers\"\n        return 1\n    fi\n\n    ## compile and install RTPEngine as a DEB package\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/rtpengine ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/rtpengine)\" != \"${RTPENGINE_VER}\" ]]; then\n            rm -rf ${SRC_DIR}/rtpengine\n            git clone --depth 1 -c advice.detachedHead=false -b ${RTPENGINE_VER} https://github.com/sipwise/rtpengine.git ${SRC_DIR}/rtpengine\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${RTPENGINE_VER} https://github.com/sipwise/rtpengine.git ${SRC_DIR}/rtpengine\n    fi\n\n    # apply our patches\n    (\n        cd ${SRC_DIR}/rtpengine &&\n        patch -p1 -N <${DSIP_PROJECT_DIR}/rtpengine/deb-${RTPENGINE_VER}.patch\n    )\n    if (( $? > 1 )); then\n        printerr 'Failed patching RTPEngine files prior to build'\n        return 1\n    fi\n\n    # build and install using dpkg\n    (\n        cd ${SRC_DIR}/rtpengine\n\n        # install all missing dependencies from the control file\n        MISSING_PKGS=$(getDebDependencies)\n        [[ -n \"$MISSING_PKGS\" ]] && apt-get install -y $MISSING_PKGS\n\n        dpkg-buildpackage -us -uc -sa --jobs=$NPROC || exit 1\n\n        systemctl mask ngcp-rtpengine-daemon.service\n\n        apt-get install -y ../ngcp-rtpengine-daemon_*${RTPENGINE_VER}*.deb ../ngcp-rtpengine-iptables_*${RTPENGINE_VER}*.deb \\\n            ../ngcp-rtpengine-kernel-dkms_*${RTPENGINE_VER}*.deb ../ngcp-rtpengine-utils_*${RTPENGINE_VER}*.deb || exit 1\n\n        systemctl unmask ngcp-rtpengine-daemon.service\n        systemctl disable ngcp-rtpengine-daemon.service\n\n        exit 0\n    )\n\n    if (( $? != 0 )); then\n        printerr \"Problem installing RTPEngine DEB's\"\n        return 1\n    fi\n\n    # make sure RTPEngine kernel module configured\n    # skip this check for older versions as we allow userspace forwarding\n    if (( ${DISTRO_VER} > 10 )); then\n        if [[ -z \"$(find /lib/modules/${OS_KERNEL}/ -name 'xt_RTPENGINE.ko' 2>/dev/null)\" ]]; then\n            printerr \"Problem installing RTPEngine kernel module\"\n            return 1\n        fi\n    fi\n\n    # ensure config dirs exist\n    mkdir -p /run/rtpengine ${SYSTEM_RTPENGINE_CONFIG_DIR}\n    chown -R rtpengine:rtpengine /run/rtpengine\n\n    # allow root to fix permissions before starting services (required to work with SELinux enabled)\n    usermod -a -G rtpengine root\n\n    # setup rtpengine defaults file\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/configs/default.conf /etc/default/rtpengine.conf\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall rules for RTPEngine\n    firewall-cmd --zone=public --add-port=${RTP_PORT_MIN}-${RTP_PORT_MAX}/udp --permanent\n    firewall-cmd --reload\n\n    # Setup RTPEngine Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rtpengine.conf /etc/rsyslog.d/rtpengine.conf\n    touch /var/log/rtpengine.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/rtpengine /etc/logrotate.d/rtpengine\n\n    # Setup tmp files\n    echo \"d /var/run/rtpengine.pid  0755 rtpengine rtpengine - -\" > /etc/tmpfiles.d/rtpengine.conf\n\n    # Reconfigure systemd service files\n    rm -f /lib/systemd/system/rtpengine*.service\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/systemd/rtpengine-v3.service /lib/systemd/system/rtpengine.service\n    chmod 644 /lib/systemd/system/rtpengine.service\n    systemctl daemon-reload\n    systemctl enable rtpengine\n\n    # preliminary check that rtpengine actually installed\n    if cmdExists rtpengine; then\n        return 0\n    else\n        return 1\n    fi\n}\n\n# Remove RTPEngine\nfunction uninstall {\n    systemctl stop rtpengine\n    systemctl disable rtpengine\n    rm -f /{etc,lib}/systemd/system/rtpengine.service 2>/dev/null\n    systemctl daemon-reload\n\n    apt-get remove -y ngcp-rtpengine\\*\n\n    rm -f /usr/sbin/rtpengine* /usr/bin/rtpengine /etc/rsyslog.d/rtpengine.conf /etc/logrotate.d/rtpengine\n\n    # remove our firewall changes\n    firewall-cmd --zone=public --remove-port=${RTP_PORT_MIN}-${RTP_PORT_MAX}/udp --permanent\n    firewall-cmd --reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "rtpengine/el-mr11.5.1.11.patch",
    "content": "Subject: [PATCH] support multiple kernel versions\nchange defaults config path\n---\nIndex: el/rtpengine.spec\nIDEA additional info:\nSubsystem: com.intellij.openapi.diff.impl.patch.CharsetEP\n<+>UTF-8\n===================================================================\ndiff --git a/el/rtpengine.spec b/el/rtpengine.spec\n--- a/el/rtpengine.spec\t(revision a6631401498937d0c03e63931e601c40fcbfa2e7)\n+++ b/el/rtpengine.spec\t(date 1717911032998)\n@@ -179,9 +179,9 @@\n sed -i -e \"s/#MODULE_VERSION#/%{version}-%{release}/g\" %{buildroot}%{_usrsrc}/%{name}-%{version}-%{release}/dkms.conf\n \n %pre\n-getent group %{name} >/dev/null || /usr/sbin/groupadd -r %{name}\n-getent passwd %{name} >/dev/null || /usr/sbin/useradd -r -g %{name} \\\n-\t-s /sbin/nologin -c \"%{name} daemon\" -d %{_sharedstatedir}/%{name} %{name}\n+getent group rtpengine >/dev/null || /usr/sbin/groupadd -r rtpengine\n+getent passwd rtpengine >/dev/null || /usr/sbin/useradd -r -g rtpengine \\\n+\t-s /sbin/nologin -c \"rtpengine daemon\" -d %{_sharedstatedir}/%{name} rtpengine\n \n \n %post\n@@ -198,17 +198,23 @@\n # Add to DKMS registry, build, and install module\n # The kernel version can be overridden with \"--define kversion foo\" on rpmbuild,\n # e.g. --define \"kversion 2.6.32-696.23.1.el6.x86_64\"\n+# Multiple kernel versions can be set by delimiting them with \",\"\n+# e.g. --define \"kversion 5.14.0-325.el9.x86_64,5.14.0-325.el9.x86_64\"\n %{!?kversion: %define kversion %{nil}}\n \n %if \"%{kversion}\" != \"\"\n-  dkms add -m %{name} -v %{version}-%{release} --rpm_safe_upgrade &&\n-  dkms build -m %{name} -v %{version}-%{release} -k %{kversion} --rpm_safe_upgrade &&\n-  dkms install -m %{name} -v %{version}-%{release} -k %{kversion} --rpm_safe_upgrade --force\n+%if 0%{?lua:print(1)}\n+%define kparams %{lua: t = {}; k = rpm.expand(\"%{kversion}\"); for s in string.gmatch(k, \"[^,]+\") do table.insert(t, \"-k \"..s) end; print(table.concat(t, \" \"))}\n+%else\n+%define kparams %(RES=(); IFS=',' read -ra ARR <<<\"%{kversion}\"; for STR in \"${ARR[@]}\"; do RES+=(\"-k $STR\"); done; IFS=' ' echo -n \"${RES[@]}\")\n+%endif\n %else\n-  dkms add -m %{name} -v %{version}-%{release} --rpm_safe_upgrade &&\n-  dkms build -m %{name} -v %{version}-%{release} --rpm_safe_upgrade &&\n-  dkms install -m %{name} -v %{version}-%{release} --rpm_safe_upgrade --force\n+%define kparams %{nil}\n %endif\n+\n+dkms add -m %{name} -v %{version}-%{release} --rpm_safe_upgrade &&\n+dkms build -m %{name} -v %{version}-%{release} %{kparams} --rpm_safe_upgrade &&\n+dkms install -m %{name} -v %{version}-%{release} %{kparams} --rpm_safe_upgrade --force\n true\n \n \nIndex: el/ngcp-rtpengine-iptables-setup\nIDEA additional info:\nSubsystem: com.intellij.openapi.diff.impl.patch.CharsetEP\n<+>UTF-8\n===================================================================\ndiff --git a/el/ngcp-rtpengine-iptables-setup b/el/ngcp-rtpengine-iptables-setup\n--- a/el/ngcp-rtpengine-iptables-setup\t(revision a6631401498937d0c03e63931e601c40fcbfa2e7)\n+++ b/el/ngcp-rtpengine-iptables-setup\t(date 1717809128848)\n@@ -4,7 +4,7 @@\n MODNAME=xt_RTPENGINE\n MANAGE_IPTABLES=yes\n \n-DEFAULTS=/etc/sysconfig/rtpengine\n+DEFAULTS=/etc/default/rtpengine.conf\n \n # Load startup options if available\n if [ -f \"$DEFAULTS\" ]; then\n"
  },
  {
    "path": "rtpengine/rhel/install.sh",
    "content": "#!/usr/bin/env bash\n\n# TODO: update based off latest changes in rtpengine/amzn/install.sh\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\n# compile and install rtpengine from RPM's\nfunction install {\n    local OS_ARCH=$(uname -m)\n    local OS_KERNEL=$(uname -r)\n    local DISTRO_VER=$(source /etc/os-release; echo \"$VERSION_ID\")\n    local DISTRO_MAJVER=$(cut -d '.' -f 1 <<<\"$DISTRO_VER\")\n\n    # Install required libraries\n    dnf install -y epel-release &&\n    {\n        dnf config-manager -y --set-enabled codeready-builder-for-rhel-9-$OS_ARCH-rpms ||\n        dnf config-manager -y --set-enabled codeready-builder-for-rhel-9-rhui-rpms\n    } &&\n    dnf install -y http://rpm.dsiprouter.org/dsiprouter-repo.noarch.rpm &&\n    dnf install -y jq curl gcc glib2 glib2-devel zlib zlib-devel openssl openssl-devel pcre pcre-devel libcurl libcurl-devel \\\n        xmlrpc-c xmlrpc-c-devel libpcap libpcap-devel hiredis hiredis-devel json-glib json-glib-devel libevent libevent-devel \\\n        iptables iptables-devel xmlrpc-c-devel gperf redhat-rpm-config rpm-build pkgconfig spandsp-devel pandoc \\\n        freetype-devel fontconfig-devel libxml2-devel nc dkms logrotate rsyslog perl perl-IPC-Cmd bc libwebsockets-devel \\\n        gperf gperftools gperftools-devel gperftools-libs gzip mariadb-devel perl-Config-Tiny spandsp librabbitmq librabbitmq-devel \\\n        ffmpeg ffmpeg-devel libjpeg-turbo-devel mosquitto-devel opus-devel iptables-legacy-devel gcc-toolset-14 &&\n    dnf install -y kernel-devel-${OS_KERNEL} kernel-headers-${OS_KERNEL}\n\n    if (( $? != 0 )); then\n        printerr \"Problem with installing the required libraries for RTPEngine\"\n        exit 1\n    fi\n    BUILD_KERN_VERSIONS=$(joinwith '' ',' '' $(rpm -q kernel-headers | sed 's/kernel-headers-//g'))\n\n    # rtpengine >= mr11.3.1.1 requires curl >= 7.43.0\n    if versionCompare \"$(tr -d '[a-zA-Z]' <<<\"$RTPENGINE_VER\")\" gteq \"11.3.1.1\"; then\n        if versionCompare \"$(curl -V | head -1 | awk '{print $2}')\" lt \"7.43.0\"; then\n            printdbg 'curl version is not recent enough.. compiling curl 7.8.0'\n            if [[ ! -d ${SRC_DIR}/curl ]]; then\n                (\n                    cd ${SRC_DIR} &&\n                    curl -sL https://curl.haxx.se/download/curl-7.80.0.tar.gz 2>/dev/null |\n                    tar -xzf - --transform 's%curl-7.80.0%curl%';\n                )\n            fi\n            (\n                cd ${SRC_DIR}/curl &&\n                ./configure --prefix=/usr --libdir=/usr/lib64 --with-ssl &&\n                make -j $NPROC &&\n                make -j $NPROC install &&\n                ldconfig\n            )\n            if (( $? != 0 )); then\n                printerr 'Failed to compile curl'\n                return 1\n            fi\n        fi\n    fi\n\n    # reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/rtpengine ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/rtpengine)\" != \"${RTPENGINE_VER}\" ]]; then\n            rm -rf ${SRC_DIR}/rtpengine\n            git clone --depth 1 -c advice.detachedHead=false -b ${RTPENGINE_VER} https://github.com/sipwise/rtpengine.git ${SRC_DIR}/rtpengine\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${RTPENGINE_VER} https://github.com/sipwise/rtpengine.git ${SRC_DIR}/rtpengine\n    fi\n\n    # apply our patches\n    (\n        cd ${SRC_DIR}/rtpengine &&\n        patch -p1 -N <${DSIP_PROJECT_DIR}/rtpengine/el-${RTPENGINE_VER}.patch\n    )\n    if (( $? > 1 )); then\n        printerr 'Failed patching RTPEngine files prior to build'\n        return 1\n    fi\n\n    RTPENGINE_RPM_VER=$(grep -oP 'Version:.+?\\K[\\w\\.\\~\\+]+' ${SRC_DIR}/rtpengine/el/rtpengine.spec)\n    RPM_BUILD_ROOT=\"${HOME}/rpmbuild\"\n    rm -rf ${RPM_BUILD_ROOT} 2>/dev/null\n    mkdir -p ${RPM_BUILD_ROOT}/SOURCES &&\n    (\n        cd ${SRC_DIR} &&\n        tar -czf ${RPM_BUILD_ROOT}/SOURCES/ngcp-rtpengine-${RTPENGINE_RPM_VER}.tar.gz \\\n            --transform=\"s%^rtpengine%ngcp-rtpengine-$RTPENGINE_RPM_VER%g\" rtpengine/ &&\n        echo \"%__make $(which make) -j $NPROC\" >~/.rpmmacros &&\n        # fix for BUG: \"exec_prefix: command not found\"\n        function exec_prefix() { echo -n '/usr'; } && export -f exec_prefix &&\n        # build the RPM's\n        rpmbuild -ba --define \"kversion $BUILD_KERN_VERSIONS\" ${SRC_DIR}/rtpengine/el/rtpengine.spec &&\n        rm -f ~/.rpmmacros && unset -f exec_prefix &&\n        systemctl mask ngcp-rtpengine-daemon.service\n\n        # install the RPM's\n        if (( ${DISTRO_MAJVER} >= 8 )); then\n            dnf install -y ${RPM_BUILD_ROOT}/RPMS/${OS_ARCH}/ngcp-rtpengine-${RTPENGINE_RPM_VER}*.rpm \\\n                ${RPM_BUILD_ROOT}/RPMS/noarch/ngcp-rtpengine-dkms-${RTPENGINE_RPM_VER}*.rpm \\\n                ${RPM_BUILD_ROOT}/RPMS/${OS_ARCH}/ngcp-rtpengine-kernel-${RTPENGINE_RPM_VER}*.rpm\n        else\n            yum localinstall -y ${RPM_BUILD_ROOT}/RPMS/${OS_ARCH}/ngcp-rtpengine-${RTPENGINE_RPM_VER}*.rpm \\\n                ${RPM_BUILD_ROOT}/RPMS/noarch/ngcp-rtpengine-dkms-${RTPENGINE_RPM_VER}*.rpm \\\n                ${RPM_BUILD_ROOT}/RPMS/${OS_ARCH}/ngcp-rtpengine-kernel-${RTPENGINE_RPM_VER}*.rpm\n        fi\n    )\n\n    if (( $? != 0 )); then\n        printerr \"Problems occurred compiling rtpengine\"\n        return 1\n    fi\n\n    # warn user if kernel module not loaded yet\n    if (( $REBOOT_REQUIRED == 1 )); then\n        printwarn \"A reboot is required to load the RTPEngine kernel module\"\n    fi\n\n    # ensure config dirs exist\n    mkdir -p /run/rtpengine ${SYSTEM_RTPENGINE_CONFIG_DIR}\n    chown -R rtpengine:rtpengine /run/rtpengine\n\n    # setup rtpengine defaults file\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/configs/default.conf /etc/default/rtpengine.conf\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    if (( $? != 0 )) && (( ${DISTRO_MAJVER} == 7 )); then\n        # fix for bug: https://bugzilla.redhat.com/show_bug.cgi?id=1575845\n        systemctl restart dbus\n        systemctl restart firewalld\n        # fix for ensuing bug: https://bugzilla.redhat.com/show_bug.cgi?id=1372925\n        systemctl restart systemd-logind\n    fi\n\n    # give rtpengine permissions in selinux\n    semanage port -a -t rtp_media_port_t -p udp ${RTP_PORT_MIN}-${RTP_PORT_MAX} ||\n    semanage port -m -t rtp_media_port_t -p udp ${RTP_PORT_MIN}-${RTP_PORT_MAX}\n\n    # Setup Firewall rules for RTPEngine\n    firewall-cmd --zone=public --add-port=${RTP_PORT_MIN}-${RTP_PORT_MAX}/udp --permanent\n    firewall-cmd --reload\n\n    # Setup RTPEngine Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rtpengine.conf /etc/rsyslog.d/rtpengine.conf\n    touch /var/log/rtpengine.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/rtpengine /etc/logrotate.d/rtpengine\n\n    # Setup tmp files\n    echo \"d /run/rtpengine/rtpengine.pid  0755 rtpengine rtpengine - -\" > /etc/tmpfiles.d/rtpengine.conf\n\n    # Reconfigure systemd service files\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/systemd/rtpengine-v1.service /lib/systemd/system/rtpengine.service\n    chmod 644 /lib/systemd/system/rtpengine.service\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/rtpengine-{start-pre,stop-post} /usr/sbin/\n    chmod +x /usr/sbin/rtpengine-{start-pre,stop-post} /usr/bin/rtpengine\n    systemctl daemon-reload\n    systemctl enable rtpengine\n\n    # preliminary check that rtpengine actually installed\n    if cmdExists rtpengine; then\n        return 0\n    else\n        return 1\n    fi\n}\n\n# Remove RTPEngine\nfunction uninstall {\n    systemctl stop rtpengine\n    systemctl disable rtpengine\n    rm -f /{etc,lib}/systemd/system/rtpengine.service 2>/dev/null\n    systemctl daemon-reload\n\n    yum remove -y ngcp-rtpengine\\*\n\n    rm -f /usr/sbin/rtpengine-{start-pre,stop-post}\n    rm -f /usr/bin/rtpengine\n    rm -f /etc/rsyslog.d/rtpengine.conf\n    rm -f /etc/logrotate.d/rtpengine\n\n    # remove our firewall changes\n    firewall-cmd --zone=public --remove-port=${RTP_PORT_MIN}-${RTP_PORT_MAX}/udp --permanent\n    firewall-cmd --reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "rtpengine/rocky/install.sh",
    "content": "#!/usr/bin/env bash\n\n# TODO: update based off latest changes in rtpengine/amzn/install.sh\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\n# search for RPM using rocky linux vault repos\n# not guaranteed to find an RPM, returns 1 if not found\n# arguments:\n#   $1 == rpm to search for\nfunction vaultSearch() {\n    local RPM_SEARCH RPM_PATH\n\n    RPM_SEARCH=\"$1\"\n\n    RPM_PATH=$(\n        curl -s https://dl.rockylinux.org/vault/rocky/fullfilelist |\n        grep -m 1 \"$RPM_SEARCH\"'.rpm$'\n    )\n    (( $? == 0 )) || return 1\n\n    VERSIONS_TO_SEARCH=($(\n        curl -s https://dl.rockylinux.org/vault/rocky/ |\n        perl -e \"\\$distro_majver='$DISTRO_MAJVER'; @matches=();\" -0777 -e '\n            $html = do { local $/; <STDIN> };\n            @matches = ($html =~ m%(?<=\\<a href=[\"'\"'\"'])(${distro_majver}\\.[0-9a-zA-Z-]+)/(?=[\"'\"'\"']\\>)%g);\n            foreach my $match (@matches) { print \"${match}\\n\"; }\n        ' 2>/dev/null\n    ))\n\n    echo \"https://dl.rockylinux.org/vault/rocky/${RPM_PATH}\"\n    return 0\n}\n\n# try installing in the following order:\n# 1: headers from repos\n# 2: headers from vault repos\nfunction installKernelDevHeaders {\n    local DISTRO_MAJVER=\"$DISTRO_MAJVER\"\n    local OS_ARCH=\"$OS_ARCH\"\n    local OS_KERNEL=\"$OS_KERNEL\"\n    local KERN_DEV KERN_HDR\n\n    dnf install -y kernel-devel-${OS_KERNEL} kernel-headers-${OS_KERNEL} || {\n        KERN_DEV=$(vaultSearch \"kernel-devel-${OS_KERNEL}\") || return 1\n        KERN_HDR=$(vaultSearch \"kernel-headers-${OS_KERNEL}\") || return 1\n\n        dnf install -y \"$KERN_DEV\" &&\n        dnf install -y \"$KERN_HDR\"\n    }\n}\n\n# compile and install rtpengine from RPM's\nfunction install {\n    local RTPENGINE_RPM_VER BUILD_KERN_VERSIONS\n    local REBOOT_REQUIRED=0\n    local OS_ARCH=$(uname -m)\n    local OS_KERNEL=$(uname -r)\n    local RHEL_BASE_VER=$(rpm -E %{rhel})\n    local DISTRO_VER=$(source /etc/os-release; echo \"$VERSION_ID\")\n    local DISTRO_MAJVER=$(cut -d '.' -f 1 <<<\"$DISTRO_VER\")\n    local NPROC=$(nproc)\n\n    # Install required libraries\n    dnf install -y distribution-gpg-keys &&\n    dnf install -y epel-release &&\n    dnf config-manager --enable -y devel &&\n    if (( ${DISTRO_MAJVER} == 9 )); then\n        dnf config-manager -y --set-enabled crb &&\n        dnf install -y http://rpm.dsiprouter.org/dsiprouter-repo.noarch.rpm\n    elif (( ${DISTRO_MAJVER} == 8 )); then\n        dnf config-manager -y --set-enabled powertools &&\n        rpmkeys --import /usr/share/distribution-gpg-keys/rpmfusion/RPM-GPG-KEY-rpmfusion-free-el-${RHEL_BASE_VER} &&\n        dnf --setopt=localpkg_gpgcheck=1 install -y https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-${RHEL_BASE_VER}.noarch.rpm\n    fi &&\n    dnf install -y jq curl gcc glib2 glib2-devel zlib zlib-devel openssl openssl-devel pcre pcre-devel libcurl libcurl-devel \\\n        xmlrpc-c xmlrpc-c-devel libpcap libpcap-devel hiredis hiredis-devel json-glib json-glib-devel libevent libevent-devel \\\n        iptables iptables-devel xmlrpc-c-devel gperf redhat-rpm-config rpm-build pkgconfig spandsp-devel pandoc \\\n        freetype-devel fontconfig-devel libxml2-devel nc dkms logrotate rsyslog perl perl-IPC-Cmd bc libwebsockets-devel \\\n        gperf gperftools gperftools-devel gperftools-libs gzip mariadb-devel perl-Config-Tiny spandsp librabbitmq librabbitmq-devel \\\n        ffmpeg ffmpeg-devel libjpeg-turbo-devel mosquitto-devel opus-devel iptables-legacy-devel gcc-toolset-14 &&\n    installKernelDevHeaders\n\n    if (( $? != 0 )); then\n        printerr \"Problem with installing the required libraries for RTPEngine\"\n        return 1\n    fi\n\n    BUILD_KERN_VERSIONS=$(joinwith '' ',' '' $(rpm -q kernel-headers | sed 's/kernel-headers-//g'))\n\n    # rtpengine >= mr11.3.1.1 requires curl >= 7.43.0\n    if versionCompare \"$(tr -d '[a-zA-Z]' <<<\"$RTPENGINE_VER\")\" gteq \"11.3.1.1\"; then\n        if versionCompare \"$(curl -V | head -1 | awk '{print $2}')\" lt \"7.43.0\"; then\n            printdbg 'curl version is not recent enough.. compiling curl 7.8.0'\n            if [[ ! -d ${SRC_DIR}/curl ]]; then\n                (\n                    cd ${SRC_DIR} &&\n                    curl -sL https://curl.haxx.se/download/curl-7.80.0.tar.gz 2>/dev/null |\n                    tar -xzf - --transform 's%curl-7.80.0%curl%';\n                )\n            fi\n            (\n                cd ${SRC_DIR}/curl &&\n                ./configure --prefix=/usr --libdir=/usr/lib64 --with-ssl &&\n                make -j $NPROC &&\n                make -j $NPROC install &&\n                ldconfig\n            )\n            if (( $? != 0 )); then\n                printerr 'Failed to compile curl'\n                return 1\n            fi\n        fi\n    fi\n\n    # reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/rtpengine ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/rtpengine)\" != \"${RTPENGINE_VER}\" ]]; then\n            rm -rf ${SRC_DIR}/rtpengine\n            git clone --depth 1 -c advice.detachedHead=false -b ${RTPENGINE_VER} https://github.com/sipwise/rtpengine.git ${SRC_DIR}/rtpengine\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${RTPENGINE_VER} https://github.com/sipwise/rtpengine.git ${SRC_DIR}/rtpengine\n    fi\n\n    # apply our patches\n    (\n        cd ${SRC_DIR}/rtpengine &&\n        patch -p1 -N <${DSIP_PROJECT_DIR}/rtpengine/el-${RTPENGINE_VER}.patch\n    )\n    if (( $? > 1 )); then\n        printerr 'Failed patching RTPEngine files prior to build'\n        return 1\n    fi\n\n    RTPENGINE_RPM_VER=$(grep -oP 'Version:.+?\\K[\\w\\.\\~\\+]+' ${SRC_DIR}/rtpengine/el/rtpengine.spec)\n    RPM_BUILD_ROOT=\"${HOME}/rpmbuild\"\n    rm -rf ${RPM_BUILD_ROOT} 2>/dev/null\n    mkdir -p ${RPM_BUILD_ROOT}/SOURCES &&\n    (\n        source scl_source enable gcc-toolset-14 &&\n        cd ${SRC_DIR} &&\n        tar -czf ${RPM_BUILD_ROOT}/SOURCES/ngcp-rtpengine-${RTPENGINE_RPM_VER}.tar.gz \\\n            --transform=\"s%^rtpengine%ngcp-rtpengine-$RTPENGINE_RPM_VER%g\" rtpengine/ &&\n        echo \"%__make $(which make) -j $NPROC\" >~/.rpmmacros &&\n        # fix for BUG: \"exec_prefix: command not found\"\n        function exec_prefix() { echo -n '/usr'; } && export -f exec_prefix &&\n        # build the RPM's\n        rpmbuild -ba --define \"kversion $BUILD_KERN_VERSIONS\" ${SRC_DIR}/rtpengine/el/rtpengine.spec &&\n        rm -f ~/.rpmmacros && unset -f exec_prefix &&\n        systemctl mask ngcp-rtpengine-daemon.service\n\n        # install the RPM's\n        dnf install -y ${RPM_BUILD_ROOT}/RPMS/${OS_ARCH}/ngcp-rtpengine-${RTPENGINE_RPM_VER}*.rpm \\\n            ${RPM_BUILD_ROOT}/RPMS/noarch/ngcp-rtpengine-dkms-${RTPENGINE_RPM_VER}*.rpm \\\n            ${RPM_BUILD_ROOT}/RPMS/${OS_ARCH}/ngcp-rtpengine-kernel-${RTPENGINE_RPM_VER}*.rpm\n    )\n\n    if (( $? != 0 )); then\n        printerr \"Problems occurred compiling rtpengine\"\n        return 1\n    fi\n\n    # warn user if kernel module not loaded yet\n    if (( $REBOOT_REQUIRED == 1 )); then\n        printwarn \"A reboot is required to load the RTPEngine kernel module\"\n    fi\n\n    # ensure config dirs exist\n    mkdir -p /run/rtpengine ${SYSTEM_RTPENGINE_CONFIG_DIR}\n    chown -R rtpengine:rtpengine /run/rtpengine\n\n    # setup rtpengine defaults file\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/configs/default.conf /etc/default/rtpengine.conf\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # give rtpengine permissions in selinux\n    semanage port -a -t rtp_media_port_t -p udp ${RTP_PORT_MIN}-${RTP_PORT_MAX} ||\n    semanage port -m -t rtp_media_port_t -p udp ${RTP_PORT_MIN}-${RTP_PORT_MAX}\n\n    # Setup Firewall rules for RTPEngine\n    firewall-cmd --zone=public --add-port=${RTP_PORT_MIN}-${RTP_PORT_MAX}/udp --permanent\n    firewall-cmd --reload\n\n    # Setup RTPEngine Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rtpengine.conf /etc/rsyslog.d/rtpengine.conf\n    touch /var/log/rtpengine.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/rtpengine /etc/logrotate.d/rtpengine\n\n    # Setup tmp files\n    echo \"d /run/rtpengine/rtpengine.pid  0755 rtpengine rtpengine - -\" > /etc/tmpfiles.d/rtpengine.conf\n\n    # Reconfigure systemd service files\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/systemd/rtpengine-v3.service /lib/systemd/system/rtpengine.service\n    chmod 644 /lib/systemd/system/rtpengine.service\n    systemctl daemon-reload\n    systemctl enable rtpengine\n\n    # preliminary check that rtpengine actually installed\n    if cmdExists rtpengine; then\n        return 0\n    else\n        return 1\n    fi\n}\n\n# Remove RTPEngine\nfunction uninstall {\n    systemctl stop rtpengine\n    systemctl disable rtpengine\n    rm -f /{etc,lib}/systemd/system/rtpengine.service 2>/dev/null\n    systemctl daemon-reload\n\n    yum remove -y ngcp-rtpengine\\*\n\n    rm -f /usr/bin/rtpengine\n    rm -f /etc/rsyslog.d/rtpengine.conf\n    rm -f /etc/logrotate.d/rtpengine\n\n    # remove our selinux changes\n    semanage port -D -t rtp_media_port_t -p udp\n\n    # remove our firewall changes\n    firewall-cmd --zone=public --remove-port=${RTP_PORT_MIN}-${RTP_PORT_MAX}/udp --permanent\n    firewall-cmd --reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "rtpengine/rtpengine-start-pre",
    "content": "#!/bin/sh\n\n# defaults if config file not provided\nPATH=/sbin:/bin:/usr/sbin:/usr/bin\nTABLE=0\nMODNAME=xt_RTPENGINE\nMANAGE_IPTABLES=yes\nDAEMON_OPTIONS_FILE=/var/run/rtpengine/daemon.conf\n\n# $1 contains the path to the configuration file. It is passed in the systemd unit file \n# When calling the rtpengine command, default location:    /etc/default/rtpengine.conf\nDEFAULTS=\"$1\"\n\n# Load rtpengine options if available\nif [ -f $DEFAULTS ]; then\n    . $DEFAULTS || true\nfi\n\nif [ \"$RUN_RTPENGINE\" != \"yes\" ]; then\n\techo \"rtpengine not yet configured. Edit $DEFAULTS first.\"\n\texit 0\nfi\n\n\n# Gradually fill the options of the command rtpengine which starts the RTPEngine daemon\n# The variables used are sourced from the configuration file rtpengine.conf\nOPTIONS=\"\"\nMODPROBE_OPTIONS=\"\"\n\nif [ ! -z \"$INTERFACES\" ]; then\n    for interface in $INTERFACES; do\n        OPTIONS=\"$OPTIONS --interface=$interface\"\n    done\nfi\n\n[ -z \"$CONFIG_FILE\" ] || OPTIONS=\"$OPTIONS --config-file=$CONFIG_FILE\"\n[ -z \"$CONFIG_SECTION\" ] || OPTIONS=\"$OPTIONS --config-section=$CONFIG_SECTION\"\n[ -z \"$ADDRESS\" ] || OPTIONS=\"$OPTIONS --interface=$ADDRESS\"\n[ -z \"$ADV_ADDRESS\" ] || OPTIONS=\"$OPTIONS!$ADV_ADDRESS\"\n[ -z \"$ADDRESS_IPV6\" ] || OPTIONS=\"$OPTIONS --interface=$ADDRESS_IPV6\"\n[ -z \"$ADV_ADDRESS_IPV6\" ] || OPTIONS=\"$OPTIONS!$ADV_ADDRESS_IPV6\"\n[ -z \"$LISTEN_TCP\" ] || OPTIONS=\"$OPTIONS --listen-tcp=$LISTEN_TCP\"\n[ -z \"$LISTEN_UDP\" ] || OPTIONS=\"$OPTIONS --listen-udp=$LISTEN_UDP\"\n[ -z \"$LISTEN_NG\" ] || OPTIONS=\"$OPTIONS --listen-ng=$LISTEN_NG\"\n[ -z \"$LISTEN_CLI\" ] || OPTIONS=\"$OPTIONS --listen-cli=$LISTEN_CLI\"\n[ -z \"$TIMEOUT\" ] || OPTIONS=\"$OPTIONS --timeout=$TIMEOUT\"\n[ -z \"$SILENT_TIMEOUT\" ] || OPTIONS=\"$OPTIONS --silent-timeout=$SILENT_TIMEOUT\"\n[ -z \"$PIDFILE\" ] || OPTIONS=\"$OPTIONS --pidfile=$PIDFILE\"\n[ -z \"$TOS\" ] || OPTIONS=\"$OPTIONS --tos=$TOS\"\n[ -z \"$PORT_MIN\" ] || OPTIONS=\"$OPTIONS --port-min=$PORT_MIN\"\n[ -z \"$PORT_MAX\" ] || OPTIONS=\"$OPTIONS --port-max=$PORT_MAX\"\n[ -z \"$REDIS\" ] || OPTIONS=\"$OPTIONS --redis=$REDIS\"\n[ -z \"$REDIS_DB\" ] || OPTIONS=\"$OPTIONS --redis-db=$REDIS_DB\"\n[ -z \"$REDIS_READ\" ] || OPTIONS=\"$OPTIONS --redis-read=$REDIS_READ\"\n[ -z \"$REDIS_READ_DB\" ] || OPTIONS=\"$OPTIONS --redis-read-db=$REDIS_READ_DB\"\n[ -z \"$REDIS_WRITE\" ] || OPTIONS=\"$OPTIONS --redis-write=$REDIS_WRITE\"\n[ -z \"$REDIS_WRITE_DB\" ] || OPTIONS=\"$OPTIONS --redis-write-db=$REDIS_WRITE_DB\"\n[ -z \"$B2B_URL\" ] || OPTIONS=\"$OPTIONS --b2b-url=$B2B_URL\"\n[ -z \"$NO_FALLBACK\" -o \\( \"$NO_FALLBACK\" != \"1\" -a \"$NO_FALLBACK\" != \"yes\" \\) ] || OPTIONS=\"$OPTIONS --no-fallback\"\nOPTIONS=\"$OPTIONS --table=$TABLE\"\n[ -z \"$LOG_LEVEL\" ] || OPTIONS=\"$OPTIONS --log-level=$LOG_LEVEL\"\n[ -z \"$LOG_FACILITY\" ] || OPTIONS=\"$OPTIONS --log-facility=$LOG_FACILITY\"\n[ -z \"$LOG_FACILITY_CDR\" ] || OPTIONS=\"$OPTIONS --log-facility-cdr=$LOG_FACILITY_CDR\"\n[ -z \"$LOG_FACILITY_RTCP\" ] || OPTIONS=\"$OPTIONS --log-facility-rtcp=$LOG_FACILITY_RTCP\"\n[ -z \"$NUM_THREADS\" ] || OPTIONS=\"$OPTIONS --num-threads=$NUM_THREADS\"\n[ -z \"$DELETE_DELAY\" ] || OPTIONS=\"$OPTIONS --delete-delay=$DELETE_DELAY\"\n[ -z \"$GRAPHITE\" ] || OPTIONS=\"$OPTIONS --graphite=$GRAPHITE\"\n[ -z \"$GRAPHITE_INTERVAL\" ] || OPTIONS=\"$OPTIONS --graphite-interval=$GRAPHITE_INTERVAL\"\n[ -z \"$GRAPHITE_PREFIX\" ] || OPTIONS=\"$OPTIONS --graphite-prefix=$GRAPHITE_PREFIX\"\n[ -z \"$MAX_SESSIONS\" ] || OPTIONS=\"$OPTIONS --max-sessions=$MAX_SESSIONS\"\n[ -z \"$HOMER\" ] || OPTIONS=\"$OPTIONS --homer=$HOMER\"\n[ -z \"$HOMER_PROTOCOL\" ] || OPTIONS=\"$OPTIONS --homer-protocol=$HOMER_PROTOCOL\"\n[ -z \"$HOMER_ID\" ] || OPTIONS=\"$OPTIONS --homer-id=$HOMER_ID\"\nif [ ! -z \"$RECORDING_DIR\" ]; then\n\tOPTIONS=\"$OPTIONS --recording-dir=$RECORDING_DIR\"\n\tif [ ! -d \"$RECORDING_DIR\" ]; then\n\t\tmkdir \"$RECORDING_DIR\" 2> /dev/null\n\t\tchmod 700 \"$RECORDING_DIR\" 2> /dev/null\n\tfi\nfi\n[ -z \"$RECORDING_METHOD\" ] || OPTIONS=\"$OPTIONS --recording-method=$RECORDING_METHOD\"\n[ -z \"$RECORDING_FORMAT\" ] || OPTIONS=\"$OPTIONS --recording-format=$RECORDING_FORMAT\"\n[ -z \"$DTLS_PASSIVE\" ] || ( [ \"$DTLS_PASSIVE\" != \"yes\" ] && [ \"$DTLS_PASSIVE\" != \"1\" ] ) || OPTIONS=\"$OPTIONS --dtls-passive\"\n\nif test \"$FORK\" = \"no\" ; then\n\tOPTIONS=\"$OPTIONS --foreground\"\nfi\n\nif test \"$LOG_STDERR\" = \"yes\" ; then\n\tOPTIONS=\"$OPTIONS --log-stderr\"\nfi\n\n# Handle requested setuid/setgid.\nif ! test -z \"$SET_USER\"; then\n    PUID=$(id -u \"$SET_USER\" 2> /dev/null)\n    test -z \"$PUID\" || MODPROBE_OPTIONS=\"$MODPROBE_OPTIONS proc_uid=$PUID\"\n    if test -z \"$SET_GROUP\"; then\n        PGID=$(id -g \"$SET_USER\" 2> /dev/null)\n        test -z \"$PGID\" || MODPROBE_OPTIONS=\"$MODPROBE_OPTIONS proc_gid=$PGID\"\n    fi\nfi\n\nif ! test -z \"$SET_GROUP\"; then\n    PGID=$(grep \"^$SET_GROUP:\" /etc/group | cut -d: -f3 2> /dev/null)\n    test -z \"$PGID\" || MODPROBE_OPTIONS=\"$MODPROBE_OPTIONS proc_gid=$PGID\"\nfi\n\n# VM / Container Specific - don't use kernel forwarding\nif [ -x /usr/sbin/ngcp-virt-identify ]; then\n    if /usr/sbin/ngcp-virt-identify --type container; then\n        VIRT=\"yes\"\n    fi\nfi\n\nfirewallSetup() {\n    if [ \"$TABLE\" -lt 0 ] || [ \"$VIRT\" = \"yes\" ]; then\n        return\n    fi\n\n    if [ \"$MANAGE_IPTABLES\" != \"yes\" ]; then\n        return\n    fi\n\n    # shellcheck disable=SC2086\n    modprobe $MODNAME $MODPROBE_OPTIONS\n\n    # ensure that the table we want to use doesn't exist\n    if [ -e /proc/rtpengine/control ]; then\n        echo \"del $TABLE\" > /proc/rtpengine/control 2>/dev/null\n    fi\n\n    # Freshly add the iptables rules to forward the udp packets to the iptables-extension \"RTPEngine\":\n    # Remember iptables table = chains, rules stored in the chains\n    # -N (create a new chain with the name rtpengine)\n    iptables -N rtpengine 2> /dev/null\n    # -D: Delete the rule for the target \"rtpengine\" if exists. -j (target): chain name or extension name\n    # from the table \"filter\" (the default -without the option '-t')\n    iptables -D INPUT -j rtpengine 2> /dev/null\n    # Add the rule again so the packets will go to rtpengine chain after the (filter-INPUT) hook point.\n    iptables -I INPUT -j rtpengine\n    # Delete and Insert a rule in the rtpengine chain to forward the UDP traffic\n    iptables -D rtpengine -p udp -j RTPENGINE --id \"$TABLE\" 2>/dev/null\n    iptables -I rtpengine -p udp -j RTPENGINE --id \"$TABLE\"\n    # The same for IPv6\n    ip6tables -N rtpengine 2> /dev/null\n    ip6tables -D INPUT -j rtpengine 2> /dev/null\n    ip6tables -I INPUT -j rtpengine\n    ip6tables -D rtpengine -p udp -j RTPENGINE --id \"$TABLE\" 2>/dev/null\n    ip6tables -I rtpengine -p udp -j RTPENGINE --id \"$TABLE\"\n}\n\nfirewallSetup\necho \"Start Command:    /usr/bin/rtpengine $OPTIONS\"\necho \"OPTIONS='$OPTIONS'\" > $DAEMON_OPTIONS_FILE\nexit 0\n"
  },
  {
    "path": "rtpengine/rtpengine-stop-post",
    "content": "#!/bin/sh\n\n# defaults if config file not provided\nPATH=/sbin:/bin:/usr/sbin:/usr/bin\nTABLE=0\nMODNAME=xt_RTPENGINE\nMANAGE_IPTABLES=yes\n\n# $1 contains the path to the configuration file. It is passed in the systemd unit file\n# When calling the rtpengine command, default location:    /etc/default/rtpengine.conf\nDEFAULTS=\"$1\"\n\n# Load rtpengine options if available\nif [ -f $DEFAULTS ]; then\n    . $DEFAULTS || true\nfi\n\n\nMODPROBE_OPTIONS=\"\"\n\n# Handle requested setuid/setgid.\nif ! test -z \"$SET_USER\"; then\n    PUID=$(id -u \"$SET_USER\" 2> /dev/null)\n    test -z \"$PUID\" || MODPROBE_OPTIONS=\"$MODPROBE_OPTIONS proc_uid=$PUID\"\n    if test -z \"$SET_GROUP\"; then\n        PGID=$(id -g \"$SET_USER\" 2> /dev/null)\n        test -z \"$PGID\" || MODPROBE_OPTIONS=\"$MODPROBE_OPTIONS proc_gid=$PGID\"\n    fi\nfi\n\nif ! test -z \"$SET_GROUP\"; then\n    PGID=$(grep \"^$SET_GROUP:\" /etc/group | cut -d: -f3 2> /dev/null)\n    test -z \"$PGID\" || MODPROBE_OPTIONS=\"$MODPROBE_OPTIONS proc_gid=$PGID\"\nfi\n\n# VM / Container Specific - don't use kernel forwarding\nif [ -x /usr/sbin/ngcp-virt-identify ]; then\n    if /usr/sbin/ngcp-virt-identify --type container; then\n        VIRT=\"yes\"\n    fi\nfi\n\n# After systemd send kill signal to the rtpengine daemon,\n# Wait 3 sec, then clean the iptables stuffs:\n# 1- delete the forwarding table, \n# 2- delete the iptables rules related to rtpengine\n# 3- Unload the kernel module xt_RTPENGINE\nfirewallTeardown() {\n    if [ \"$TABLE\" -lt 0 ] || [ \"$VIRT\" = \"yes\" ]; then\n        return\n    fi\n\n    sleep 3\n\n    # Delete the Table\n    if [ -e /proc/rtpengine/control ]; then\n        echo \"del $TABLE\" > /proc/rtpengine/control 2>/dev/null\n    fi\n\n    if [ \"$MANAGE_IPTABLES\" != \"yes\" ]; then\n        return\n    fi\n\n    # Remove iptables forwarding rules\n    iptables -D rtpengine -p udp -j RTPENGINE --id \"$TABLE\" 2>/dev/null\n    iptables -D INPUT -j rtpengine 2> /dev/null\n    iptables -D rtpengine 2> /dev/null\n\n    # The same for ip6tables rules\n    ip6tables -D rtpengine -p udp -j RTPENGINE --id \"$TABLE\" 2>/dev/null\n    ip6tables -D INPUT -j rtpengine 2> /dev/null\n    ip6tables -D rtpengine 2> /dev/null\n\n    # Remove kernel module if loaded\n    if lsmod | grep -q \"$MODNAME\" 2>/dev/null; then\n        rmmod $MODNAME 2>/dev/null\n    fi\n}\n\nfirewallTeardown\nexit 0\n"
  },
  {
    "path": "rtpengine/systemd/dummy.service",
    "content": "[Unit]\nDescription=RTPEngine Dummy Service\n\n[Service]\nType=oneshot\nExecStart=/bin/true\nRemainAfterExit=true\nTimeoutSec=0\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "rtpengine/systemd/rtpengine-v1.service",
    "content": "[Unit]\nDescription=RTPEngine proxy for RTP and other media streams\nRequires=basic.target network.target\nAfter=network.target network-online.target systemd-journald.socket basic.target\nAfter=iptables.service redis.service rsyslog.service\n# iptables.service is required only if the RTPEngine uses its kernel module.\n# redis.service is required if the Redis server is working on the same machine along with the RTPEngine\nDefaultDependencies=no\n\n[Service]\nType=simple\nPermissionsStartOnly=true\nEnvironment='CFGFILE=/etc/default/rtpengine.conf'\nEnvironment='RUNDIR=/run/rtpengine'\n# PIDFile requires an absolute path\nPIDFile=/run/rtpengine/rtpengine.pid\n# ExecStart* requires an absolute path for the program\nExecStartPre=/usr/bin/dsiprouter chown -rtpengine\nExecStartPre=/usr/sbin/rtpengine-start-pre $CFGFILE\nExecStart=/usr/bin/rtpengine -f $OPTIONS\nExecStopPost=/usr/sbin/rtpengine-stop-post $CFGFILE\nRestart=on-failure\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "rtpengine/systemd/rtpengine-v2.service",
    "content": "[Unit]\nDescription=RTPEngine proxy for RTP and other media streams\nRequires=basic.target network.target\nAfter=network.target network-online.target systemd-journald.socket basic.target\nAfter=iptables.service redis.service rsyslog.service\n# iptables.service is required only if the RTPEngine uses its kernel module.\n# redis.service is required if the Redis server is working on the same machine along with the RTPEngine\nDefaultDependencies=no\n\n[Service]\nType=forking\nPermissionsStartOnly=true\nEnvironmentFile=/etc/default/rtpengine.conf\nUser=rtpengine\nGroup=rtpengine\n# runtime only directory /run/rtpengine\nRuntimeDirectory=rtpengine\nRuntimeDirectoryMode=0770\n# PIDFile requires an absolute path\nPIDFile=/run/rtpengine/rtpengine.pid\n# process capabilities\nAmbientCapabilities=CAP_NET_ADMIN CAP_SYS_NICE\nCapabilityBoundingSet=CAP_NET_ADMIN CAP_SYS_NICE\n# ExecStart* requires an absolute path for the program\nExecStartPre=/usr/bin/dsiprouter chown -rtpengine\nExecStartPre=/usr/sbin/ngcp-rtpengine-iptables-setup start\nExecStart=/usr/bin/rtpengine --config-file=${CONFIG_FILE} --pidfile=${PID_FILE}\nExecStopPost=/usr/sbin/ngcp-rtpengine-iptables-setup stop\nRestart=on-failure\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "rtpengine/systemd/rtpengine-v3.service",
    "content": "[Unit]\nDescription=RTPEngine proxy for RTP and other media streams\nRequires=basic.target network.target\nAfter=network.target network-online.target systemd-journald.socket basic.target\nAfter=iptables.service redis.service rsyslog.service\n# iptables.service is required only if the RTPEngine uses its kernel module.\n# redis.service is required if the Redis server is working on the same machine along with the RTPEngine\nDefaultDependencies=no\n\n[Service]\nType=forking\nEnvironmentFile=/etc/default/rtpengine.conf\nUser=rtpengine\nGroup=rtpengine\n# runtime only directory /run/rtpengine\nRuntimeDirectory=rtpengine\nRuntimeDirectoryMode=0770\n# PIDFile requires an absolute path\nPIDFile=/run/rtpengine/rtpengine.pid\n# process capabilities\nAmbientCapabilities=CAP_NET_ADMIN CAP_SYS_NICE\nCapabilityBoundingSet=CAP_NET_ADMIN CAP_SYS_NICE\n# ExecStart* requires an absolute path for the program\nExecStartPre=!-/usr/bin/dsiprouter chown -rtpengine\nExecStartPre=+/usr/sbin/ngcp-rtpengine-iptables-setup start\nExecStart=/usr/bin/rtpengine --config-file=${CONFIG_FILE} --pidfile=${PID_FILE}\nExecStopPost=+/usr/sbin/ngcp-rtpengine-iptables-setup stop\nRestart=on-failure\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "rtpengine/ubuntu/install.sh",
    "content": "#!/usr/bin/env bash\n\n# TODO: update based off latest changes in rtpengine/debian/install.sh\n\n# Debug this script if in debug mode\n(( $DEBUG == 1 )) && set -x\n\n# Import dsip_lib utility / shared functions if not already\nif [[ \"$DSIP_LIB_IMPORTED\" != \"1\" ]]; then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\nfunction debSearch() {\n    local DEB_SEARCH=\"$1\" SEARCH_URL\n\n    # search security.ubuntu.com for package\n    SEARCH_URL=\"http://security.ubuntu.com/ubuntu/pool/main/l/linux/${DEB_SEARCH}.deb\"\n    if [[ $(curl -sL -I -w \"%{http_code}\" \"$SEARCH_URL\" -o /dev/null) == \"200\" ]]; then\n        echo \"$SEARCH_URL\"\n        return 0\n    fi\n\n    # search archive.ubuntu.com for package\n    SEARCH_URL=\"http://archive.ubuntu.com/ubuntu/pool/main/l/linux/${DEB_SEARCH}.deb\"\n    if [[ $(curl -sL -I -w \"%{http_code}\" \"$SEARCH_URL\" -o /dev/null) == \"200\" ]]; then\n        echo \"$SEARCH_URL\"\n        return 0\n    fi\n\n    # nowhere else we trust to search\n    return 1\n}\n\nfunction aptInstallKernelHeadersFromURI() {\n    local RET=0\n    local KERN_HDR_URI=\"$1\" KERN_HDR_DEB=$(basename \"$1\")\n    local KERN_HDR_COMMON_URI=\"\" KERN_HDR_COMMON_DEB=\"\"\n\n    (\n        # download the .deb file\n        cd /tmp/\n        curl -sLO --retry 3 \"$KERN_HDR_URI\"\n\n        # install dependent common headers\n        KERN_HDR_COMMON_URI=$(\n            debSearch $(\n                dpkg --info \"$KERN_HDR_DEB\" 2>/dev/null |\n                grep 'Depends:' |\n                cut -d ':' -f 2 |\n                tr ',' '\\n' |\n                grep -oP 'linux-headers-.*-common'\n            )\n        ) &&\n        KERN_HDR_COMMON_DEB=$(basename \"$KERN_HDR_COMMON_URI\") &&\n        curl -sLO --retry 3 \"$KERN_HDR_COMMON_URI\" && {\n            apt-get install -y ./${KERN_HDR_COMMON_DEB}\n            RET=$((RET + $?))\n            apt-get install -y -f\n            rm -f \"$KERN_HDR_COMMON_DEB\"\n        }\n\n        # install the kernel headers\n        apt-get install -y ./${KERN_HDR_DEB}\n        RET=$((RET + $?))\n        rm -f \"$KERN_HDR_DEB\"\n        exit $RET\n    )\n\n    return $?\n}\n\n# prints $1 if not virtual or the package that provides $1 if virtual\nfunction resolveAptVirtualPkg() {\n    apt-cache search \"^$1\\$\" | awk '{print $1}'\n}\n\n# when run from root of a debian repo finds the package dependencies\nfunction getDebDependencies() {\n    local TMP DISCRETE_PKGS CONDITIONAL_PKGS RESULT_PKGS=()\n\n    TMP=$(\n        dpkg-checkbuilddeps 2>&1 |\n        awk -F 'Unmet build dependencies: ' '{print $2}' |\n        perl -pe 's% \\(.*?\\)%%g'\n    )\n    DISCRETE_PKGS=$(perl -pe 's%[^ ]+ \\| [^ ]+%%g' <<<\"$TMP\")\n    CONDITIONAL_PKGS=$(\n        grep -oP '[^ ]+ \\| [^ ]+' <<<\"$TMP\" | (\n            while IFS= read -r LINE; do\n                PKG=$(resolveAptVirtualPkg $(awk -F ' | ' '{print $1}' <<<\"$LINE\"))\n                if [[ -n \"$(apt-cache search $PKG 2>/dev/null)\" ]]; then\n                    echo \"$PKG\"\n                else\n                    PKG=$(resolveAptVirtualPkg $(awk -F ' | ' '{print $2}' <<<\"$LINE\"))\n                    [[ -n \"$(apt-cache search $PKG 2>/dev/null)\" ]] && echo \"$PKG\"\n                fi\n            done\n        )\n    )\n\n    for PKG in $DISCRETE_PKGS; do\n        RESULT_PKGS+=( $(resolveAptVirtualPkg \"$PKG\") )\n    done\n    for PKG in $CONDITIONAL_PKGS; do\n        RESULT_PKGS+=( \"$PKG\" )\n    done\n\n    echo ${RESULT_PKGS[@]}\n}\n\nfunction install {\n    local MISSING_PKGS\n    local NPROC=$(nproc)\n\n    # Install required packages and remove conflicting packages\n    { dpkg -l ufw &>/dev/null && apt-get remove -y ufw || :; } &&\n    apt-get install -y git perl logrotate rsyslog firewalld dpkg-dev\n\n    if (( $? != 0 )); then\n        printerr \"Problem with installing the required libraries for RTPEngine\"\n        return 1\n    fi\n\n    # try installing kernel dev headers in the following order:\n    # 1: headers from security.ubuntu.com\n    # 2: headers from archive.ubuntu.com\n    # NOTE: headers should be installed for all kernels on the system\n    #       but we do not want to support ancient kernel dependencies\n    (\n        RET=0\n        for OS_KERNEL in $(ls /lib/modules/ 2>/dev/null); do\n            apt-get install -y linux-headers-${OS_KERNEL} ||\n            aptInstallKernelHeadersFromURI $(debSearch linux-headers-${OS_KERNEL})\n            RET=$((RET+$?))\n        done\n        exit $RET\n    )\n\n    # require kernel module\n    if (( $? != 0 )); then\n        printerr \"Problems occurred installing one or more kernel headers\"\n        return 1\n    fi\n\n    ## compile and install RTPEngine as a DEB package\n    ## reuse repo if it exists and matches version we want to install\n    if [[ -d ${SRC_DIR}/rtpengine ]]; then\n        if [[ \"$(getGitTagFromShallowRepo ${SRC_DIR}/rtpengine)\" != \"${RTPENGINE_VER}\" ]]; then\n            rm -rf ${SRC_DIR}/rtpengine\n            git clone --depth 1 -c advice.detachedHead=false -b ${RTPENGINE_VER} https://github.com/sipwise/rtpengine.git ${SRC_DIR}/rtpengine\n        fi\n    else\n        git clone --depth 1 -c advice.detachedHead=false -b ${RTPENGINE_VER} https://github.com/sipwise/rtpengine.git ${SRC_DIR}/rtpengine\n    fi\n\n    # apply our patches\n    (\n        cd ${SRC_DIR}/rtpengine &&\n        patch -p1 -N <${DSIP_PROJECT_DIR}/rtpengine/deb-${RTPENGINE_VER}.patch\n    )\n    if (( $? > 1 )); then\n        printerr 'Failed patching RTPEngine files prior to build'\n        return 1\n    fi\n\n    # build and install using dpkg\n    (\n        cd ${SRC_DIR}/rtpengine\n\n        # install all missing dependencies from the control file\n        MISSING_PKGS=$(getDebDependencies)\n        [[ -n \"$MISSING_PKGS\" ]] && apt-get install -y $MISSING_PKGS\n\n        dpkg-buildpackage -us -uc -sa --jobs=$NPROC || exit 1\n\n        systemctl mask ngcp-rtpengine-daemon.service\n\n        apt-get install -y ../ngcp-rtpengine-daemon_*${RTPENGINE_VER}*.deb ../ngcp-rtpengine-iptables_*${RTPENGINE_VER}*.deb \\\n            ../ngcp-rtpengine-kernel-dkms_*${RTPENGINE_VER}*.deb ../ngcp-rtpengine-utils_*${RTPENGINE_VER}*.deb || exit 1\n\n        systemctl unmask ngcp-rtpengine-daemon.service\n        systemctl disable ngcp-rtpengine-daemon.service\n\n        exit 0\n    )\n\n    if (( $? != 0 )); then\n        printerr \"Problem installing RTPEngine DEB's\"\n        return 1\n    fi\n\n    # ensure config dirs exist\n    mkdir -p /run/rtpengine ${SYSTEM_RTPENGINE_CONFIG_DIR}\n    chown -R rtpengine:rtpengine /run/rtpengine\n\n    # allow root to fix permissions before starting services (required to work with SELinux enabled)\n    usermod -a -G rtpengine root\n\n    # setup rtpengine defaults file\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/configs/default.conf /etc/default/rtpengine.conf\n\n    # Enable and start firewalld if not already running\n    systemctl enable firewalld\n    systemctl start firewalld\n\n    # Setup Firewall rules for RTPEngine\n    firewall-cmd --zone=public --add-port=${RTP_PORT_MIN}-${RTP_PORT_MAX}/udp --permanent\n    firewall-cmd --reload\n\n    # Setup RTPEngine Logging\n    cp -f ${DSIP_PROJECT_DIR}/resources/syslog/rtpengine.conf /etc/rsyslog.d/rtpengine.conf\n    touch /var/log/rtpengine.log\n    systemctl restart rsyslog\n\n    # Setup logrotate\n    cp -f ${DSIP_PROJECT_DIR}/resources/logrotate/rtpengine /etc/logrotate.d/rtpengine\n\n    # Setup tmp files\n    echo \"d /var/run/rtpengine.pid  0755 rtpengine rtpengine - -\" > /etc/tmpfiles.d/rtpengine.conf\n\n   # Reconfigure systemd service files\n    rm -f /lib/systemd/system/rtpengine*.service\n    cp -f ${DSIP_PROJECT_DIR}/rtpengine/systemd/rtpengine-v3.service /lib/systemd/system/rtpengine.service\n    chmod 644 /lib/systemd/system/rtpengine.service\n    systemctl daemon-reload\n    systemctl enable rtpengine\n\n    # Reload systemd configs\n    systemctl daemon-reload\n    # Enable the RTPEngine to start during boot\n    systemctl enable rtpengine\n\n    # preliminary check that rtpengine actually installed\n    if cmdExists rtpengine; then\n        return 0\n    else\n        return 1\n    fi\n}\n\n# Remove RTPEngine\nfunction uninstall {\n    systemctl stop rtpengine\n    systemctl disable rtpengine\n    rm -f /{etc,lib}/systemd/system/rtpengine.service 2>/dev/null\n    systemctl daemon-reload\n\n    apt-get remove -y ngcp-rtpengine\\*\n\n rm -f /usr/sbin/rtpengine* /usr/bin/rtpengine /etc/rsyslog.d/rtpengine.conf /etc/logrotate.d/rtpengine\n\n    # remove our firewall changes\n    firewall-cmd --zone=public --remove-port=${RTP_PORT_MIN}-${RTP_PORT_MAX}/udp --permanent\n    firewall-cmd --reload\n\n    return 0\n}\n\ncase \"$1\" in\n    install)\n        install && exit 0 || exit 1\n        ;;\n    uninstall)\n        uninstall && exit 0 || exit 1\n        ;;\n    *)\n        printerr \"Usage: $0 [install | uninstall]\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "testing/0.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\ntest=\"Syslog Started\"\n\n# Is service started\nsystemctl is-active --quiet rsyslog; ret=$?\n\nprocess_result \"$test\" $ret\n"
  },
  {
    "path": "testing/1.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\ntest=\"Mysql Started\"\n\n# Is service started\nsystemctl is-active --quiet mariadb; ret=$?\n\nprocess_result \"$test\" $ret\n"
  },
  {
    "path": "testing/10.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\ntest=\"AWS AMI Instance Requirements Satisfied\"\n\n# static settings\nproject_dir=/opt/dsiprouter\ncookie_file=/tmp/cookie\n\n# dynamic settings\nproto=$(getConfigAttrib 'DSIP_PROTO' ${DSIP_CONFIG_FILE})\nport=$(getConfigAttrib 'DSIP_PORT' ${DSIP_CONFIG_FILE})\nexport DSIP_PASSWORD=\"temp\"\n\n# In Accordance With AWS Marketplace Policy:\n# https://docs.aws.amazon.com/marketplace/latest/userguide/product-and-ami-policies.html\nvalidateInstance() {\n    # if server is not AMI Instance then it passes\n    if ! isInstanceAMI; then\n        return 0\n    fi\n\n    # check dsiprouter DSIP_PASSWORD is set to instance id\n    [ \"$(getInstanceID)\" = \"$DSIP_PASSWORD\" ] || return 1\n\n    # check debian-sys-maint user's DSIP_PASSWORD is set to instance-id (file & runtime)\n    if [ -f /etc/debian_version ]; then\n        maintpass=$(grep -oP '^(?!#)DSIP_PASSWORD[ \\t]*=[ \\t]*\\K([\\w\\d\\.\\-]+)' /etc/mysql/debian.cnf | head -1)\n        [ \"$maintpass\" = \"$DSIP_PASSWORD\" ] || return 1\n        mysql --user=\"debian-sys-maint\" --DSIP_PASSWORD=\"$maintpass\" -e 'SELECT VERSION();' >/dev/null\n        [ $? -ne 0 ] && return 1\n    fi\n\n    # dsiprouter accessible via instance public ip\n    public_ip=$(curl -s http://169.254.169.254/latest/meta-data/public-ipv4 2>/dev/null)\n    curl -s -L -f --connect-timeout 2 \"${proto}://${public_ip}:${port}\" >/dev/null\n    [ $? -ne 0 ] && return 1\n\n    #\n    # TODO: add check for ssh pubkey access using instance key\n    # how to pull key on running instance (install pub key creds section):\n    # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/building-shared-amis.html\n    #\n\n    # if we made it this far all checks passed\n    return 0\n}\n\nvalidateInstance; ret=$?\n\nunset DSIP_PASSWORD\n\nprocess_result \"$test\" $ret"
  },
  {
    "path": "testing/11.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\ntest=\"dSIPRouter GUI Login\"\n\n# static settings\nproject_dir=/opt/dsiprouter\ncookie_file=/tmp/cookie\ntemp_pass='temp'\n\n# dynamic settings\nproto=$(getConfigAttrib 'DSIP_PROTO' ${DSIP_CONFIG_FILE})\nhost='127.0.0.1'\nport=$(getConfigAttrib 'DSIP_PORT' ${DSIP_CONFIG_FILE})\nusername=$(getConfigAttrib 'DSIP_USERNAME' ${DSIP_CONFIG_FILE})\ndsip_id=$(getConfigAttrib 'DSIP_ID' ${DSIP_CONFIG_FILE})\npid_file=$(getConfigAttrib 'DSIP_PID_FILE' ${DSIP_CONFIG_FILE})\nload_from=$(getConfigAttrib 'LOAD_SETTINGS_FROM' ${DSIP_CONFIG_FILE})\nkam_db_host=$(getConfigAttrib 'KAM_DB_HOST' ${DSIP_CONFIG_FILE})\nkam_db_port=$(getConfigAttrib 'KAM_DB_PORT' ${DSIP_CONFIG_FILE})\nkam_db_name=$(getConfigAttrib 'KAM_DB_NAME' ${DSIP_CONFIG_FILE})\nkam_db_user=$(getConfigAttrib 'KAM_DB_USER' ${DSIP_CONFIG_FILE})\nkam_db_pass=$(decryptConfigAttrib 'KAM_DB_PASS' ${DSIP_CONFIG_FILE})\n\n# overload password\n\n# if dsip is bound to all available addresses use localhost\n[ \"$host\" = \"0.0.0.0\" ] && host=\"localhost\"\n\n# attempt to login to dsiprouter\nbase_url=\"${proto}://${host}:${port}\"\npayload=\"username=$(uriEncode ${username})&password=$(uriEncode ${temp_pass})&nextpage=\"\n\ndeclare -a flat_headers=()\ndeclare -A headers=(\n    ['Accept']='text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8'\n    ['Accept-Encoding']='gzip, deflate'\n    ['Accept-Language']='en-US,en;q=0.9'\n    ['Cache-Control']='max-age=0'\n    ['Connection']='keep-alive'\n    ['Content-Type']='application/x-www-form-urlencoded'\n    ['DNT']='1'\n    ['Host']=\"${host}:${port}\"\n    ['Origin']=\"${proto}://${host}:${port}\"\n    ['Referer']=\"${proto}://${host}:${port}/\"\n    ['Upgrade-Insecure-Requests']='1'\n    ['User-Agent']='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'\n)\nfor key in ${!headers[@]}; do flat_headers+=( \"$key: ${headers[$key]}\" ); done\n\nsetLoginOverride() {\n    # make copy of settings\n    cp -f ${DSIP_CONFIG_FILE} ${DSIP_CONFIG_FILE}.bak\n    # update setting\n    if [[ \"$load_from\" == \"file\" ]]; then\n        setConfigAttrib 'DSIP_PASSWORD' \"${temp_pass}\" ${DSIP_CONFIG_FILE} -q\n    elif [[ \"$load_from\" == \"db\" ]]; then\n        mysql --user=\"${kam_db_user}\" --password=\"${kam_db_pass}\" --host=\"${kam_db_host}\" --port=\"${kam_db_port}\" --database=\"${kam_db_name}\" \\\n            -e \"update dsip_settings set DSIP_PASSWORD='${temp_pass}' where dsip_id=${dsip_id}\"\n    fi\n    # sync settings\n    kill -SIGUSR1 $(cat $pid_file) 2>/dev/null\n    sleep 1\n}\n\nunsetLoginOverride() {\n    # revert changes\n    mv -f ${DSIP_CONFIG_FILE}.bak ${DSIP_CONFIG_FILE}\n    # sync settings\n    kill -SIGUSR1 $(cat $pid_file) 2>/dev/null\n    sleep 1\n}\n\nvalidateDsipAuth() {\n    # attempt to auth and store cookie, we will get a 200 OK on good auth\n    status=$(curl -s -L --connect-timeout 3 -c \"$cookie_file\" -w \"%{http_code}\" -d \"$payload\" \"${flat_headers[@]/#/-H}\" \"$base_url/login\" -o /dev/null)\n    [ ${status:-400} -ne 200 ] && return 1\n\n    # try navigating to endpoint without cookie, we should get a 302 redirect\n    status=$(curl -X GET -s --connect-timeout 3 -w \"%{http_code}\" \"${flat_headers[@]/#/-H}\" \"$base_url/carriergroups\" -o /dev/null)\n    [ ${status:-400} -ne 302 ] && return 1\n\n    # try navigating to endpoint with cookie, we should get a 200 OK\n    unset headers['Content-Type']; flat_headers=()\n    for key in ${!headers[@]}; do flat_headers+=( \"$key: ${headers[$key]}\" ); done\n    status=$(curl -X GET -s --connect-timeout 3 -b \"$cookie_file\" -w \"%{http_code}\" \"${flat_headers[@]/#/-H}\" \"$base_url/carriergroups\" -o /dev/null)\n    [ ${status:-400} -ne 200 ] && return 1\n\n    # cleanup, remove cookie\n    rm -f $cookie_file\n\n    # if we made it this far all checks passed\n    return 0\n}\n\ncleanupHandler() {\n    rm -f $cookie_file\n    unsetLoginOverride\n}\n\n# main\ntrap cleanupHandler EXIT\nsetLoginOverride\nvalidateDsipAuth; ret=$?\n\n\nprocess_result \"$test\" $ret\n"
  },
  {
    "path": "testing/12.sh.dev",
    "content": "#!/usr/bin/env bash\nset -x\n\n. include/common\n\nunitname=\"dSIP Custom Routing\"\n\n# static settings\nusername=\"smoketest\"\nhost=\"localhost\"\nport=\"5060\"\n\n# TODO: add custom routes to db\n# - locality prefix matching\n# - custom kamailio route matching\n# mysql -e 'insert into custom_routing ...'\n# mysql -e 'insert into dr_rules ...'\n\n# TODO: send invite to test routes\n# we should be using a template INVITE and replacing values with sed\n#sipsak -f INVITE.sip -s sip:$username@$host:$port -H $host -vvv >/dev/null\n\nprocess_result \"$unitname\" $ret\n"
  },
  {
    "path": "testing/13.sh.dev",
    "content": "#!/usr/bin/env bash\nset -x\n\n. include/common\n\nunitname=\"FusionPBX Sync Module\"\n\n# static settings\nusername=\"smoketest\"\nhost=\"localhost\"\nport=\"5060\"\n\n# TODO: check that cron entry exists\n\n# TODO: check that nginx server is running and accepts requests\n\n# TODO: test fusionpbx db syncing\n# - create new local db\n# - add some date\n# - add pbx to kamailio db w/ fusionpbx enabled\n# - check that db tables synced\n\nprocess_result \"$unitname\" $ret\n"
  },
  {
    "path": "testing/14.sh.dev",
    "content": "#!/usr/bin/env bash\nset -x\n\n. include/common\n\nunitname=\"Domain Auth\"\n\n# static settings\nusername=\"smoketest\"\nhost=\"localhost\"\nport=\"5060\"\n\n# TODO: add some domain entries\n\n# TODO: send invite with domain auth\n# we should be using a template INVITE and replacing values with sed\n#sipsak -f INVITE.sip -s sip:$username@$host:$port -H $host -vvv >/dev/null\n\nprocess_result \"$unitname\" $ret\n"
  },
  {
    "path": "testing/15.sh.dev",
    "content": "#!/usr/bin/env bash\nset -x\n\n. include/common\n\nunitname=\"Flowroute DID Sync Module\"\n\n# static settings\nusername=\"smoketest\"\nhost=\"localhost\"\nport=\"5060\"\n\n# TODO: create some DID's using flowroute's API (not ours)\n\n# TODO: test flowroute module's python functions (verify we get data we sent to flowroute)\n\n# TODO: check DB and see if those DID's were synced\n\nprocess_result \"$unitname\" $ret\n"
  },
  {
    "path": "testing/16.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\nunitname=\"JSONRPC Access to Kamailio\"\n\n# static settings\nproject_dir=/opt/dsiprouter\nsource_ip=\"127.0.0.1\"\nsip_port=\"5060\"\nrpc_proto=\"http\"\nhost=\"localhost\"\n\n# Send a bunch of of requests to the server\ncurl -s -X GET --connect-timeout 3 -d '{\"jsonrpc\": \"2.0\", \"method\": \"core.psx\", \"id\": 1}' \"${rpc_proto}://${host}:${sip_port}/api/kamailio\" > /dev/null\nret=$?\n\nprocess_result \"$unitname\" $ret"
  },
  {
    "path": "testing/17.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\nunitname=\"Domain Pass-Thru using FreePBX\"\n\n# static settings\nusername=\"1001\"\npassword=\"1RSk8l6VGKUsl0zzUFYmIwsAIFT9qARM4vGoVB0pf88=\"\ndomain=\"smoketest.com\"\nhost=\"localhost\"\nport=\"5060\"\nexternalip=$(getExternalIP)\ninternalip=$(ip route get 8.8.8.8 | awk 'NR == 1 {print $7}')\n\n$(addPBX)\n$(addDomain)\n\n#Reload Kamailio Modules\nkamcmd domain.reload\nkamcmd drouting.reload\n\n# Register User\n# Try external ip\nsipsak -U -C sip:$username@home.com --from sip:$username@$domain -u $username -a $password -p $externalip:$port -s sip:$username@$domain -i -vvv >/dev/null\nret=$?\n# Try internal ip if it fails\nif [ \"$ret\" != \"0\" ]; then\n\tsipsak -U -C sip:$username@home.com --from sip:$username@$domain -u $username -a $password -p $internalip:$port -s sip:$username@$domain -i -vvv >/dev/null\n\tret=$?\nfi\n\n#Clean Up\ndeletePBX\ndeleteDomain\n\nprocess_result \"$unitname\" $ret\n"
  },
  {
    "path": "testing/18.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\nunitname=\"DoS and SIP Security - Doesn't Block Known Carrier IP(s)\"\n\n# settings\nsource_ip=\"127.0.0.11\"\nusername=\"smoketest\"\nhost=\"localhost\"\n\n# Add Carrier IP to address table\nmysql kamailio -e \"insert into address values (null,8,'$source_ip',32,0,'name:Smoke Test Carrier,gwgroup:0');\"\n\n# Reload the address table\nkamcmd permissions.addressReload >/dev/null\n\n# Send a bunch of of requests to the server\nsipsak -F -d -k $source_ip  -e 500 -s sip:$username@$host >/dev/null\n\nsleep 1\n# Check the ipban htable to see if the ipaddress is being blocked after sending a \n# bunch of SIP requests\n# Using '!' to negate the return code.  I want return code of 1 to negate to a 0 if the source_ip is not found in the ipban table\n! kamcmd htable.dump ipban | grep -q \"$source_ip\"\nret=$?\n\n# Clean up, remove the entry\nkamcmd htable.delete ipban $source_ip\n\n# TODO: Add a test to validate that the Server user agent is no longer sento\n\n# Remove IP from Carrier table\nmysql kamailio -e \"DELETE FROM address WHERE tag LIKE '%name:Smoke Test Carrier%';\"\n\nprocess_result \"$unitname\" $ret \n"
  },
  {
    "path": "testing/19.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\ntest=\"dSIPRouter API Test\"\n\n# static settings\nproject_dir=/opt/dsiprouter\ncookie_file=/tmp/cookie\ntemp_pass='temp'\ntemp_token='temp'\n\n# dynamic settings\nproto=$(getConfigAttrib 'DSIP_PROTO' ${DSIP_CONFIG_FILE})\nhost='127.0.0.1'\nport=$(getConfigAttrib 'DSIP_PORT' ${DSIP_CONFIG_FILE})\nusername=$(getConfigAttrib 'DSIP_USERNAME' ${DSIP_CONFIG_FILE})\ndsip_id=$(getConfigAttrib 'DSIP_ID' ${DSIP_CONFIG_FILE})\npid_file=$(getConfigAttrib 'DSIP_PID_FILE' ${DSIP_CONFIG_FILE})\nload_from=$(getConfigAttrib 'LOAD_SETTINGS_FROM' ${DSIP_CONFIG_FILE})\nkam_db_host=$(getConfigAttrib 'KAM_DB_HOST' ${DSIP_CONFIG_FILE})\nkam_db_port=$(getConfigAttrib 'KAM_DB_PORT' ${DSIP_CONFIG_FILE})\nkam_db_name=$(getConfigAttrib 'KAM_DB_NAME' ${DSIP_CONFIG_FILE})\nkam_db_user=$(getConfigAttrib 'KAM_DB_USER' ${DSIP_CONFIG_FILE})\nkam_db_pass=$(decryptConfigAttrib 'KAM_DB_PASS' ${DSIP_CONFIG_FILE})\nold_api_token=$(kamcmd cfg.get server api_token)\ninbound_flag=$(getConfigAttrib 'FLT_INBOUND' ${DSIP_CONFIG_FILE})\n\n# if dsip is bound to all available addresses use localhost\n[ \"$host\" = \"0.0.0.0\" ] && host=\"localhost\"\n\n# attempt to login to dsiprouter\nbase_url=\"${proto}://${host}:${port}\"\npayload=\"username=$(uriEncode ${username})&password=$(uriEncode ${temp_pass})&nextpage=\"\n\ndeclare -a flat_headers=()\ndeclare -A headers=(\n    ['Accept']='text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8'\n    ['Accept-Encoding']='gzip, deflate'\n    ['Accept-Language']='en-US,en;q=0.9'\n    ['Cache-Control']='max-age=0'\n    ['Connection']='keep-alive'\n    ['Content-Type']='application/x-www-form-urlencoded'\n    ['DNT']='1'\n    ['Host']=\"${host}:${port}\"\n    ['Origin']=\"${proto}://${host}:${port}\"\n    ['Referer']=\"${proto}://${host}:${port}/\"\n    ['Upgrade-Insecure-Requests']='1'\n    ['User-Agent']='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'\n)\nfor key in ${!headers[@]}; do flat_headers+=( \"$key: ${headers[$key]}\" ); done\n\nsetLoginOverride() {\n    # make copy of settings\n    cp -f ${DSIP_CONFIG_FILE} ${DSIP_CONFIG_FILE}.bak\n    # update setting\n    if [[ \"$load_from\" == \"file\" ]]; then\n        setConfigAttrib 'DSIP_PASSWORD' \"${temp_pass}\" ${DSIP_CONFIG_FILE} -q\n        setConfigAttrib 'DSIP_API_TOKEN' \"${temp_pass}\" ${DSIP_CONFIG_FILE} -q\n    elif [[ \"$load_from\" == \"db\" ]]; then\n        mysql --user=\"${kam_db_user}\" --password=\"${kam_db_pass}\" --host=\"${kam_db_host}\" --port=\"${kam_db_port}\" --database=\"${kam_db_name}\" \\\n            -e \"update dsip_settings set DSIP_PASSWORD='${temp_pass}', DSIP_API_TOKEN='${temp_token}' where dsip_id=${dsip_id}\"\n    fi\n    # sync settings\n    kill -SIGUSR1 $(cat $pid_file) 2>/dev/null\n    sleep 1\n}\n\nunsetLoginOverride() {\n    # revert changes\n    mv -f ${DSIP_CONFIG_FILE}.bak ${DSIP_CONFIG_FILE}\n    # sync settings\n    kill -SIGUSR1 $(cat $pid_file) 2>/dev/null\n    sleep 1\n}\n\n# TODO: update these tests for new endpoint args, etc..\nvalidate() {\n    # update kams api token for testing\n    kamcmd cfg.sets server api_token $temp_token\n\n    # attempt to auth and store cookie, we will get a 200 OK on good auth\n    status=$(curl -s -L --connect-timeout 3 -c \"$cookie_file\" -w \"%{http_code}\" -d \"$payload\" \"${flat_headers[@]/#/-H}\" \"$base_url/login\" -o /dev/null)\n    [ ${status:-400} -ne 200 ] && return 1\n\n    # try navigating to endpoint with cookie, we should get a 200 OK\n    status=$(curl -X GET -s --connect-timeout 3 -b \"$cookie_file\" -w \"%{http_code}\" \"${flat_headers[@]/#/-H}\" \"$base_url/api/v1/kamailio/stats\" -o /dev/null)\n    [ ${status:-400} -ne 200 ] && return 1\n\n    # try again with API token\n    status=$(curl -X GET -s --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" \"$base_url/api/v1/kamailio/stats\" -o /dev/null)\n    [ ${status:-400} -ne 200 ] && return 1\n\n    # create entries for testing /api/v1/mapping endpoint\n    prefix0='123456789'\n    prefix1='987654321'\n    prefix2='01234'\n    prefix3='56789'\n    mysql --user=\"${kam_db_user}\" --password=\"${kam_db_pass}\" --host=\"${kam_db_host}\" --port=\"${kam_db_port}\" --database=\"${kam_db_name}\" \\\n        -e \"insert into dr_rules values (null,'$inbound_flag','$prefix0','',0,'','66,67','name:Test DID Mapping 1');\" \\\n        -e \"insert into dr_rules values (null,'$inbound_flag','$prefix1','',0,'','66','name:Test DID Mapping 2');\"\n    ruleid0=$(mysql --user=\"${kam_db_user}\" --password=\"${kam_db_pass}\" --host=\"${kam_db_host}\" --port=\"${kam_db_port}\" --database=\"${kam_db_name}\" \\\n        -sA -e \"select ruleid from dr_rules where groupid='$inbound_flag' limit 1;\")\n\n\n    # ==========================\n    # GET /api/v1/inboundmapping\n    # ==========================\n    # valid requests\n    [ $(curl -s -X GET --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" \"$base_url/api/v1/inboundmapping\" -o /dev/null) -ne 200 ] && return 1\n    [ $(curl -s -X GET --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" \"$base_url/api/v1/inboundmapping?ruleid=${ruleid0}\" -o /dev/null) -ne 200 ] && return 1\n    [ $(curl -s -X GET --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" \"$base_url/api/v1/inboundmapping?did='\"${prefix1}\"'\" -o /dev/null) -ne 200 ] && return 1\n    [ $(curl -s -X GET --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" \"$base_url/api/v1/inboundmapping?ruleid=1000000\" -o /dev/null) -ne 200 ] && return 1\n    [ $(curl -s -X GET --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" \"$base_url/api/v1/inboundmapping?did=1000000\" -o /dev/null) -ne 200 ] && return 1\n    [ $(curl -s -X GET --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" \"$base_url/api/v1/inboundmapping?ruleid=abcdef\" -o /dev/null) -ne 200 ] && return 1\n    [ $(curl -s -X GET --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" \"$base_url/api/v1/inboundmapping?did=abcdef\" -o /dev/null) -ne 200 ] && return 1\n    # invalid requests\n    [ $(curl -s -X GET --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" \"$base_url/api/v1/inboundmapping?doesntexist=123\" -o /dev/null) -eq 200 ] && return 1\n\n    # ===========================\n    # POST /api/v1/inboundmapping\n    # ===========================\n    # valid requests\n    [ $(curl -s -X POST --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping\" \\\n        -d '{\"did\": \"'\"${prefix2}\"'\", \"servers\": [\"66\",\"67\"], \"name\": \"'\"${prefix2}\"' DID Mapping\"}' -o /dev/null) -ne 200 ] && return 1\n    [ $(curl -s -X POST --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping\" \\\n        -d '{\"did\": \"'\"${prefix3}\"'\",\"servers\": [\"66\",\"67\"]}' -o /dev/null) -ne 200 ] && return 1\n    [ $(curl -s -X POST --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping\" \\\n        -d '{\"did\": \"\", \"servers\": [\"66\"], \"name\": \"Default DID Mapping\"}' -o /dev/null) -ne 200 ] && return 1\n    # invalid requests\n    [ $(curl -s -X POST --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping\" \\\n        -d '{\"servers\": [\"66\",\"67\"], \"name\": \"'\"${prefix2}\"' DID Mapping\"}' -o /dev/null) -eq 200 ] && return 1\n    [ $(curl -s -X POST --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping\" \\\n        -d '{\"did\": \"0\", \"servers\": [\"66\",\"67\",\"68\",\"69\",\"70\",\"71\",\"71\"], \"name\": \"0 DID Mapping\"}' -o /dev/null) -eq 200 ] && return 1\n    [ $(curl -s -X POST --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping\" \\\n        -d '{\"did\": \"00\", \"servers\": [\"\",\"\"], \"name\": \"00 DID Mapping\"}' -o /dev/null) -eq 200 ] && return 1\n    [ $(curl -s -X POST --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping\" \\\n        -d '{\"did\": \"000\", \"servers\": [\"abc\",\"efg\"], \"name\": \"000 DID Mapping\"}' -o /dev/null) -eq 200 ] && return 1\n    [ $(curl -s -X POST --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping\" \\\n        -d '{\"did\": \"0000\", \"servers\": [], \"name\": \"0000 DID Mapping\"}' -o /dev/null) -eq 200 ] && return 1\n    [ $(curl -s -X POST --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping\" \\\n        -d '{\"did\": \"00000\", \"name\": \"00000 DID Mapping\"}' -o /dev/null) -eq 200 ] && return 1\n\n    # ==========================\n    # PUT /api/v1/inboundmapping\n    # ==========================\n    # valid requests\n    [ $(curl -s -X PUT --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping?ruleid=${ruleid0}\" \\\n        -d '{\"did\": \"01234\", \"name\": \"01234 DID Mapping\"}' -o /dev/null) -ne 200 ] && return 1\n    [ $(curl -s -X PUT --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping?did='\"${prefix1}\"'\" \\\n        -d '{\"servers\": [\"67\"]}' -o /dev/null) -ne 200 ] && return 1\n    [ $(curl -s -X PUT --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping?did=10000000\" \\\n        -d '{\"did\": \"01234\", \"name\": \"01234 DID Mapping\"}' -o /dev/null) -ne 200 ] && return 1\n    # invalid requests\n    [ $(curl -s -X PUT --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping?doesntexist=123\" \\\n        -d '{\"name\": \"New DID Mapping\"}' -o /dev/null) -eq 200 ] && return 1\n    [ $(curl -s -X PUT --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping\" \\\n        -d '{\"name\": \"Newer DID Mapping\"}' -o /dev/null) -eq 200 ] && return 1\n    [ $(curl -s -X PUT --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping?did='\"${prefix1}\"'\" \\\n        -d '{}' -o /dev/null) -eq 200 ] && return 1\n    [ $(curl -s -X PUT --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping?did=01234\" \\\n        -d '{\"doesntexist\": \"2\"}' -o /dev/null) -eq 200 ] && return 1\n    [ $(curl -s -X PUT --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" --connect-timeout 3 -H \"Content-Type: application/json\" \"$base_url/api/v1/inboundmapping?did=01234\" \\\n        -d '{\"doesntexist\": \"2\", \"name\": \"New DID Mapping\"}' -o /dev/null) -eq 200 ] && return 1\n\n    # =============================\n    # DELETE /api/v1/inboundmapping\n    # =============================\n    # valid requests\n    [ $(curl -s -X DELETE --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" \"$base_url/api/v1/inboundmapping?ruleid=${ruleid0}\" -o /dev/null) -ne 200 ] && return 1\n    [ $(curl -s -X DELETE --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" \"$base_url/api/v1/inboundmapping?did='\"${prefix1}\"'\" -o /dev/null) -ne 200 ] && return 1\n    # invalid requests\n    [ $(curl -s -X DELETE --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" \"$base_url/api/v1/inboundmapping?doesntexist=123\" -o /dev/null) -eq 200 ] && return 1\n    [ $(curl -s -X DELETE --connect-timeout 3 -H \"Authorization: Bearer ${temp_token}\" -w \"%{http_code}\" \"$base_url/api/v1/inboundmapping\" -o /dev/null) -eq 200 ] && return 1\n\n\n    # if we made it this far all checks passed\n    return 0\n}\n\n# cleanup, remove cookie, remove DB entries\ncleanupHandler() {\n    rm -f $cookie_file\n    mysql --user=\"${kam_db_user}\" --password=\"${kam_db_pass}\" --host=\"${kam_db_host}\" --port=\"${kam_db_port}\" --database=\"${kam_db_name}\" \\\n        -e \"delete from dr_rules where groupid='$inbound_flag' and (prefix='$prefix0' or prefix='$prefix1' or prefix='$prefix2' or prefix='$prefix3');\"\n    kamcmd cfg.sets server api_token $old_api_token\n    unsetLoginOverride\n}\n\n# main\ntrap cleanupHandler EXIT\nsetLoginOverride\nvalidate; ret=$?\n\nprocess_result \"$test\" $ret\n"
  },
  {
    "path": "testing/2.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\ntest=\"dsip-init Service Started\"\n\n# Is service started\nsystemctl is-active --quiet dsip-init; ret=$?\n\nprocess_result \"$test\" $ret \n"
  },
  {
    "path": "testing/20.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\ntest=\"DNSmasq Started\"\n\n# Is service started\nsystemctl is-active --quiet dnsmasq; ret=$?\n\nprocess_result \"$test\" $ret\n"
  },
  {
    "path": "testing/21.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\ntest=\"DNS Resolver Test\"\n\nvalidateResolver() {\n    # can localhost be resolved\n    nslookup localhost 2>&1 >/dev/null || return 1\n\n    # can DMQ domain local.cluster be resolved\n    nslookup local.cluster 2>&1 >/dev/null || return 1\n\n    # can an external host be resolved\n    nslookup www.google.com 2>&1 >/dev/null || return 1\n\n    return 0\n}\n\nvalidateResolver; ret=$?\n\nprocess_result \"$test\" $ret\n"
  },
  {
    "path": "testing/3.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\ntest=\"Kamailio Started\"\n\n# Is service started\nsystemctl is-active --quiet kamailio; ret=$?\n\nprocess_result \"$test\" $ret \n"
  },
  {
    "path": "testing/4.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\ntest=\"dSIPRouter Started\"\n\n# Is service started\nsystemctl is-active --quiet dsiprouter; ret=$?\n\nprocess_result \"$test\" $ret \n"
  },
  {
    "path": "testing/5.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\ntest=\"RTPEngine Started\"\n\n# Is service started\nsystemctl is-active --quiet rtpengine; ret=$?\n\nprocess_result \"$test\" $ret \n"
  },
  {
    "path": "testing/6.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\nunitname=\"PBX and Endpoint Registration\"\n\n\n# settings\nusername=\"smoketest\"\npassword=\"90dsip2432x\"\ndomain=\"sip.dsiprouter.org\"\nhost=\"localhost\"\nport=\"5060\"\n\n# Create a new user\nmysql kamailio -e \"insert into subscriber values (null,'$username','$host','$password','','','','');\"\n\n\n# Register User\nsipsak -U -C sip:$username@$domain -s sip:$username@$host:$port -u $username -a $password -H $host -i -vvv >/dev/null\nret=$?\n\n# Clean up\n# Delete user\nmysql kamailio -e \"delete from subscriber where username='$username' and password='$password';\"\n\n\nprocess_result \"$unitname\" $ret \n\n\n"
  },
  {
    "path": "testing/7.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\nunitname=\"SIP INVITE via Carrier using Username/Password Auth\"\n\n# settings\nusername=\"smoketest\"\npassword=\"90dsip2432x\"\ndomain=\"sip.dsiprouter.org\"\nhost=\"localhost\"\nport=\"5060\"\n\n# Create a new user\nmysql kamailio -e \"insert into subscriber values (null,'$username','$host','$password','','','','');\"\n\n# Add Carrier Group\nmysql kamailio -e \"insert into dr_gw_lists values (null,'','name:Smoketest CarrierGroup');\"\ngwgroupid=`mysql kamailio -s -N -e \"select id from dr_gw_lists where description regexp 'name:Smoketest CarrierGroup(,|\"'$'\")';\"`\n\n# Add Carrier\nmysql kamailio -e \"insert into dr_gateways values (null,8,'demo.dsiprouter.org',0,'','','name:Smoketest Carrier,gwgroup:$gwgroupid');\"\ngwid=`mysql kamailio -s -N -e \"select gwid from dr_gateways where description regexp 'name:Smoketest Carrier(,|\"'$'\")';\"`\n\n# Update the Carrier Group with the Carrier id\nmysql kamailio -e \"update dr_gw_lists set gwlist=$gwid where id=$gwgroupid;\"\n\n# Add Carrier Username/Password Auth info\nexternalip=$(getExternalIP)\nmysql kamailio -e \"insert into uacreg values (null,$gwgroupid,'$username','$externalip','$username','$domain','$domain','$username','$password','','','60','1','0','');\"\n\n# Test auth credentials of the user created\nsipsak -U -C sip:$username@$domain -s sip:$username@$host:$port -u $username -a $password -H $host -i -vvv >/dev/null\nret=$?\n\n# TODO: we need to send INVITE, but sipsak won't allow us to change contact header on invite:\n#sipsak -I -U -C sip:$username@$domain -s sip:$username@$host:$port -u $username -a $password -H $host -i -vvv >/dev/null\n#sipsak -I -U -s sip:$username@$host:$port -u $username -a $password -H $host -i -vvv >/dev/null\n\n# Clean up\n# Delete all database entries\nmysql kamailio -e \"delete from subscriber where username='$username' and password='$password';\"\nmysql kamailio -e \"delete from dr_gw_lists where id=$gwgroupid;\"\nmysql kamailio -e \"delete from dr_gateways where gwid=$gwid;\"\nmysql kamailio -e \"delete from uacreg where l_uuid=$gwgroupid;\"\n\nprocess_result \"$unitname\" $ret\n"
  },
  {
    "path": "testing/8.sh",
    "content": "#!/usr/bin/env bash\n\n. include/common\n\nunitname=\"DoS and SIP Security - Block Unknown IP\"\n\n# settings\nsource_ip=\"127.0.0.10\"\nusername=\"smoketest\"\nhost=\"localhost\"\n\n# Send a bunch of of requests to the server\nsipsak -F -d -k $source_ip  -e 500 -s sip:$username@$host >/dev/null\n\nsleep 1\n# Check the ipban htable to see if the ipaddress is being blocked after sending a \n# bunch of SIP requests\nkamcmd htable.dump ipban | grep -q \"$source_ip\"\nret=$?\n\n# Clean up, remove the entry\nkamcmd htable.delete ipban $source_ip\n\n# TODO: Add a test to validate that the Server user agent is no longer sent\n\nprocess_result \"$unitname\" $ret \n"
  },
  {
    "path": "testing/9.sh.dev",
    "content": "#!/usr/bin/env bash\nset -x\n\n. include/common\n\nunitname=\"Call Detail Records\"\n\n# settings\nusername=\"smoketest\"\nhost=\"localhost\"\nport=\"5060\"\n\n# TODO: send invite to generate CDR's\n# we should be using a template INVITE and replacing values with sed\n#sipsak -f INVITE.sip -s sip:$username@$host:$port -H $host -vvv >/dev/null\n\n# check acc table\nmysql kamailio -sN -e \"select * from acc;\"\n\n# check cdrs table\nmysql kamailio -sN -e \"select * from cdrs;\"\n\nprocess_result \"$unitname\" $ret \n"
  },
  {
    "path": "testing/INVITE.sip",
    "content": "INVITE sip:5554443333@localhost;transport=UDP SIP/2.0\nVia: SIP/2.0/UDP 10.0.0.140:43017;branch=z9hG4bK-d8754z-71c87a1a39321e30-1---d8754z-\nMax-Forwards: 70\nContact: <sip:smoketest@10.0.0.140:43017;transport=UDP>\nTo: <sip:5554443333@localhost;transport=UDP>\nFrom: <sip:smoketest@localhost;transport=UDP>;tag=cbe80728\nCall-ID: YWI1Mjk1OTBmZWEzY2EyYTg4OGFlYjQ5NzhhMWE0Y2I.\nCSeq: 1 INVITE\nAllow: INVITE, ACK, CANCEL, BYE, NOTIFY, REFER, MESSAGE, OPTIONS, INFO, SUBSCRIBE\nContent-Type: application/sdp\nSupported: replaces, norefersub, extended-refer, timer, X-cisco-serviceuri\nUser-Agent: Z 3.3.21937 r21903\nAllow-Events: presence, kpml\nContent-Length: 282\n\nv=0\no=Z 0 0 IN IP4 10.0.0.140\ns=Z\nc=IN IP4 10.0.0.140\nt=0 0\nm=audio 8000 RTP/AVP 18 3 110 8 0 98 101\na=rtpmap:18 G729/8000\na=fmtp:18 annexb=no\na=rtpmap:110 speex/8000\na=rtpmap:98 iLBC/8000\na=fmtp:98 mode=20\na=rtpmap:101 telephone-event/8000\n"
  },
  {
    "path": "testing/Makefile",
    "content": "# Makefile for running test unit\n#\n\n# Our own sorting function\nsort-files = $(shell echo -e $1 | tr ' ' '\\n' | sort -V)\n\n\nTESTS_FILES ?= $(wildcard *.sh)\nTESTS_EXCLUDE ?=\nTESTS ?= $(call sort-files,$(filter-out $(patsubst %,%.sh,$(TESTS_EXCLUDE)), $(TESTS_FILES)))\nDSIP_CONFIG_FILE ?= $(shell grep -oP '^DSIP_CONFIG_FILE[ \\t]*=[ \\t]*\"\\K(.*)(?=\")' include/common 2>/dev/null)\nDEBUG ?= $(shell grep -oP '^DEBUG\\s?\\=\\s?\\K(.*)' $(DSIP_CONFIG_FILE) 2>/dev/null)\n\n_setdebug:\n\t\t@if [ \"$(DEBUG)\" = \"False\" ]; then \\\n\t\t\t\tsed -i -r 's|^DEBUG\\s?\\=\\s?.*|DEBUG = True|g' $(DSIP_CONFIG_FILE) ; \\\n\t\t\t\tsystemctl restart dsiprouter ; \\\n\t\tfi ;\n\n_resetdebug:\n\t\t@if [ \"$(DEBUG)\" = \"False\" ]; then \\\n\t\t\t\tsed -i -r 's|^DEBUG\\s?\\=\\s?.*|DEBUG = False|g' $(DSIP_CONFIG_FILE) ; \\\n\t\t\t\tsystemctl restart dsiprouter ; \\\n\t\tfi ;\n\n_all:\n\t\t@for FILE in $(TESTS) ; do \\\n\t\t\t\tif [ -f $$FILE ] ; then \\\n\t\t\t\t\t\tif [ -x $$FILE ] ; then \\\n\t\t\t\t\t\t\t\techo \"Run test `basename $$FILE .sh`:\" `head -n 2 \"$$FILE\" | tail -n 1 | cut -c 3-` ; \\\n\t\t\t\t\t\t\t\t./$$FILE ; \\\n\t\t\t\t\t\t\t\tret=$$? ; \\\n\t\t\t\t\t\t\t\tif [ ! \"$$ret\" -eq 0 ] ; then \\\n\t\t\t\t\t\t\t\t\t\techo \"Test unit file $$FILE: failed\" ; \\\n\t\t\t\t\t\t\t\telse \\\n\t\t\t\t\t\t\t\t\t\techo \"Test unit file $$FILE: ok\" ; \\\n\t\t\t\t\t\t\t\tfi ; \\\n\t\t\t\t\t\tfi ; \\\n\t\t\t\tfi ; \\\n\t\tdone ; \\\n\t\texit $$RES;\n\n_run:\n\t\t-@if [ -f $(UNIT) ] ; then \\\n\t\t\t\tif [ -x $(UNIT) ] ; then \\\n\t\t\t\t\t\techo \"Run test `basename $(UNIT) .sh`:\" `head -n 2 \"$(UNIT)\" | tail -n 1 | cut -c 3-` ; \\\n\t\t\t\t\t\t./$(UNIT) ; \\\n\t\t\t\t\t\tret=$$? ; \\\n\t\t\t\t\t\tif [ ! \"$$ret\" -eq 0 ] ; then \\\n\t\t\t\t\t\t\t\techo \"Test unit file $(UNIT): failed\" ; \\\n\t\t\t\t\t\telse \\\n\t\t\t\t\t\t\t\techo \"Test unit file $(UNIT): ok\" ; \\\n\t\t\t\t\t\tfi ; \\\n\t\t\t\tfi ; \\\n\t\telse \\\n\t\t\t\techo \"Test unit file $(UNIT): not found\" ; \\\n\t\tfi ;\n\n# run all tests\nall: _setdebug _all _resetdebug\n\n# run one test specified in variable UNIT\n# # example: make UNIT=1.sh run\nrun: _setdebug _run _resetdebug\n"
  },
  {
    "path": "testing/README.md",
    "content": "# Testing dSIPRouter\n\nThis directory contains a number of scripts that contains Unit Test for testing dSIPRouter\nfunctionality.  \n\n## Execute all Unit Tests\n\n```\nmake all\n```\n\n## Execute a Single Unit Tests\n\n```\nmake run UNIT=1.sh\n```\n\n## List of Unit Tests\n\nContains the Unittest number and a description of what the tests validates\n\n|      Unittest Number      |             Test Description             |\n|:-------------------------:|:----------------------------------------:|\n|0|syslog is started|\n|1|mysql is started|\n|2|dsip-init is started|\n|3|kamailio is started|\n|4|dsiprouter is started|\n|5|rtpengine is started|\n|6|PBX and Endpoint Registration|\n|7|SIP INVITE with Username/Password Auth|\n|8|DoS and SIP Security - Block Unknown IP|\n|9|CDR data is stored on SIP INVITE **(dev)**|\n|10|AWS instance deployment requirements satisfied|\n|11|dSIPRouter GUI login|\n|12|dSIP Custom Routing|\n|13|FusionPBX Sync Module|\n|14|Domain Auth|\n|15|Flowroute DID Sync Module|\n|16|Sending JSONRPC Commands to Kamailio|\n|17|Domain Pass-Thru using FreePBX|\n|18|DoS and SIP Security - Doesn't Block Known Carrier IP(s)|\n|19|dSIPRouter API|\n|20|DNSmasq is started|\n|21|DNS Resolver Test|\n**More to come\n"
  },
  {
    "path": "testing/api/dsiprouter.postman_collection.json",
    "content": "{\n\t\"info\": {\n\t\t\"_postman_id\": \"5ac300ac-1cc4-4569-b3fa-514cb76b9e38\",\n\t\t\"name\": \"dsiprouter\",\n\t\t\"schema\": \"https://schema.getpostman.com/json/collection/v2.1.0/collection.json\",\n\t\t\"_exporter_id\": \"1379127\"\n\t},\n\t\"item\": [\n\t\t{\n\t\t\t\"name\": \"endpointgroups\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/endpointgroups\",\n\t\t\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\t\t\"disableBodyPruning\": true\n\t\t\t\t\t},\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/endpointgroups\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"endpointgroups\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/endpointgroups/<int id>\",\n\t\t\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\t\t\"disableBodyPruning\": true\n\t\t\t\t\t},\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/endpointgroups/9\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"endpointgroups\",\n\t\t\t\t\t\t\t\t\"9\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Get a single endpointgroup\"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/endpointgroups (FusionPBX PassThru)\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n    \\\"name\\\": \\\"FusionPBX PassThru\\\",\\n    \\\"call_settings\\\": {\\n        \\\"limit\\\": 5,\\n        \\\"timeout\\\": 3600\\n    },\\n    \\n    \\\"fusionpbx\\\": {\\n        \\\"enabled\\\": true,\\n        \\\"dbhost\\\": \\\"fusionpbx.dsiprouter.net\\\",\\n        \\\"dbuser\\\": \\\"fusionpbx\\\",\\n        \\\"dbpass\\\": \\\"\\\"\\n    }\\n}\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/endpointgroups\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"endpointgroups\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Create an endpointgroup\"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/endpointgroups (SIP Trunk - In/Out - User/Password)\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \" {\\n    \\\"name\\\": \\\"SIP Trunk In/Out - User/Password\\\",\\n    \\\"call_settings\\\": {\\n        \\\"limit\\\": 5,\\n        \\\"timeout\\\": 3600\\n    },\\n    \\\"auth\\\": {\\n        \\\"type\\\": \\\"userpwd\\\",\\n        \\\"user\\\": \\\"18889072085\\\",\\n        \\\"pass\\\": \\\"example\\\",\\n        \\\"domain\\\": \\\"pbx.example.com\\\"\\n    },\\n    \\\"strip\\\": 0,\\n    \\\"prefix\\\": \\\"\\\",\\n    \\\"notifications\\\": {\\n        \\\"overmaxcalllimit\\\": \\\"email@example.com\\\",\\n        \\\"endpointfailure\\\": \\\"email@example.com\\\"\\n    }\\n           \\n}\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/endpointgroups\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"endpointgroups\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Create an endpointgroup\"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/endpointgroups (SIP Trunk - In/Out - User/Password) Copy\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \" {\\n    \\\"name\\\": \\\"SIP Trunk In/Out - User/Password\\\",\\n    \\\"call_settings\\\": {\\n        \\\"limit\\\": 5,\\n        \\\"timeout\\\": 3600\\n    },\\n    \\\"auth\\\": {\\n        \\\"type\\\": \\\"userpwd\\\",\\n        \\\"user\\\": \\\"18889072085\\\",\\n        \\\"pass\\\": \\\"example\\\",\\n        \\\"domain\\\": \\\"pbx.example.com\\\"\\n    },\\n    \\\"strip\\\": 0,\\n    \\\"prefix\\\": \\\"\\\",\\n    \\\"notifications\\\": {\\n        \\\"overmaxcalllimit\\\": \\\"email@example.com\\\",\\n        \\\"endpointfailure\\\": \\\"email@example.com\\\"\\n    }\\n           \\n}\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/endpointgroups\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"endpointgroups\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Create an endpointgroup\"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/endpointgroups (SIP Trunk - IP Auth)\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n    \\\"name\\\": \\\"SIP Trunk IP Auth\\\",\\n    \\\"call_settings\\\": {\\n        \\\"limit\\\": 5,\\n        \\\"timeout\\\": 3600\\n    },\\n    \\\"auth\\\": {\\n        \\\"type\\\": \\\"ip\\\"\\n    },\\n    \\\"endpoints\\\": [\\n                {\\n                    \\\"host\\\": \\\"50.192.97.226\\\",\\n                    \\\"port\\\": 5060,\\n                    \\\"signalling\\\": \\\"proxy\\\",\\n                    \\\"media\\\": \\\"proxy\\\",\\n                    \\\"description\\\": \\\"SIP Trunk Endpoint\\\",\\n                    \\\"rweight\\\": 1\\n                }\\n    ],\\n    \\\"strip\\\": 0,\\n    \\\"prefix\\\": \\\"\\\",\\n    \\\"notifications\\\": {\\n        \\\"overmaxcalllimit\\\": \\\"email@example.com\\\",\\n        \\\"endpointfailure\\\": \\\"email@example.com\\\"\\n    }\\n           \\n}\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/endpointgroups\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"endpointgroups\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Create an endpointgroup\"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/endpointgroups/<int id>\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/endpointgroups/53\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"endpointgroups\",\n\t\t\t\t\t\t\t\t\"53\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Delete endpointgroup\"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/endpointgroups/<int id> (SIP Trunk - IP Auth Update)\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"PUT\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n    \\\"name\\\": \\\"SIP Trunk IP Auth Update\\\",\\n    \\\"call_settings\\\": {\\n        \\\"limit\\\": 5,\\n        \\\"timeout\\\": 3600\\n    },\\n    \\\"auth\\\": {\\n        \\\"type\\\": \\\"ip\\\"\\n    },\\n    \\\"endpoints\\\": [\\n                {\\n                    \\\"host\\\": \\\"50.192.97.227\\\",\\n                    \\\"port\\\": 5060,\\n                    \\\"signalling\\\": \\\"proxy\\\",\\n                    \\\"media\\\": \\\"proxy\\\",\\n                    \\\"description\\\": \\\"SIP Trunk Endpoint\\\",\\n                    \\\"rweight\\\": 1\\n                }\\n    ],\\n    \\\"strip\\\": 0,\\n    \\\"prefix\\\": \\\"\\\",\\n    \\\"notifications\\\": {\\n        \\\"overmaxcalllimit\\\": \\\"email@example.com\\\",\\n        \\\"endpointfailure\\\": \\\"email@example.com\\\"\\n    }\\n           \\n}\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/endpointgroups/34\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"endpointgroups\",\n\t\t\t\t\t\t\t\t\"34\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Update an endpointgroup\"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"kamailio\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/reload/kamailio\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/reload/kamailio\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"reload\",\n\t\t\t\t\t\t\t\t\"kamailio\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Trigger a reload of Kamailio.  This is needed after changes are made\"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/kamailio/stats/\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/kamailio/stats\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"kamailio\",\n\t\t\t\t\t\t\t\t\"stats\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Obtain call statistics \"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"inboundmapping\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/inboundmapping\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/inboundmapping\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"inboundmapping\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Get a list of inboundmappings\"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/inboundmapping\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{ \\n    \\\"did\\\": \\\"13132222223\\\",\\n    \\\"servers\\\": [\\\"#22\\\"],\\n    \\\"name\\\": \\\"Taste Pizzabar\\\"\\n}\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/inboundmapping\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"inboundmapping\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Create new inboundmapping\"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/inboundmapping?did=<string>\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"PUT\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{ \\n    \\\"servers\\\": [\\\"#10\\\"],\\n    \\\"name\\\": \\\"Flyball\\\"\\n}\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/inboundmapping?did=13132222223\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"inboundmapping\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"query\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"did\",\n\t\t\t\t\t\t\t\t\t\"value\": \"13132222223\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Create new inboundmapping\"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/inboundmapping?did=<string>\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/inboundmapping?did=13132222223\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"inboundmapping\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"query\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"did\",\n\t\t\t\t\t\t\t\t\t\"value\": \"13132222223\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Create new inboundmapping\"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"leases\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/lease/endpoint/ (User/Pass)\",\n\t\t\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\t\t\"disableBodyPruning\": true\n\t\t\t\t\t},\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/lease/endpoint?email=mack@goflyball.com&ttl=5m\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"lease\",\n\t\t\t\t\t\t\t\t\"endpoint\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"query\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"email\",\n\t\t\t\t\t\t\t\t\t\"value\": \"mack@goflyball.com\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"ttl\",\n\t\t\t\t\t\t\t\t\t\"value\": \"5m\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Get a single endpointgroup\"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/lease/endpoint/  (IP Type)\",\n\t\t\t\t\t\"protocolProfileBehavior\": {\n\t\t\t\t\t\t\"disableBodyPruning\": true\n\t\t\t\t\t},\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"X-Slack-Signature\",\n\t\t\t\t\t\t\t\t\"value\": \"test\",\n\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/lease/endpoint?email=mack@goflyball.com&ttl=1m&type=ip&auth_ip=172.145.24.2\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"lease\",\n\t\t\t\t\t\t\t\t\"endpoint\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"query\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"email\",\n\t\t\t\t\t\t\t\t\t\"value\": \"mack@goflyball.com\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"ttl\",\n\t\t\t\t\t\t\t\t\t\"value\": \"1m\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"type\",\n\t\t\t\t\t\t\t\t\t\"value\": \"ip\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"auth_ip\",\n\t\t\t\t\t\t\t\t\t\"value\": \"172.145.24.2\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Get a single endpointgroup\"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/lease/endpoint/<leaseid>/revoke\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/lease/endpoint/34/revoke\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"lease\",\n\t\t\t\t\t\t\t\t\"endpoint\",\n\t\t\t\t\t\t\t\t\"34\",\n\t\t\t\t\t\t\t\t\"revoke\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"description\": \"Get a single endpointgroup\"\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"carriergroups\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/carriergroups\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/carriergroups\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"carriergroups\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"/api/v1/carriergroups\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n    \\\"name\\\": \\\"Test1\\\",\\n    \\\"strip\\\": \\\"\\\",\\n    \\\"prefix\\\": \\\"\\\",\\n    \\\"auth\\\": {\\n        \\\"type\\\": \\\"ip\\\",\\n        \\\"r_username\\\": \\\"\\\",\\n        \\\"auth_username\\\": \\\"\\\",\\n        \\\"auth_password\\\": \\\"\\\",\\n        \\\"auth_domain\\\": \\\"\\\",\\n        \\\"auth_proxy\\\": \\\"\\\"\\n    },\\n    \\\"plugin\\\" : {\\n        \\\"name\\\":\\\"\\\",\\n        \\\"plugin_prefix\\\":\\\"\\\",\\n        \\\"account_sid\\\": \\\"\\\",\\n        \\\"account_token\\\":\\\"\\\"\\n    },\\n    \\\"endpoints\\\":[\\n        {\\n            \\\"name\\\": \\\"proxy.detroitpbx.com\\\",\\n            \\\"hostname\\\": \\\"proxy.detroitpbx.com\\\",\\n            \\\"strip\\\": \\\"\\\",\\n            \\\"prefix\\\": \\\"\\\"\\n        }\\n    ]\\n}\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/carriergroups\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"carriergroups\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"auth\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Auth / User - Add\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n    \\\"username\\\": \\\"yahoo2\\\",\\n    \\\"password\\\": \\\"123456\\\",\\n    \\\"firstname\\\": \\\"First\\\",\\n    \\\"lastname\\\": \\\"DeLast\\\",\\n    \\\"roles\\\": [],\\n    \\\"domains\\\": []\\n}\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/auth/user\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"auth\",\n\t\t\t\t\t\t\t\t\"user\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Auth / User - Update\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"PUT\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer \",\n\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n    \\\"username\\\": \\\"de_uzer\\\",\\n    \\\"password\\\": \\\"1234567\\\",\\n    \\\"firstname\\\": \\\"First\\\",\\n    \\\"lastname\\\": \\\"DeLast\\\",\\n    \\\"roles\\\": [],\\n    \\\"domains\\\": []\\n}\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/auth/user/2\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"auth\",\n\t\t\t\t\t\t\t\t\"user\",\n\t\t\t\t\t\t\t\t\"2\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Auth / User - Delete\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer \",\n\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/auth/user/2\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"auth\",\n\t\t\t\t\t\t\t\t\"user\",\n\t\t\t\t\t\t\t\t\"2\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Auth / User - List\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\t\t\t\"disabled\": true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/auth/user\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"auth\",\n\t\t\t\t\t\t\t\t\"user\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Auth / Login\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"auth\": {\n\t\t\t\t\t\t\t\"type\": \"bearer\",\n\t\t\t\t\t\t\t\"bearer\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"token\",\n\t\t\t\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n    \\\"username\\\": \\\"yahoo2\\\",\\n    \\\"password\\\": \\\"123456\\\"\\n}\",\n\t\t\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\t\t\"raw\": {\n\t\t\t\t\t\t\t\t\t\"language\": \"json\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/auth/login\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"auth\",\n\t\t\t\t\t\t\t\t\"login\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"cdr\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"api/v1/cdrs/endpointgroups/<<endpointgroup>>\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/cdrs/endpointgroups/17?type=csv&dtfilter=2022-09-14&email=True\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"cdrs\",\n\t\t\t\t\t\t\t\t\"endpointgroups\",\n\t\t\t\t\t\t\t\t\"17\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"query\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"type\",\n\t\t\t\t\t\t\t\t\t\"value\": \"csv\",\n\t\t\t\t\t\t\t\t\t\"description\": \"csv or json - The default is json\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"dtfilter\",\n\t\t\t\t\t\t\t\t\t\"value\": \"2022-09-14\",\n\t\t\t\t\t\t\t\t\t\"description\": \"Date/Time Filter - mysql format\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"key\": \"email\",\n\t\t\t\t\t\t\t\t\t\"value\": \"True\",\n\t\t\t\t\t\t\t\t\t\"description\": \"Will send the CDR to email addresses in the Endpoint Group\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"api/v1/cdrs/endpoint/<<endpoint>>\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"https://{{DSIP_ADDR}}:5000/api/v1/cdrs/endpoint/54\",\n\t\t\t\t\t\t\t\"protocol\": \"https\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{DSIP_ADDR}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"port\": \"5000\",\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"api\",\n\t\t\t\t\t\t\t\t\"v1\",\n\t\t\t\t\t\t\t\t\"cdrs\",\n\t\t\t\t\t\t\t\t\"endpoint\",\n\t\t\t\t\t\t\t\t\"54\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t],\n\t\"auth\": {\n\t\t\"type\": \"bearer\",\n\t\t\"bearer\": [\n\t\t\t{\n\t\t\t\t\"key\": \"token\",\n\t\t\t\t\"value\": \"{{DSIP_TOKEN}}\",\n\t\t\t\t\"type\": \"string\"\n\t\t\t}\n\t\t]\n\t},\n\t\"event\": [\n\t\t{\n\t\t\t\"listen\": \"prerequest\",\n\t\t\t\"script\": {\n\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\"exec\": [\n\t\t\t\t\t\"\"\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"listen\": \"test\",\n\t\t\t\"script\": {\n\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\"exec\": [\n\t\t\t\t\t\"\"\n\t\t\t\t]\n\t\t\t}\n\t\t}\n\t],\n\t\"variable\": [\n\t\t{\n\t\t\t\"key\": \"DSIP_ADDR\",\n\t\t\t\"value\": \"demo.dsiprouter.net\"\n\t\t},\n\t\t{\n\t\t\t\"key\": \"DSIP_TOKEN\",\n\t\t\t\"value\": \"91RpJidL1f2eEDSyxUDnn83B7jipuDyl\",\n\t\t\t\"type\": \"string\"\n\t\t}\n\t]\n}"
  },
  {
    "path": "testing/include/common",
    "content": "#!/usr/bin/env bash\n\nDSIP_PROJECT_DIR=${DSIP_PROJECT_DIR:-$(dirname $(dirname $(dirname $(readlink -f \"$BASH_SOURCE\"))))}\nDSIP_SYSTEM_CONFIG_DIR=\"/etc/dsiprouter\"\nDSIP_CONFIG_FILE=\"${DSIP_SYSTEM_CONFIG_DIR}/gui/settings.py\"\n\n# import funcs from dsip_lib.sh\nif (( ${DSIP_LIB_IMPORTED:-0} == 0 )); then\n    . ${DSIP_PROJECT_DIR}/dsiprouter/dsip_lib.sh\nfi\n\n# Configured by install script\nPATH_UPDATE_FILE=\"/etc/profile.d/dsip_paths.sh\"\n# Import path updates so we don't have to reload shell to test\n. ${PATH_UPDATE_FILE}\n\nprocess_result() {\n    [ $((${#1}%2)) -eq 0 ] && desc=\"$1\" || desc=\"$1 \"\n\n    if [ $2 -eq 0 ]; then\n        printf \"%-*s %s %*s\\n\" $(( (100-${#desc}) / 2 )) \"($(pprint $(basename \"\"$0\"\" | cut -d '.' -f 1)))\" \"$(printbold \"\"$desc\"\")\" $(( (100-${#desc}) / 2 )) \"[$(printdbg 'PASS')]\"\n        return 0\n    else\n        printf \"%-*s %s %*s\\n\" $(( (100-${#desc}) / 2 )) \"($(pprint $(basename \"\"$0\"\" | cut -d '.' -f 1)))\" \"$(printbold \"\"$desc\"\")\" $(( (100-${#desc}) / 2 )) \"[$(printerr 'FAIL')]\"\n        return 1\n    fi\n}\n\n# $* == string to encode (prevent splitting by slurping all the args)\n# notes: print URI encoded string\nuriEncode() {\n    perl -MURI::Escape -e 'print uri_escape shift, , q{^A-Za-z0-9\\-._~/}' -- \"$*\"\n}\n\n# Add PBX\n# return the PBX ID\naddPBX() {\n\tmysql kamailio -e \"insert into dr_gateways values (null,9,'18.191.20.204',0,'','','name:Smoketest PBX');\"\n\tpbxid=`mysql kamailio -s -N -e \"select gwid from dr_gateways where description regexp 'name:Smoketest PBX(,|\"'$'\")';\"`\n\n\treturn $pbxid\n}\n\ndeletePBX() {\n\tmysql kamailio -e \"delete from dr_gateways where description regexp 'name:Smoketest PBX(,|\"'$'\")';\"\n}\n\naddDomain() {\n\tmysql kamailio -e \"insert into domain values (null,'smoketest.com','smoketest.com',now());\"\n\tmysql kamailio -e \"insert into domain_attrs values (null,'smoketest.com','pbx_list',2,'64',now())\";\n\tmysql kamailio -e \"insert into domain_attrs values (null,'smoketest.com','pbx_type',2,'0',now())\"\n\tmysql kamailio -e \"insert into domain_attrs values (null,'smoketest.com','created_by',2,'0',now());\"\n\tmysql kamailio -e \"insert into domain_attrs values (null,'smoketest.com','domain_auth',2,'passthru',now());\"\n\tmysql kamailio -e \"insert into domain_attrs values (null,'smoketest.com','description',2,'notes:Smoketest Domain',now());\"\n\tmysql kamailio -e \"insert into domain_attrs values (null,'smoketest.com','pbx_ip',2,'18.191.20.204',now());\"\n}\n\ndeleteDomain() {\n\tmysql kamailio -e \"delete from domain where did='smoketest.com';\"\n\tmysql kamailio -e \"delete from domain_attrs where did='smoketest.com';\"\n}\n"
  },
  {
    "path": "testing/payload.json",
    "content": "{ \"name\": \"Endpoint1\",\n  \"calllimit\": \"0\",\n  \"auth\": { \"type\":\"ip\",\n\t    \"user\":\"mack10\",\n\t    \"pass\":\"1234\",\n\t    \"domain\":\"sip.dsiprouter.org\"\n  },\n  \"endpoints\": [\n                    {\"hostname\":\"142.131.313.1\",\"description\":\"Endpoint1\",\"maintmode\":0},\n                    {\"hostname\":\"143.131.344.2\",\"description\":\"Endpoint2\",\"maintmode\":1}\n   ],\n  \"strip\": 0,\n  \"prefix\": \"\",\n  \"notifications\": {\n            \"overmaxcalllimit\": \"mack.hendricks@gmail.com\",\n            \"endpointfailure\": \"mack.hendricks@gmail.com\"\n  },\n  \n  \"fusionpbx\": {\n            \"enabled\": 1,\n            \"dbhost\": \"13.13.24.22\",\n            \"dbuser\": \"fusiopbx\",\n            \"dbpass\": \"1234\"\n            }\n    \n}\n"
  },
  {
    "path": "testing/sql/v0.522/kamailio.sql",
    "content": "-- MySQL dump 10.13  Distrib 5.7.23, for osx10.14 (x86_64)\n--\n-- Host: 127.0.0.1    Database: kamailio\n-- ------------------------------------------------------\n-- Server version\t5.7.27\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!40101 SET NAMES utf8 */;\n/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n/*!40103 SET TIME_ZONE='+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n--\n-- Table structure for table `acc`\n--\n\nDROP TABLE IF EXISTS `acc`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `acc` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `method` varchar(16) NOT NULL DEFAULT '',\n  `from_tag` varchar(64) NOT NULL DEFAULT '',\n  `to_tag` varchar(64) NOT NULL DEFAULT '',\n  `callid` varchar(128) NOT NULL DEFAULT '',\n  `sip_code` char(3) NOT NULL DEFAULT '',\n  `sip_reason` varchar(32) NOT NULL DEFAULT '',\n  `time` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',\n  `src_ip` varchar(64) NOT NULL DEFAULT '',\n  `dst_ouser` varchar(64) NOT NULL DEFAULT '',\n  `dst_user` varchar(64) NOT NULL DEFAULT '',\n  `dst_domain` varchar(128) NOT NULL DEFAULT '',\n  `src_user` varchar(64) NOT NULL DEFAULT '',\n  `src_domain` varchar(128) NOT NULL DEFAULT '',\n  `cdr_id` int(11) NOT NULL DEFAULT '0',\n  `calltype` varchar(20) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `acc_callid` (`callid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `acc`\n--\n\nLOCK TABLES `acc` WRITE;\n/*!40000 ALTER TABLE `acc` DISABLE KEYS */;\n/*!40000 ALTER TABLE `acc` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `acc_cdrs`\n--\n\nDROP TABLE IF EXISTS `acc_cdrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `acc_cdrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `start_time` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',\n  `end_time` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',\n  `duration` float(10,3) NOT NULL DEFAULT '0.000',\n  PRIMARY KEY (`id`),\n  KEY `start_time_idx` (`start_time`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `acc_cdrs`\n--\n\nLOCK TABLES `acc_cdrs` WRITE;\n/*!40000 ALTER TABLE `acc_cdrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `acc_cdrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `active_watchers`\n--\n\nDROP TABLE IF EXISTS `active_watchers`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `active_watchers` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `presentity_uri` varchar(128) NOT NULL,\n  `watcher_username` varchar(64) NOT NULL,\n  `watcher_domain` varchar(64) NOT NULL,\n  `to_user` varchar(64) NOT NULL,\n  `to_domain` varchar(64) NOT NULL,\n  `event` varchar(64) NOT NULL DEFAULT 'presence',\n  `event_id` varchar(64) DEFAULT NULL,\n  `to_tag` varchar(64) NOT NULL,\n  `from_tag` varchar(64) NOT NULL,\n  `callid` varchar(255) NOT NULL,\n  `local_cseq` int(11) NOT NULL,\n  `remote_cseq` int(11) NOT NULL,\n  `contact` varchar(128) NOT NULL,\n  `record_route` text,\n  `expires` int(11) NOT NULL,\n  `status` int(11) NOT NULL DEFAULT '2',\n  `reason` varchar(64) DEFAULT NULL,\n  `version` int(11) NOT NULL DEFAULT '0',\n  `socket_info` varchar(64) NOT NULL,\n  `local_contact` varchar(128) NOT NULL,\n  `from_user` varchar(64) NOT NULL,\n  `from_domain` varchar(64) NOT NULL,\n  `updated` int(11) NOT NULL,\n  `updated_winfo` int(11) NOT NULL,\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `user_agent` varchar(255) DEFAULT '',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `active_watchers_idx` (`callid`,`to_tag`,`from_tag`),\n  KEY `active_watchers_expires` (`expires`),\n  KEY `active_watchers_pres` (`presentity_uri`,`event`),\n  KEY `updated_idx` (`updated`),\n  KEY `updated_winfo_idx` (`updated_winfo`,`presentity_uri`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `active_watchers`\n--\n\nLOCK TABLES `active_watchers` WRITE;\n/*!40000 ALTER TABLE `active_watchers` DISABLE KEYS */;\n/*!40000 ALTER TABLE `active_watchers` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `address`\n--\n\nDROP TABLE IF EXISTS `address`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `address` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `grp` int(11) unsigned NOT NULL DEFAULT '1',\n  `ip_addr` varchar(50) NOT NULL,\n  `mask` int(11) NOT NULL DEFAULT '32',\n  `port` smallint(5) unsigned NOT NULL DEFAULT '0',\n  `tag` varchar(64) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `address`\n--\n\nLOCK TABLES `address` WRITE;\n/*!40000 ALTER TABLE `address` DISABLE KEYS */;\nINSERT INTO `address` VALUES (1,8,'52.41.52.34',32,0,'name:Skyetel North West Inbound,gwgroup:1'),(2,8,'52.8.201.128',32,0,'name:Skyetel South West Inbound,gwgroup:1'),(3,8,'52.60.138.31',32,0,'name:Skyetel North East Inbound,gwgroup:1'),(4,8,'50.17.48.216',32,0,'name:Skyetel South East Inbound,gwgroup:1'),(5,8,'35.156.192.164',32,0,'name:Skyetel Europe Inbound,gwgroup:1'),(6,8,'term.skyetel.com',32,0,'name:Skyetel 1st Priority Outbound Call,gwgroup:1'),(7,8,'52.41.52.34',32,0,'name:Skyetel 2nd Priority Outbound Call,gwgroup:1'),(8,8,'52.8.201.128',32,0,'name:Skyetel 3rd Priority Outbound Call,gwgroup:1'),(9,8,'50.17.48.216',32,0,'name:Skyetel 4rd Priority Outbound Call,gwgroup:1'),(10,8,'52.32.223.28',32,0,'name:Skyetel North West High Cost Outbound Traffic,gwgroup:1'),(11,8,'52.4.178.107',32,0,'name:Skyetel South East High Cost Outbound Traffic,gwgroup:1'),(12,8,'147.75.60.160',32,0,'name:Flowroute US-West-WA,gwgroup:2'),(13,8,'34.210.91.112',32,0,'name:Flowroute US-West-OR,gwgroup:2'),(14,8,'147.75.65.192',32,0,'name:Flowroute US-East-NJ,gwgroup:2'),(15,8,'34.226.36.32',32,0,'name:Flowroute US-East-VA,gwgroup:2'),(16,8,'81.201.82.45',32,0,'name:Voxbone Belgium,gwgroup:3'),(17,8,'81.201.84.195',32,0,'name:Voxbone LA,gwgroup:3'),(18,8,'81.201.85.45',32,0,'name:Voxbone NYC,gwgroup:3'),(19,8,'81.201.83.45',32,0,'name:Voxbone Germany,gwgroup:3'),(20,8,'81.201.86.45',32,0,'name:Voxbone Hong Kong,gwgroup:3'),(21,8,'81.201.84.195',32,0,'name:Voxbone Australia,gwgroup:3'),(22,8,'64.136.174.30',32,0,'name:VI Carrier,gwgroup:4'),(23,8,'64.136.173.22',32,0,'name:VI Carrier,gwgroup:4'),(24,8,'209.166.128.200',32,0,'name:VI Carrier,gwgroup:4'),(25,8,'192.240.151.100',32,0,'name:VI Carrier,gwgroup:4'),(26,8,'64.136.173.31',32,0,'name:VI Carrier,gwgroup:4'),(27,8,'64.136.174.30',32,0,'name:VI Carrier,gwgroup:4'),(28,8,'64.136.174.20',32,0,'name:VI Carrier,gwgroup:4'),(29,8,'209.166.154.70',32,0,'name:VI Carrier,gwgroup:4'),(30,8,'64.136.174.65',32,0,'name:VI Carrier,gwgroup:4'),(31,8,'64.136.173.23',32,0,'name:VI Carrier,gwgroup:4'),(32,8,'209.166.128.201',32,0,'name:VI Carrier,gwgroup:4'),(33,8,'192.240.151.101',32,0,'name:VI Carrier,gwgroup:4'),(34,8,'64.136.173.65',32,0,'name:VI Carrier,gwgroup:4'),(35,8,'64.136.174.65',32,0,'name:VI Carrier,gwgroup:4'),(36,8,'64.136.174.21',32,0,'name:VI Carrier,gwgroup:4'),(37,8,'209.166.154.71',32,0,'name:VI Carrier,gwgroup:4'),(38,8,'72.15.219.140',32,0,'name:Thinq Carrier,gwgroup:5'),(39,8,'216.147.191.157',32,0,'name:Voxtelesys Carrier,gwgroup:6'),(40,8,'64.34.181.47',32,0,'name:Les.net Carrier,gwgroup:7');\n/*!40000 ALTER TABLE `address` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `aliases`\n--\n\nDROP TABLE IF EXISTS `aliases`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `aliases` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `ruid` varchar(64) NOT NULL DEFAULT '',\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) DEFAULT NULL,\n  `contact` varchar(255) NOT NULL DEFAULT '',\n  `received` varchar(128) DEFAULT NULL,\n  `path` varchar(512) DEFAULT NULL,\n  `expires` datetime NOT NULL DEFAULT '2030-05-28 21:32:15',\n  `q` float(10,2) NOT NULL DEFAULT '1.00',\n  `callid` varchar(255) NOT NULL DEFAULT 'Default-Call-ID',\n  `cseq` int(11) NOT NULL DEFAULT '1',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `cflags` int(11) NOT NULL DEFAULT '0',\n  `user_agent` varchar(255) NOT NULL DEFAULT '',\n  `socket` varchar(64) DEFAULT NULL,\n  `methods` int(11) DEFAULT NULL,\n  `instance` varchar(255) DEFAULT NULL,\n  `reg_id` int(11) NOT NULL DEFAULT '0',\n  `server_id` int(11) NOT NULL DEFAULT '0',\n  `connection_id` int(11) NOT NULL DEFAULT '0',\n  `keepalive` int(11) NOT NULL DEFAULT '0',\n  `partition` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `ruid_idx` (`ruid`),\n  KEY `account_contact_idx` (`username`,`domain`,`contact`),\n  KEY `expires_idx` (`expires`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `aliases`\n--\n\nLOCK TABLES `aliases` WRITE;\n/*!40000 ALTER TABLE `aliases` DISABLE KEYS */;\n/*!40000 ALTER TABLE `aliases` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `carrier_name`\n--\n\nDROP TABLE IF EXISTS `carrier_name`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `carrier_name` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `carrier` varchar(64) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `carrier_name`\n--\n\nLOCK TABLES `carrier_name` WRITE;\n/*!40000 ALTER TABLE `carrier_name` DISABLE KEYS */;\n/*!40000 ALTER TABLE `carrier_name` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `carrierfailureroute`\n--\n\nDROP TABLE IF EXISTS `carrierfailureroute`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `carrierfailureroute` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `carrier` int(10) unsigned NOT NULL DEFAULT '0',\n  `domain` int(10) unsigned NOT NULL DEFAULT '0',\n  `scan_prefix` varchar(64) NOT NULL DEFAULT '',\n  `host_name` varchar(128) NOT NULL DEFAULT '',\n  `reply_code` varchar(3) NOT NULL DEFAULT '',\n  `flags` int(11) unsigned NOT NULL DEFAULT '0',\n  `mask` int(11) unsigned NOT NULL DEFAULT '0',\n  `next_domain` int(10) unsigned NOT NULL DEFAULT '0',\n  `description` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `carrierfailureroute`\n--\n\nLOCK TABLES `carrierfailureroute` WRITE;\n/*!40000 ALTER TABLE `carrierfailureroute` DISABLE KEYS */;\n/*!40000 ALTER TABLE `carrierfailureroute` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `carrierroute`\n--\n\nDROP TABLE IF EXISTS `carrierroute`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `carrierroute` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `carrier` int(10) unsigned NOT NULL DEFAULT '0',\n  `domain` int(10) unsigned NOT NULL DEFAULT '0',\n  `scan_prefix` varchar(64) NOT NULL DEFAULT '',\n  `flags` int(11) unsigned NOT NULL DEFAULT '0',\n  `mask` int(11) unsigned NOT NULL DEFAULT '0',\n  `prob` float NOT NULL DEFAULT '0',\n  `strip` int(11) unsigned NOT NULL DEFAULT '0',\n  `rewrite_host` varchar(128) NOT NULL DEFAULT '',\n  `rewrite_prefix` varchar(64) NOT NULL DEFAULT '',\n  `rewrite_suffix` varchar(64) NOT NULL DEFAULT '',\n  `description` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `carrierroute`\n--\n\nLOCK TABLES `carrierroute` WRITE;\n/*!40000 ALTER TABLE `carrierroute` DISABLE KEYS */;\n/*!40000 ALTER TABLE `carrierroute` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `cdrs`\n--\n\nDROP TABLE IF EXISTS `cdrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `cdrs` (\n  `cdr_id` bigint(20) NOT NULL AUTO_INCREMENT,\n  `src_username` varchar(64) NOT NULL DEFAULT '',\n  `src_domain` varchar(128) NOT NULL DEFAULT '',\n  `dst_username` varchar(64) NOT NULL DEFAULT '',\n  `dst_domain` varchar(128) NOT NULL DEFAULT '',\n  `dst_ousername` varchar(64) NOT NULL DEFAULT '',\n  `call_start_time` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',\n  `duration` int(10) unsigned NOT NULL DEFAULT '0',\n  `sip_call_id` varchar(128) NOT NULL DEFAULT '',\n  `sip_from_tag` varchar(128) NOT NULL DEFAULT '',\n  `sip_to_tag` varchar(128) NOT NULL DEFAULT '',\n  `src_ip` varchar(64) NOT NULL DEFAULT '',\n  `cost` int(11) NOT NULL DEFAULT '0',\n  `rated` int(11) NOT NULL DEFAULT '0',\n  `created` datetime NOT NULL,\n  `calltype` varchar(20) DEFAULT NULL,\n  `fraud` tinyint(1) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`cdr_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `cdrs`\n--\n\nLOCK TABLES `cdrs` WRITE;\n/*!40000 ALTER TABLE `cdrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `cdrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `cpl`\n--\n\nDROP TABLE IF EXISTS `cpl`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `cpl` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `cpl_xml` text,\n  `cpl_bin` text,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `account_idx` (`username`,`domain`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `cpl`\n--\n\nLOCK TABLES `cpl` WRITE;\n/*!40000 ALTER TABLE `cpl` DISABLE KEYS */;\n/*!40000 ALTER TABLE `cpl` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dbaliases`\n--\n\nDROP TABLE IF EXISTS `dbaliases`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dbaliases` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `alias_username` varchar(64) NOT NULL DEFAULT '',\n  `alias_domain` varchar(64) NOT NULL DEFAULT '',\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  KEY `alias_user_idx` (`alias_username`),\n  KEY `alias_idx` (`alias_username`,`alias_domain`),\n  KEY `target_idx` (`username`,`domain`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dbaliases`\n--\n\nLOCK TABLES `dbaliases` WRITE;\n/*!40000 ALTER TABLE `dbaliases` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dbaliases` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dialog`\n--\n\nDROP TABLE IF EXISTS `dialog`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dialog` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `hash_entry` int(10) unsigned NOT NULL,\n  `hash_id` int(10) unsigned NOT NULL,\n  `callid` varchar(255) NOT NULL,\n  `from_uri` varchar(128) NOT NULL,\n  `from_tag` varchar(64) NOT NULL,\n  `to_uri` varchar(128) NOT NULL,\n  `to_tag` varchar(64) NOT NULL,\n  `caller_cseq` varchar(20) NOT NULL,\n  `callee_cseq` varchar(20) NOT NULL,\n  `caller_route_set` varchar(512) DEFAULT NULL,\n  `callee_route_set` varchar(512) DEFAULT NULL,\n  `caller_contact` varchar(128) NOT NULL,\n  `callee_contact` varchar(128) NOT NULL,\n  `caller_sock` varchar(64) NOT NULL,\n  `callee_sock` varchar(64) NOT NULL,\n  `state` int(10) unsigned NOT NULL,\n  `start_time` int(10) unsigned NOT NULL,\n  `timeout` int(10) unsigned NOT NULL DEFAULT '0',\n  `sflags` int(10) unsigned NOT NULL DEFAULT '0',\n  `iflags` int(10) unsigned NOT NULL DEFAULT '0',\n  `toroute_name` varchar(32) DEFAULT NULL,\n  `req_uri` varchar(128) NOT NULL,\n  `xdata` varchar(512) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `hash_idx` (`hash_entry`,`hash_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dialog`\n--\n\nLOCK TABLES `dialog` WRITE;\n/*!40000 ALTER TABLE `dialog` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dialog` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dialog_vars`\n--\n\nDROP TABLE IF EXISTS `dialog_vars`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dialog_vars` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `hash_entry` int(10) unsigned NOT NULL,\n  `hash_id` int(10) unsigned NOT NULL,\n  `dialog_key` varchar(128) NOT NULL,\n  `dialog_value` varchar(512) NOT NULL,\n  PRIMARY KEY (`id`),\n  KEY `hash_idx` (`hash_entry`,`hash_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dialog_vars`\n--\n\nLOCK TABLES `dialog_vars` WRITE;\n/*!40000 ALTER TABLE `dialog_vars` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dialog_vars` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dialplan`\n--\n\nDROP TABLE IF EXISTS `dialplan`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dialplan` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `dpid` int(11) NOT NULL,\n  `pr` int(11) NOT NULL,\n  `match_op` int(11) NOT NULL,\n  `match_exp` varchar(64) NOT NULL,\n  `match_len` int(11) NOT NULL,\n  `subst_exp` varchar(64) NOT NULL,\n  `repl_exp` varchar(256) NOT NULL,\n  `attrs` varchar(64) NOT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dialplan`\n--\n\nLOCK TABLES `dialplan` WRITE;\n/*!40000 ALTER TABLE `dialplan` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dialplan` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dispatcher`\n--\n\nDROP TABLE IF EXISTS `dispatcher`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dispatcher` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `setid` int(11) NOT NULL DEFAULT '0',\n  `destination` varchar(192) NOT NULL DEFAULT '',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `priority` int(11) NOT NULL DEFAULT '0',\n  `attrs` varchar(128) NOT NULL DEFAULT '',\n  `description` varchar(64) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dispatcher`\n--\n\nLOCK TABLES `dispatcher` WRITE;\n/*!40000 ALTER TABLE `dispatcher` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dispatcher` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `domain`\n--\n\nDROP TABLE IF EXISTS `domain`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `domain` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `domain` varchar(64) NOT NULL,\n  `did` varchar(64) DEFAULT NULL,\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `domain_idx` (`domain`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `domain`\n--\n\nLOCK TABLES `domain` WRITE;\n/*!40000 ALTER TABLE `domain` DISABLE KEYS */;\n/*!40000 ALTER TABLE `domain` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `domain_attrs`\n--\n\nDROP TABLE IF EXISTS `domain_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `domain_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `did` varchar(64) NOT NULL,\n  `name` varchar(32) NOT NULL,\n  `type` int(10) unsigned NOT NULL,\n  `value` varchar(255) NOT NULL,\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  KEY `domain_attrs_idx` (`did`,`name`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `domain_attrs`\n--\n\nLOCK TABLES `domain_attrs` WRITE;\n/*!40000 ALTER TABLE `domain_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `domain_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `domain_name`\n--\n\nDROP TABLE IF EXISTS `domain_name`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `domain_name` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `domain` varchar(64) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `domain_name`\n--\n\nLOCK TABLES `domain_name` WRITE;\n/*!40000 ALTER TABLE `domain_name` DISABLE KEYS */;\n/*!40000 ALTER TABLE `domain_name` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `domainpolicy`\n--\n\nDROP TABLE IF EXISTS `domainpolicy`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `domainpolicy` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `rule` varchar(255) NOT NULL,\n  `type` varchar(255) NOT NULL,\n  `att` varchar(255) DEFAULT NULL,\n  `val` varchar(128) DEFAULT NULL,\n  `description` varchar(255) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `rav_idx` (`rule`,`att`,`val`),\n  KEY `rule_idx` (`rule`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `domainpolicy`\n--\n\nLOCK TABLES `domainpolicy` WRITE;\n/*!40000 ALTER TABLE `domainpolicy` DISABLE KEYS */;\n/*!40000 ALTER TABLE `domainpolicy` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dr_custom_rules`\n--\n\nDROP TABLE IF EXISTS `dr_custom_rules`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dr_custom_rules` (\n  `dr_ruleid` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `locality` varchar(64) NOT NULL DEFAULT '',\n  `ppm` decimal(10,2) NOT NULL DEFAULT '0.00',\n  `description` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`dr_ruleid`),\n  CONSTRAINT `dr_custom_rules_ibfk_1` FOREIGN KEY (`dr_ruleid`) REFERENCES `dr_rules` (`ruleid`) ON DELETE CASCADE ON UPDATE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dr_custom_rules`\n--\n\nLOCK TABLES `dr_custom_rules` WRITE;\n/*!40000 ALTER TABLE `dr_custom_rules` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dr_custom_rules` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dr_gateways`\n--\n\nDROP TABLE IF EXISTS `dr_gateways`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dr_gateways` (\n  `gwid` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `type` int(11) unsigned NOT NULL DEFAULT '0',\n  `address` varchar(128) NOT NULL,\n  `strip` int(11) unsigned NOT NULL DEFAULT '0',\n  `pri_prefix` varchar(64) DEFAULT NULL,\n  `attrs` varchar(255) DEFAULT NULL,\n  `description` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`gwid`)\n) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dr_gateways`\n--\n\nLOCK TABLES `dr_gateways` WRITE;\n/*!40000 ALTER TABLE `dr_gateways` DISABLE KEYS */;\nINSERT INTO `dr_gateways` VALUES (1,8,'52.41.52.34',0,'','','name:Skyetel North West Inbound,gwgroup:1'),(2,8,'52.8.201.128',0,'','','name:Skyetel South West Inbound,gwgroup:1'),(3,8,'52.60.138.31',0,'','','name:Skyetel North East Inbound,gwgroup:1'),(4,8,'50.17.48.216',0,'','','name:Skyetel South East Inbound,gwgroup:1'),(5,8,'35.156.192.164',0,'','','name:Skyetel Europe Inbound,gwgroup:1'),(6,8,'term.skyetel.com',0,'','','name:Skyetel 1st Priority Outbound Call,gwgroup:1'),(7,8,'52.41.52.34',0,'','','name:Skyetel 2nd Priority Outbound Call,gwgroup:1'),(8,8,'52.8.201.128',0,'','','name:Skyetel 3rd Priority Outbound Call,gwgroup:1'),(9,8,'50.17.48.216',0,'','','name:Skyetel 4rd Priority Outbound Call,gwgroup:1'),(10,8,'52.32.223.28',0,'','','name:Skyetel North West High Cost Outbound Traffic,gwgroup:1'),(11,8,'52.4.178.107',0,'','','name:Skyetel South East High Cost Outbound Traffic,gwgroup:1'),(12,8,'147.75.60.160',0,'','','name:Flowroute US-West-WA,gwgroup:2'),(13,8,'34.210.91.112',0,'','','name:Flowroute US-West-OR,gwgroup:2'),(14,8,'147.75.65.192',0,'','','name:Flowroute US-East-NJ,gwgroup:2'),(15,8,'34.226.36.32',0,'','','name:Flowroute US-East-VA,gwgroup:2'),(16,8,'81.201.82.45',0,'','','name:Voxbone Belgium,gwgroup:3'),(17,8,'81.201.84.195',0,'','','name:Voxbone LA,gwgroup:3'),(18,8,'81.201.85.45',0,'','','name:Voxbone NYC,gwgroup:3'),(19,8,'81.201.83.45',0,'','','name:Voxbone Germany,gwgroup:3'),(20,8,'81.201.86.45',0,'','','name:Voxbone Hong Kong,gwgroup:3'),(21,8,'81.201.84.195',0,'','','name:Voxbone Australia,gwgroup:3'),(22,8,'64.136.174.30',0,'','','name:VI Carrier,gwgroup:4'),(23,8,'64.136.173.22',0,'','','name:VI Carrier,gwgroup:4'),(24,8,'209.166.128.200',0,'','','name:VI Carrier,gwgroup:4'),(25,8,'192.240.151.100',0,'','','name:VI Carrier,gwgroup:4'),(26,8,'64.136.173.31',0,'','','name:VI Carrier,gwgroup:4'),(27,8,'64.136.174.30',0,'','','name:VI Carrier,gwgroup:4'),(28,8,'64.136.174.20',0,'','','name:VI Carrier,gwgroup:4'),(29,8,'209.166.154.70',0,'','','name:VI Carrier,gwgroup:4'),(30,8,'64.136.174.65',0,'','','name:VI Carrier,gwgroup:4'),(31,8,'64.136.173.23',0,'','','name:VI Carrier,gwgroup:4'),(32,8,'209.166.128.201',0,'','','name:VI Carrier,gwgroup:4'),(33,8,'192.240.151.101',0,'','','name:VI Carrier,gwgroup:4'),(34,8,'64.136.173.65',0,'','','name:VI Carrier,gwgroup:4'),(35,8,'64.136.174.65',0,'','','name:VI Carrier,gwgroup:4'),(36,8,'64.136.174.21',0,'','','name:VI Carrier,gwgroup:4'),(37,8,'209.166.154.71',0,'','','name:VI Carrier,gwgroup:4'),(38,8,'72.15.219.140',0,'','','name:Thinq Carrier,gwgroup:5'),(39,8,'216.147.191.157',0,'','','name:Voxtelesys Carrier,gwgroup:6'),(40,8,'64.34.181.47',0,'','','name:Les.net Carrier,gwgroup:7');\n/*!40000 ALTER TABLE `dr_gateways` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dr_groups`\n--\n\nDROP TABLE IF EXISTS `dr_groups`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dr_groups` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `domain` varchar(128) NOT NULL DEFAULT '',\n  `groupid` int(11) unsigned NOT NULL DEFAULT '0',\n  `description` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dr_groups`\n--\n\nLOCK TABLES `dr_groups` WRITE;\n/*!40000 ALTER TABLE `dr_groups` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dr_groups` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dr_gw_lists`\n--\n\nDROP TABLE IF EXISTS `dr_gw_lists`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dr_gw_lists` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `gwlist` varchar(255) NOT NULL,\n  `description` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dr_gw_lists`\n--\n\nLOCK TABLES `dr_gw_lists` WRITE;\n/*!40000 ALTER TABLE `dr_gw_lists` DISABLE KEYS */;\nINSERT INTO `dr_gw_lists` VALUES (1,'1,2,3,4,5,6,7,8,9,10,11','name:Skyetel CarrierGroup,type:8'),(2,'12,13,14,15','name:Flowroute CarrierGroup,type:8'),(3,'16,17,18,19,20,21','name:Voxbone CarrierGroup,type:8'),(4,'22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37','name:VI CarrierGroup,type:8'),(5,'38','name:Thinq CarrierGroup,type:8'),(6,'39','name:Voxtelesys CarrierGroup,type:8'),(7,'40','name:Les.net CarrierGroup,type:8');\n/*!40000 ALTER TABLE `dr_gw_lists` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dr_rules`\n--\n\nDROP TABLE IF EXISTS `dr_rules`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dr_rules` (\n  `ruleid` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `groupid` varchar(255) NOT NULL,\n  `prefix` varchar(64) NOT NULL,\n  `timerec` varchar(255) NOT NULL,\n  `priority` int(11) NOT NULL DEFAULT '0',\n  `routeid` varchar(64) NOT NULL,\n  `gwlist` varchar(255) NOT NULL,\n  `description` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`ruleid`)\n) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dr_rules`\n--\n\nLOCK TABLES `dr_rules` WRITE;\n/*!40000 ALTER TABLE `dr_rules` DISABLE KEYS */;\nINSERT INTO `dr_rules` VALUES (1,'8000','','',0,'','1,2','name:Default Outbound Route');\n/*!40000 ALTER TABLE `dr_rules` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_calllimit`\n--\n\nDROP TABLE IF EXISTS `dsip_calllimit`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_calllimit` (\n  `gwid` varchar(64) NOT NULL DEFAULT '',\n  `key_type` varchar(64) NOT NULL DEFAULT '0',\n  `limit` varchar(64) NOT NULL DEFAULT '',\n  `value_type` varchar(64) NOT NULL DEFAULT '0',\n  `status` tinyint(4) NOT NULL DEFAULT '1',\n  PRIMARY KEY (`gwid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_calllimit`\n--\n\nLOCK TABLES `dsip_calllimit` WRITE;\n/*!40000 ALTER TABLE `dsip_calllimit` DISABLE KEYS */;\nINSERT INTO `dsip_calllimit` VALUES ('64','0','9','1',1),('72','0','2','1',1),('73','0','2','1',1),('74','0','2','0',1);\n/*!40000 ALTER TABLE `dsip_calllimit` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_domain_mapping`\n--\n\nDROP TABLE IF EXISTS `dsip_domain_mapping`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_domain_mapping` (\n  `id` int(10) NOT NULL AUTO_INCREMENT,\n  `pbx_id` int(10) NOT NULL,\n  `domain_id` int(10) NOT NULL,\n  `attr_list` varchar(255) NOT NULL,\n  `type` tinyint(3) NOT NULL DEFAULT '0',\n  `enabled` tinyint(1) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_domain_mapping`\n--\n\nLOCK TABLES `dsip_domain_mapping` WRITE;\n/*!40000 ALTER TABLE `dsip_domain_mapping` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_domain_mapping` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_endpoint_lease`\n--\n\nDROP TABLE IF EXISTS `dsip_endpoint_lease`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_endpoint_lease` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `gwid` int(10) unsigned NOT NULL,\n  `sid` int(10) unsigned NOT NULL,\n  `expiration` datetime NOT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_endpoint_lease`\n--\n\nLOCK TABLES `dsip_endpoint_lease` WRITE;\n/*!40000 ALTER TABLE `dsip_endpoint_lease` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_endpoint_lease` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_lcr`\n--\n\nDROP TABLE IF EXISTS `dsip_lcr`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_lcr` (\n  `pattern` varchar(64) NOT NULL DEFAULT '',\n  `key_type` varchar(64) NOT NULL DEFAULT '0',\n  `dr_groupid` varchar(64) NOT NULL DEFAULT '',\n  `value_type` varchar(64) NOT NULL DEFAULT '0',\n  `cost` decimal(3,2) NOT NULL DEFAULT '0.00',\n  `from_prefix` varchar(64) NOT NULL DEFAULT '',\n  `expires` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`pattern`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_lcr`\n--\n\nLOCK TABLES `dsip_lcr` WRITE;\n/*!40000 ALTER TABLE `dsip_lcr` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_lcr` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_maintmode`\n--\n\nDROP TABLE IF EXISTS `dsip_maintmode`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_maintmode` (\n  `ipaddr` varchar(64) NOT NULL DEFAULT '',\n  `key_type` varchar(64) NOT NULL DEFAULT '0',\n  `gwid` varchar(64) NOT NULL DEFAULT '',\n  `value_type` varchar(64) NOT NULL DEFAULT '0',\n  `status` tinyint(4) NOT NULL DEFAULT '1',\n  PRIMARY KEY (`ipaddr`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_maintmode`\n--\n\nLOCK TABLES `dsip_maintmode` WRITE;\n/*!40000 ALTER TABLE `dsip_maintmode` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_maintmode` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_multidomain_mapping`\n--\n\nDROP TABLE IF EXISTS `dsip_multidomain_mapping`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_multidomain_mapping` (\n  `id` int(10) NOT NULL AUTO_INCREMENT,\n  `pbx_id` int(10) NOT NULL,\n  `db_host` varchar(20) NOT NULL,\n  `db_username` varchar(40) NOT NULL,\n  `db_password` varchar(40) NOT NULL,\n  `domain_list` varchar(255) NOT NULL DEFAULT '',\n  `attr_list` varchar(255) NOT NULL DEFAULT '',\n  `type` tinyint(3) NOT NULL DEFAULT '0',\n  `enabled` tinyint(1) NOT NULL DEFAULT '0',\n  `lastsync` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n  `syncstatus` tinyint(1) NOT NULL DEFAULT '0',\n  `syncerror` varchar(200) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_multidomain_mapping`\n--\n\nLOCK TABLES `dsip_multidomain_mapping` WRITE;\n/*!40000 ALTER TABLE `dsip_multidomain_mapping` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_multidomain_mapping` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_notification`\n--\n\nDROP TABLE IF EXISTS `dsip_notification`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_notification` (\n  `gwgroupid` int(11) NOT NULL,\n  `type` int(11) NOT NULL,\n  `method` int(11) DEFAULT NULL,\n  `value` varchar(255) DEFAULT NULL,\n  `createdate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n  PRIMARY KEY (`gwgroupid`,`type`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_notification`\n--\n\nLOCK TABLES `dsip_notification` WRITE;\n/*!40000 ALTER TABLE `dsip_notification` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_notification` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `globalblacklist`\n--\n\nDROP TABLE IF EXISTS `globalblacklist`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `globalblacklist` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `prefix` varchar(64) NOT NULL DEFAULT '',\n  `whitelist` tinyint(1) NOT NULL DEFAULT '0',\n  `description` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `globalblacklist_idx` (`prefix`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `globalblacklist`\n--\n\nLOCK TABLES `globalblacklist` WRITE;\n/*!40000 ALTER TABLE `globalblacklist` DISABLE KEYS */;\n/*!40000 ALTER TABLE `globalblacklist` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `grp`\n--\n\nDROP TABLE IF EXISTS `grp`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `grp` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `grp` varchar(64) NOT NULL DEFAULT '',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `account_group_idx` (`username`,`domain`,`grp`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `grp`\n--\n\nLOCK TABLES `grp` WRITE;\n/*!40000 ALTER TABLE `grp` DISABLE KEYS */;\n/*!40000 ALTER TABLE `grp` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `htable`\n--\n\nDROP TABLE IF EXISTS `htable`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `htable` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `key_name` varchar(64) NOT NULL DEFAULT '',\n  `key_type` int(11) NOT NULL DEFAULT '0',\n  `value_type` int(11) NOT NULL DEFAULT '0',\n  `key_value` varchar(128) NOT NULL DEFAULT '',\n  `expires` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `htable`\n--\n\nLOCK TABLES `htable` WRITE;\n/*!40000 ALTER TABLE `htable` DISABLE KEYS */;\n/*!40000 ALTER TABLE `htable` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `imc_members`\n--\n\nDROP TABLE IF EXISTS `imc_members`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `imc_members` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL,\n  `room` varchar(64) NOT NULL,\n  `flag` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `account_room_idx` (`username`,`domain`,`room`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `imc_members`\n--\n\nLOCK TABLES `imc_members` WRITE;\n/*!40000 ALTER TABLE `imc_members` DISABLE KEYS */;\n/*!40000 ALTER TABLE `imc_members` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `imc_rooms`\n--\n\nDROP TABLE IF EXISTS `imc_rooms`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `imc_rooms` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `name` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL,\n  `flag` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `name_domain_idx` (`name`,`domain`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `imc_rooms`\n--\n\nLOCK TABLES `imc_rooms` WRITE;\n/*!40000 ALTER TABLE `imc_rooms` DISABLE KEYS */;\n/*!40000 ALTER TABLE `imc_rooms` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `lcr_gw`\n--\n\nDROP TABLE IF EXISTS `lcr_gw`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `lcr_gw` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `lcr_id` smallint(5) unsigned NOT NULL,\n  `gw_name` varchar(128) DEFAULT NULL,\n  `ip_addr` varchar(50) DEFAULT NULL,\n  `hostname` varchar(64) DEFAULT NULL,\n  `port` smallint(5) unsigned DEFAULT NULL,\n  `params` varchar(64) DEFAULT NULL,\n  `uri_scheme` tinyint(3) unsigned DEFAULT NULL,\n  `transport` tinyint(3) unsigned DEFAULT NULL,\n  `strip` tinyint(3) unsigned DEFAULT NULL,\n  `prefix` varchar(16) DEFAULT NULL,\n  `tag` varchar(64) DEFAULT NULL,\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  `defunct` int(10) unsigned DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `lcr_id_idx` (`lcr_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `lcr_gw`\n--\n\nLOCK TABLES `lcr_gw` WRITE;\n/*!40000 ALTER TABLE `lcr_gw` DISABLE KEYS */;\n/*!40000 ALTER TABLE `lcr_gw` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `lcr_rule`\n--\n\nDROP TABLE IF EXISTS `lcr_rule`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `lcr_rule` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `lcr_id` smallint(5) unsigned NOT NULL,\n  `prefix` varchar(16) DEFAULT NULL,\n  `from_uri` varchar(64) DEFAULT NULL,\n  `request_uri` varchar(64) DEFAULT NULL,\n  `mt_tvalue` varchar(128) DEFAULT NULL,\n  `stopper` int(10) unsigned NOT NULL DEFAULT '0',\n  `enabled` int(10) unsigned NOT NULL DEFAULT '1',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `lcr_id_prefix_from_uri_idx` (`lcr_id`,`prefix`,`from_uri`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `lcr_rule`\n--\n\nLOCK TABLES `lcr_rule` WRITE;\n/*!40000 ALTER TABLE `lcr_rule` DISABLE KEYS */;\n/*!40000 ALTER TABLE `lcr_rule` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `lcr_rule_target`\n--\n\nDROP TABLE IF EXISTS `lcr_rule_target`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `lcr_rule_target` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `lcr_id` smallint(5) unsigned NOT NULL,\n  `rule_id` int(10) unsigned NOT NULL,\n  `gw_id` int(10) unsigned NOT NULL,\n  `priority` tinyint(3) unsigned NOT NULL,\n  `weight` int(10) unsigned NOT NULL DEFAULT '1',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `rule_id_gw_id_idx` (`rule_id`,`gw_id`),\n  KEY `lcr_id_idx` (`lcr_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `lcr_rule_target`\n--\n\nLOCK TABLES `lcr_rule_target` WRITE;\n/*!40000 ALTER TABLE `lcr_rule_target` DISABLE KEYS */;\n/*!40000 ALTER TABLE `lcr_rule_target` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `locale_lookup`\n--\n\nDROP TABLE IF EXISTS `locale_lookup`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `locale_lookup` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `locale` varchar(64) NOT NULL DEFAULT '',\n  `fprefix` varchar(64) NOT NULL DEFAULT '0',\n  `tprefix` varchar(64) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `locale_lookup`\n--\n\nLOCK TABLES `locale_lookup` WRITE;\n/*!40000 ALTER TABLE `locale_lookup` DISABLE KEYS */;\n/*!40000 ALTER TABLE `locale_lookup` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `location`\n--\n\nDROP TABLE IF EXISTS `location`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `location` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `ruid` varchar(64) NOT NULL DEFAULT '',\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) DEFAULT NULL,\n  `contact` varchar(512) NOT NULL DEFAULT '',\n  `received` varchar(128) DEFAULT NULL,\n  `path` varchar(512) DEFAULT NULL,\n  `expires` datetime NOT NULL DEFAULT '2030-05-28 21:32:15',\n  `q` float(10,2) NOT NULL DEFAULT '1.00',\n  `callid` varchar(255) NOT NULL DEFAULT 'Default-Call-ID',\n  `cseq` int(11) NOT NULL DEFAULT '1',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `cflags` int(11) NOT NULL DEFAULT '0',\n  `user_agent` varchar(255) NOT NULL DEFAULT '',\n  `socket` varchar(64) DEFAULT NULL,\n  `methods` int(11) DEFAULT NULL,\n  `instance` varchar(255) DEFAULT NULL,\n  `reg_id` int(11) NOT NULL DEFAULT '0',\n  `server_id` int(11) NOT NULL DEFAULT '0',\n  `connection_id` int(11) NOT NULL DEFAULT '0',\n  `keepalive` int(11) NOT NULL DEFAULT '0',\n  `partition` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `ruid_idx` (`ruid`),\n  KEY `account_contact_idx` (`username`,`domain`,`contact`),\n  KEY `expires_idx` (`expires`),\n  KEY `connection_idx` (`server_id`,`connection_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `location`\n--\n\nLOCK TABLES `location` WRITE;\n/*!40000 ALTER TABLE `location` DISABLE KEYS */;\n/*!40000 ALTER TABLE `location` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `location_attrs`\n--\n\nDROP TABLE IF EXISTS `location_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `location_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `ruid` varchar(64) NOT NULL DEFAULT '',\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) DEFAULT NULL,\n  `aname` varchar(64) NOT NULL DEFAULT '',\n  `atype` int(11) NOT NULL DEFAULT '0',\n  `avalue` varchar(255) NOT NULL DEFAULT '',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  KEY `account_record_idx` (`username`,`domain`,`ruid`),\n  KEY `last_modified_idx` (`last_modified`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `location_attrs`\n--\n\nLOCK TABLES `location_attrs` WRITE;\n/*!40000 ALTER TABLE `location_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `location_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `missed_calls`\n--\n\nDROP TABLE IF EXISTS `missed_calls`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `missed_calls` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `method` varchar(16) NOT NULL DEFAULT '',\n  `from_tag` varchar(64) NOT NULL DEFAULT '',\n  `to_tag` varchar(64) NOT NULL DEFAULT '',\n  `callid` varchar(255) NOT NULL DEFAULT '',\n  `sip_code` varchar(3) NOT NULL DEFAULT '',\n  `sip_reason` varchar(128) NOT NULL DEFAULT '',\n  `time` datetime NOT NULL,\n  PRIMARY KEY (`id`),\n  KEY `callid_idx` (`callid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `missed_calls`\n--\n\nLOCK TABLES `missed_calls` WRITE;\n/*!40000 ALTER TABLE `missed_calls` DISABLE KEYS */;\n/*!40000 ALTER TABLE `missed_calls` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `mohqcalls`\n--\n\nDROP TABLE IF EXISTS `mohqcalls`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `mohqcalls` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `mohq_id` int(10) unsigned NOT NULL,\n  `call_id` varchar(100) NOT NULL,\n  `call_status` int(10) unsigned NOT NULL,\n  `call_from` varchar(100) NOT NULL,\n  `call_contact` varchar(100) DEFAULT NULL,\n  `call_time` datetime NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `mohqcalls_idx` (`call_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `mohqcalls`\n--\n\nLOCK TABLES `mohqcalls` WRITE;\n/*!40000 ALTER TABLE `mohqcalls` DISABLE KEYS */;\n/*!40000 ALTER TABLE `mohqcalls` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `mohqueues`\n--\n\nDROP TABLE IF EXISTS `mohqueues`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `mohqueues` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `name` varchar(25) NOT NULL,\n  `uri` varchar(100) NOT NULL,\n  `mohdir` varchar(100) DEFAULT NULL,\n  `mohfile` varchar(100) NOT NULL,\n  `debug` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `mohqueue_uri_idx` (`uri`),\n  UNIQUE KEY `mohqueue_name_idx` (`name`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `mohqueues`\n--\n\nLOCK TABLES `mohqueues` WRITE;\n/*!40000 ALTER TABLE `mohqueues` DISABLE KEYS */;\n/*!40000 ALTER TABLE `mohqueues` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `mtree`\n--\n\nDROP TABLE IF EXISTS `mtree`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `mtree` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `tprefix` varchar(32) NOT NULL DEFAULT '',\n  `tvalue` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `tprefix_idx` (`tprefix`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `mtree`\n--\n\nLOCK TABLES `mtree` WRITE;\n/*!40000 ALTER TABLE `mtree` DISABLE KEYS */;\n/*!40000 ALTER TABLE `mtree` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `mtrees`\n--\n\nDROP TABLE IF EXISTS `mtrees`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `mtrees` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `tname` varchar(128) NOT NULL DEFAULT '',\n  `tprefix` varchar(32) NOT NULL DEFAULT '',\n  `tvalue` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `tname_tprefix_tvalue_idx` (`tname`,`tprefix`,`tvalue`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `mtrees`\n--\n\nLOCK TABLES `mtrees` WRITE;\n/*!40000 ALTER TABLE `mtrees` DISABLE KEYS */;\n/*!40000 ALTER TABLE `mtrees` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `pdt`\n--\n\nDROP TABLE IF EXISTS `pdt`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `pdt` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `sdomain` varchar(128) NOT NULL,\n  `prefix` varchar(32) NOT NULL,\n  `domain` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `sdomain_prefix_idx` (`sdomain`,`prefix`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `pdt`\n--\n\nLOCK TABLES `pdt` WRITE;\n/*!40000 ALTER TABLE `pdt` DISABLE KEYS */;\n/*!40000 ALTER TABLE `pdt` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `pl_pipes`\n--\n\nDROP TABLE IF EXISTS `pl_pipes`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `pl_pipes` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `pipeid` varchar(64) NOT NULL DEFAULT '',\n  `algorithm` varchar(32) NOT NULL DEFAULT '',\n  `plimit` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `pl_pipes`\n--\n\nLOCK TABLES `pl_pipes` WRITE;\n/*!40000 ALTER TABLE `pl_pipes` DISABLE KEYS */;\n/*!40000 ALTER TABLE `pl_pipes` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `presentity`\n--\n\nDROP TABLE IF EXISTS `presentity`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `presentity` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL,\n  `event` varchar(64) NOT NULL,\n  `etag` varchar(64) NOT NULL,\n  `expires` int(11) NOT NULL,\n  `received_time` int(11) NOT NULL,\n  `body` blob NOT NULL,\n  `sender` varchar(128) NOT NULL,\n  `priority` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `presentity_idx` (`username`,`domain`,`event`,`etag`),\n  KEY `presentity_expires` (`expires`),\n  KEY `account_idx` (`username`,`domain`,`event`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `presentity`\n--\n\nLOCK TABLES `presentity` WRITE;\n/*!40000 ALTER TABLE `presentity` DISABLE KEYS */;\n/*!40000 ALTER TABLE `presentity` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `pua`\n--\n\nDROP TABLE IF EXISTS `pua`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `pua` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `pres_uri` varchar(128) NOT NULL,\n  `pres_id` varchar(255) NOT NULL,\n  `event` int(11) NOT NULL,\n  `expires` int(11) NOT NULL,\n  `desired_expires` int(11) NOT NULL,\n  `flag` int(11) NOT NULL,\n  `etag` varchar(64) NOT NULL,\n  `tuple_id` varchar(64) DEFAULT NULL,\n  `watcher_uri` varchar(128) NOT NULL,\n  `call_id` varchar(255) NOT NULL,\n  `to_tag` varchar(64) NOT NULL,\n  `from_tag` varchar(64) NOT NULL,\n  `cseq` int(11) NOT NULL,\n  `record_route` text,\n  `contact` varchar(128) NOT NULL,\n  `remote_contact` varchar(128) NOT NULL,\n  `version` int(11) NOT NULL,\n  `extra_headers` text NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `pua_idx` (`etag`,`tuple_id`,`call_id`,`from_tag`),\n  KEY `expires_idx` (`expires`),\n  KEY `dialog1_idx` (`pres_id`,`pres_uri`),\n  KEY `dialog2_idx` (`call_id`,`from_tag`),\n  KEY `record_idx` (`pres_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `pua`\n--\n\nLOCK TABLES `pua` WRITE;\n/*!40000 ALTER TABLE `pua` DISABLE KEYS */;\n/*!40000 ALTER TABLE `pua` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `purplemap`\n--\n\nDROP TABLE IF EXISTS `purplemap`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `purplemap` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `sip_user` varchar(128) NOT NULL,\n  `ext_user` varchar(128) NOT NULL,\n  `ext_prot` varchar(16) NOT NULL,\n  `ext_pass` varchar(64) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `purplemap`\n--\n\nLOCK TABLES `purplemap` WRITE;\n/*!40000 ALTER TABLE `purplemap` DISABLE KEYS */;\n/*!40000 ALTER TABLE `purplemap` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `re_grp`\n--\n\nDROP TABLE IF EXISTS `re_grp`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `re_grp` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `reg_exp` varchar(128) NOT NULL DEFAULT '',\n  `group_id` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  KEY `group_idx` (`group_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `re_grp`\n--\n\nLOCK TABLES `re_grp` WRITE;\n/*!40000 ALTER TABLE `re_grp` DISABLE KEYS */;\n/*!40000 ALTER TABLE `re_grp` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `rls_presentity`\n--\n\nDROP TABLE IF EXISTS `rls_presentity`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `rls_presentity` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `rlsubs_did` varchar(255) NOT NULL,\n  `resource_uri` varchar(128) NOT NULL,\n  `content_type` varchar(255) NOT NULL,\n  `presence_state` blob NOT NULL,\n  `expires` int(11) NOT NULL,\n  `updated` int(11) NOT NULL,\n  `auth_state` int(11) NOT NULL,\n  `reason` varchar(64) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `rls_presentity_idx` (`rlsubs_did`,`resource_uri`),\n  KEY `rlsubs_idx` (`rlsubs_did`),\n  KEY `updated_idx` (`updated`),\n  KEY `expires_idx` (`expires`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `rls_presentity`\n--\n\nLOCK TABLES `rls_presentity` WRITE;\n/*!40000 ALTER TABLE `rls_presentity` DISABLE KEYS */;\n/*!40000 ALTER TABLE `rls_presentity` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `rls_watchers`\n--\n\nDROP TABLE IF EXISTS `rls_watchers`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `rls_watchers` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `presentity_uri` varchar(128) NOT NULL,\n  `to_user` varchar(64) NOT NULL,\n  `to_domain` varchar(64) NOT NULL,\n  `watcher_username` varchar(64) NOT NULL,\n  `watcher_domain` varchar(64) NOT NULL,\n  `event` varchar(64) NOT NULL DEFAULT 'presence',\n  `event_id` varchar(64) DEFAULT NULL,\n  `to_tag` varchar(64) NOT NULL,\n  `from_tag` varchar(64) NOT NULL,\n  `callid` varchar(255) NOT NULL,\n  `local_cseq` int(11) NOT NULL,\n  `remote_cseq` int(11) NOT NULL,\n  `contact` varchar(128) NOT NULL,\n  `record_route` text,\n  `expires` int(11) NOT NULL,\n  `status` int(11) NOT NULL DEFAULT '2',\n  `reason` varchar(64) NOT NULL,\n  `version` int(11) NOT NULL DEFAULT '0',\n  `socket_info` varchar(64) NOT NULL,\n  `local_contact` varchar(128) NOT NULL,\n  `from_user` varchar(64) NOT NULL,\n  `from_domain` varchar(64) NOT NULL,\n  `updated` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `rls_watcher_idx` (`callid`,`to_tag`,`from_tag`),\n  KEY `rls_watchers_update` (`watcher_username`,`watcher_domain`,`event`),\n  KEY `rls_watchers_expires` (`expires`),\n  KEY `updated_idx` (`updated`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `rls_watchers`\n--\n\nLOCK TABLES `rls_watchers` WRITE;\n/*!40000 ALTER TABLE `rls_watchers` DISABLE KEYS */;\n/*!40000 ALTER TABLE `rls_watchers` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `rtpengine`\n--\n\nDROP TABLE IF EXISTS `rtpengine`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `rtpengine` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `setid` int(10) unsigned NOT NULL DEFAULT '0',\n  `url` varchar(64) NOT NULL,\n  `weight` int(10) unsigned NOT NULL DEFAULT '1',\n  `disabled` int(1) NOT NULL DEFAULT '0',\n  `stamp` datetime NOT NULL DEFAULT '1900-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `rtpengine_nodes` (`setid`,`url`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `rtpengine`\n--\n\nLOCK TABLES `rtpengine` WRITE;\n/*!40000 ALTER TABLE `rtpengine` DISABLE KEYS */;\n/*!40000 ALTER TABLE `rtpengine` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `rtpproxy`\n--\n\nDROP TABLE IF EXISTS `rtpproxy`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `rtpproxy` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `setid` varchar(32) NOT NULL DEFAULT '0',\n  `url` varchar(64) NOT NULL DEFAULT '',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `weight` int(11) NOT NULL DEFAULT '1',\n  `description` varchar(64) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `rtpproxy`\n--\n\nLOCK TABLES `rtpproxy` WRITE;\n/*!40000 ALTER TABLE `rtpproxy` DISABLE KEYS */;\n/*!40000 ALTER TABLE `rtpproxy` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `sca_subscriptions`\n--\n\nDROP TABLE IF EXISTS `sca_subscriptions`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `sca_subscriptions` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `subscriber` varchar(255) NOT NULL,\n  `aor` varchar(255) NOT NULL,\n  `event` int(11) NOT NULL DEFAULT '0',\n  `expires` int(11) NOT NULL DEFAULT '0',\n  `state` int(11) NOT NULL DEFAULT '0',\n  `app_idx` int(11) NOT NULL DEFAULT '0',\n  `call_id` varchar(255) NOT NULL,\n  `from_tag` varchar(64) NOT NULL,\n  `to_tag` varchar(64) NOT NULL,\n  `record_route` text,\n  `notify_cseq` int(11) NOT NULL,\n  `subscribe_cseq` int(11) NOT NULL,\n  `server_id` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `sca_subscriptions_idx` (`subscriber`,`call_id`,`from_tag`,`to_tag`),\n  KEY `sca_expires_idx` (`server_id`,`expires`),\n  KEY `sca_subscribers_idx` (`subscriber`,`event`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `sca_subscriptions`\n--\n\nLOCK TABLES `sca_subscriptions` WRITE;\n/*!40000 ALTER TABLE `sca_subscriptions` DISABLE KEYS */;\n/*!40000 ALTER TABLE `sca_subscriptions` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `silo`\n--\n\nDROP TABLE IF EXISTS `silo`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `silo` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `src_addr` varchar(128) NOT NULL DEFAULT '',\n  `dst_addr` varchar(128) NOT NULL DEFAULT '',\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `inc_time` int(11) NOT NULL DEFAULT '0',\n  `exp_time` int(11) NOT NULL DEFAULT '0',\n  `snd_time` int(11) NOT NULL DEFAULT '0',\n  `ctype` varchar(32) NOT NULL DEFAULT 'text/plain',\n  `body` blob,\n  `extra_hdrs` text,\n  `callid` varchar(128) NOT NULL DEFAULT '',\n  `status` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  KEY `account_idx` (`username`,`domain`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `silo`\n--\n\nLOCK TABLES `silo` WRITE;\n/*!40000 ALTER TABLE `silo` DISABLE KEYS */;\n/*!40000 ALTER TABLE `silo` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `sip_trace`\n--\n\nDROP TABLE IF EXISTS `sip_trace`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `sip_trace` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `time_stamp` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  `time_us` int(10) unsigned NOT NULL DEFAULT '0',\n  `callid` varchar(255) NOT NULL DEFAULT '',\n  `traced_user` varchar(128) NOT NULL DEFAULT '',\n  `msg` mediumtext NOT NULL,\n  `method` varchar(50) NOT NULL DEFAULT '',\n  `status` varchar(128) NOT NULL DEFAULT '',\n  `fromip` varchar(50) NOT NULL DEFAULT '',\n  `toip` varchar(50) NOT NULL DEFAULT '',\n  `fromtag` varchar(64) NOT NULL DEFAULT '',\n  `totag` varchar(64) NOT NULL DEFAULT '',\n  `direction` varchar(4) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  KEY `traced_user_idx` (`traced_user`),\n  KEY `date_idx` (`time_stamp`),\n  KEY `fromip_idx` (`fromip`),\n  KEY `callid_idx` (`callid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `sip_trace`\n--\n\nLOCK TABLES `sip_trace` WRITE;\n/*!40000 ALTER TABLE `sip_trace` DISABLE KEYS */;\n/*!40000 ALTER TABLE `sip_trace` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `speed_dial`\n--\n\nDROP TABLE IF EXISTS `speed_dial`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `speed_dial` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `sd_username` varchar(64) NOT NULL DEFAULT '',\n  `sd_domain` varchar(64) NOT NULL DEFAULT '',\n  `new_uri` varchar(128) NOT NULL DEFAULT '',\n  `fname` varchar(64) NOT NULL DEFAULT '',\n  `lname` varchar(64) NOT NULL DEFAULT '',\n  `description` varchar(64) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `speed_dial_idx` (`username`,`domain`,`sd_domain`,`sd_username`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `speed_dial`\n--\n\nLOCK TABLES `speed_dial` WRITE;\n/*!40000 ALTER TABLE `speed_dial` DISABLE KEYS */;\n/*!40000 ALTER TABLE `speed_dial` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `subscriber`\n--\n\nDROP TABLE IF EXISTS `subscriber`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `subscriber` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `password` varchar(64) NOT NULL DEFAULT '',\n  `ha1` varchar(128) NOT NULL DEFAULT '',\n  `ha1b` varchar(128) NOT NULL DEFAULT '',\n  `email_address` varchar(128) DEFAULT NULL,\n  `rpid` varchar(128) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `account_idx` (`username`,`domain`),\n  KEY `username_idx` (`username`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `subscriber`\n--\n\nLOCK TABLES `subscriber` WRITE;\n/*!40000 ALTER TABLE `subscriber` DISABLE KEYS */;\n/*!40000 ALTER TABLE `subscriber` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `topos_d`\n--\n\nDROP TABLE IF EXISTS `topos_d`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `topos_d` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `rectime` datetime NOT NULL,\n  `s_method` varchar(64) NOT NULL DEFAULT '',\n  `s_cseq` varchar(64) NOT NULL DEFAULT '',\n  `a_callid` varchar(255) NOT NULL DEFAULT '',\n  `a_uuid` varchar(255) NOT NULL DEFAULT '',\n  `b_uuid` varchar(255) NOT NULL DEFAULT '',\n  `a_contact` varchar(128) NOT NULL DEFAULT '',\n  `b_contact` varchar(128) NOT NULL DEFAULT '',\n  `as_contact` varchar(128) NOT NULL DEFAULT '',\n  `bs_contact` varchar(128) NOT NULL DEFAULT '',\n  `a_tag` varchar(255) NOT NULL DEFAULT '',\n  `b_tag` varchar(255) NOT NULL DEFAULT '',\n  `a_rr` mediumtext,\n  `b_rr` mediumtext,\n  `s_rr` mediumtext,\n  `iflags` int(10) unsigned NOT NULL DEFAULT '0',\n  `a_uri` varchar(128) NOT NULL DEFAULT '',\n  `b_uri` varchar(128) NOT NULL DEFAULT '',\n  `r_uri` varchar(128) NOT NULL DEFAULT '',\n  `a_srcaddr` varchar(128) NOT NULL DEFAULT '',\n  `b_srcaddr` varchar(128) NOT NULL DEFAULT '',\n  `a_socket` varchar(128) NOT NULL DEFAULT '',\n  `b_socket` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  KEY `rectime_idx` (`rectime`),\n  KEY `a_callid_idx` (`a_callid`),\n  KEY `a_uuid_idx` (`a_uuid`),\n  KEY `b_uuid_idx` (`b_uuid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `topos_d`\n--\n\nLOCK TABLES `topos_d` WRITE;\n/*!40000 ALTER TABLE `topos_d` DISABLE KEYS */;\n/*!40000 ALTER TABLE `topos_d` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `topos_t`\n--\n\nDROP TABLE IF EXISTS `topos_t`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `topos_t` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `rectime` datetime NOT NULL,\n  `s_method` varchar(64) NOT NULL DEFAULT '',\n  `s_cseq` varchar(64) NOT NULL DEFAULT '',\n  `a_callid` varchar(255) NOT NULL DEFAULT '',\n  `a_uuid` varchar(255) NOT NULL DEFAULT '',\n  `b_uuid` varchar(255) NOT NULL DEFAULT '',\n  `direction` int(11) NOT NULL DEFAULT '0',\n  `x_via` mediumtext,\n  `x_vbranch` varchar(255) NOT NULL DEFAULT '',\n  `x_rr` mediumtext,\n  `y_rr` mediumtext,\n  `s_rr` mediumtext,\n  `x_uri` varchar(128) NOT NULL DEFAULT '',\n  `a_contact` varchar(128) NOT NULL DEFAULT '',\n  `b_contact` varchar(128) NOT NULL DEFAULT '',\n  `as_contact` varchar(128) NOT NULL DEFAULT '',\n  `bs_contact` varchar(128) NOT NULL DEFAULT '',\n  `x_tag` varchar(255) NOT NULL DEFAULT '',\n  `a_tag` varchar(255) NOT NULL DEFAULT '',\n  `b_tag` varchar(255) NOT NULL DEFAULT '',\n  `a_srcaddr` varchar(128) NOT NULL DEFAULT '',\n  `b_srcaddr` varchar(128) NOT NULL DEFAULT '',\n  `a_socket` varchar(128) NOT NULL DEFAULT '',\n  `b_socket` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  KEY `rectime_idx` (`rectime`),\n  KEY `a_callid_idx` (`a_callid`),\n  KEY `x_vbranch_idx` (`x_vbranch`),\n  KEY `a_uuid_idx` (`a_uuid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `topos_t`\n--\n\nLOCK TABLES `topos_t` WRITE;\n/*!40000 ALTER TABLE `topos_t` DISABLE KEYS */;\n/*!40000 ALTER TABLE `topos_t` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `trusted`\n--\n\nDROP TABLE IF EXISTS `trusted`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `trusted` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `src_ip` varchar(50) NOT NULL,\n  `proto` varchar(4) NOT NULL,\n  `from_pattern` varchar(64) DEFAULT NULL,\n  `ruri_pattern` varchar(64) DEFAULT NULL,\n  `tag` varchar(64) DEFAULT NULL,\n  `priority` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  KEY `peer_idx` (`src_ip`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `trusted`\n--\n\nLOCK TABLES `trusted` WRITE;\n/*!40000 ALTER TABLE `trusted` DISABLE KEYS */;\n/*!40000 ALTER TABLE `trusted` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uacreg`\n--\n\nDROP TABLE IF EXISTS `uacreg`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uacreg` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `l_uuid` varchar(64) NOT NULL DEFAULT '',\n  `l_username` varchar(64) NOT NULL DEFAULT '',\n  `l_domain` varchar(64) NOT NULL DEFAULT '',\n  `r_username` varchar(64) NOT NULL DEFAULT '',\n  `r_domain` varchar(64) NOT NULL DEFAULT '',\n  `realm` varchar(64) NOT NULL DEFAULT '',\n  `auth_username` varchar(64) NOT NULL DEFAULT '',\n  `auth_password` varchar(64) NOT NULL DEFAULT '',\n  `auth_ha1` varchar(128) NOT NULL DEFAULT '',\n  `auth_proxy` varchar(128) NOT NULL DEFAULT '',\n  `expires` int(11) NOT NULL DEFAULT '0',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `reg_delay` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `l_uuid_idx` (`l_uuid`)\n) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uacreg`\n--\n\nLOCK TABLES `uacreg` WRITE;\n/*!40000 ALTER TABLE `uacreg` DISABLE KEYS */;\nINSERT INTO `uacreg` VALUES (1,'1','1','67.205.143.99','','','','','','','',60,1,0),(2,'2','2','67.205.143.99','','','','','','','',60,1,0),(3,'3','3','67.205.143.99','','','','','','','',60,1,0),(4,'4','4','67.205.143.99','','','','','','','',60,1,0),(5,'5','5','67.205.143.99','','','','','','','',60,1,0),(6,'6','6','67.205.143.99','','','','','','','',60,1,0),(7,'7','7','67.205.143.99','','','','','','','',60,1,0);\n/*!40000 ALTER TABLE `uacreg` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_credentials`\n--\n\nDROP TABLE IF EXISTS `uid_credentials`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_credentials` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `auth_username` varchar(64) NOT NULL,\n  `did` varchar(64) NOT NULL DEFAULT '_default',\n  `realm` varchar(64) NOT NULL,\n  `password` varchar(28) NOT NULL DEFAULT '',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `ha1` varchar(32) NOT NULL,\n  `ha1b` varchar(32) NOT NULL DEFAULT '',\n  `uid` varchar(64) NOT NULL,\n  PRIMARY KEY (`id`),\n  KEY `cred_idx` (`auth_username`,`did`),\n  KEY `uid` (`uid`),\n  KEY `did_idx` (`did`),\n  KEY `realm_idx` (`realm`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_credentials`\n--\n\nLOCK TABLES `uid_credentials` WRITE;\n/*!40000 ALTER TABLE `uid_credentials` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_credentials` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_domain`\n--\n\nDROP TABLE IF EXISTS `uid_domain`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_domain` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `did` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL,\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `domain_idx` (`domain`),\n  KEY `did_idx` (`did`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_domain`\n--\n\nLOCK TABLES `uid_domain` WRITE;\n/*!40000 ALTER TABLE `uid_domain` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_domain` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_domain_attrs`\n--\n\nDROP TABLE IF EXISTS `uid_domain_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_domain_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `did` varchar(64) DEFAULT NULL,\n  `name` varchar(32) NOT NULL,\n  `type` int(11) NOT NULL DEFAULT '0',\n  `value` varchar(128) DEFAULT NULL,\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `domain_attr_idx` (`did`,`name`,`value`),\n  KEY `domain_did` (`did`,`flags`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_domain_attrs`\n--\n\nLOCK TABLES `uid_domain_attrs` WRITE;\n/*!40000 ALTER TABLE `uid_domain_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_domain_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_global_attrs`\n--\n\nDROP TABLE IF EXISTS `uid_global_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_global_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `name` varchar(32) NOT NULL,\n  `type` int(11) NOT NULL DEFAULT '0',\n  `value` varchar(128) DEFAULT NULL,\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `global_attrs_idx` (`name`,`value`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_global_attrs`\n--\n\nLOCK TABLES `uid_global_attrs` WRITE;\n/*!40000 ALTER TABLE `uid_global_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_global_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_uri`\n--\n\nDROP TABLE IF EXISTS `uid_uri`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_uri` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `uid` varchar(64) NOT NULL,\n  `did` varchar(64) NOT NULL,\n  `username` varchar(64) NOT NULL,\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  `scheme` varchar(8) NOT NULL DEFAULT 'sip',\n  PRIMARY KEY (`id`),\n  KEY `uri_idx1` (`username`,`did`,`scheme`),\n  KEY `uri_uid` (`uid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_uri`\n--\n\nLOCK TABLES `uid_uri` WRITE;\n/*!40000 ALTER TABLE `uid_uri` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_uri` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_uri_attrs`\n--\n\nDROP TABLE IF EXISTS `uid_uri_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_uri_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `did` varchar(64) NOT NULL,\n  `name` varchar(32) NOT NULL,\n  `value` varchar(128) DEFAULT NULL,\n  `type` int(11) NOT NULL DEFAULT '0',\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  `scheme` varchar(8) NOT NULL DEFAULT 'sip',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `uriattrs_idx` (`username`,`did`,`name`,`value`,`scheme`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_uri_attrs`\n--\n\nLOCK TABLES `uid_uri_attrs` WRITE;\n/*!40000 ALTER TABLE `uid_uri_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_uri_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_user_attrs`\n--\n\nDROP TABLE IF EXISTS `uid_user_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_user_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `uid` varchar(64) NOT NULL,\n  `name` varchar(32) NOT NULL,\n  `value` varchar(128) DEFAULT NULL,\n  `type` int(11) NOT NULL DEFAULT '0',\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `userattrs_idx` (`uid`,`name`,`value`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_user_attrs`\n--\n\nLOCK TABLES `uid_user_attrs` WRITE;\n/*!40000 ALTER TABLE `uid_user_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_user_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uri`\n--\n\nDROP TABLE IF EXISTS `uri`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uri` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `uri_user` varchar(64) NOT NULL DEFAULT '',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `account_idx` (`username`,`domain`,`uri_user`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uri`\n--\n\nLOCK TABLES `uri` WRITE;\n/*!40000 ALTER TABLE `uri` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uri` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `userblacklist`\n--\n\nDROP TABLE IF EXISTS `userblacklist`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `userblacklist` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `prefix` varchar(64) NOT NULL DEFAULT '',\n  `whitelist` tinyint(1) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  KEY `userblacklist_idx` (`username`,`domain`,`prefix`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `userblacklist`\n--\n\nLOCK TABLES `userblacklist` WRITE;\n/*!40000 ALTER TABLE `userblacklist` DISABLE KEYS */;\n/*!40000 ALTER TABLE `userblacklist` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `usr_preferences`\n--\n\nDROP TABLE IF EXISTS `usr_preferences`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `usr_preferences` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `uuid` varchar(64) NOT NULL DEFAULT '',\n  `username` varchar(128) NOT NULL DEFAULT '0',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `attribute` varchar(32) NOT NULL DEFAULT '',\n  `type` int(11) NOT NULL DEFAULT '0',\n  `value` varchar(128) NOT NULL DEFAULT '',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  KEY `ua_idx` (`uuid`,`attribute`),\n  KEY `uda_idx` (`username`,`domain`,`attribute`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `usr_preferences`\n--\n\nLOCK TABLES `usr_preferences` WRITE;\n/*!40000 ALTER TABLE `usr_preferences` DISABLE KEYS */;\n/*!40000 ALTER TABLE `usr_preferences` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `version`\n--\n\nDROP TABLE IF EXISTS `version`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `version` (\n  `table_name` varchar(32) NOT NULL,\n  `table_version` int(10) unsigned NOT NULL DEFAULT '0',\n  UNIQUE KEY `table_name_idx` (`table_name`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `version`\n--\n\nLOCK TABLES `version` WRITE;\n/*!40000 ALTER TABLE `version` DISABLE KEYS */;\nINSERT INTO `version` VALUES ('acc',5),('acc_cdrs',2),('active_watchers',12),('address',6),('aliases',8),('carrierfailureroute',2),('carrierroute',3),('carrier_name',1),('cpl',1),('dbaliases',1),('dialog',7),('dialog_vars',1),('dialplan',2),('dispatcher',4),('domain',2),('domainpolicy',2),('domain_attrs',1),('domain_name',1),('dr_gateways',3),('dr_groups',2),('dr_gw_lists',1),('dr_rules',3),('globalblacklist',1),('grp',2),('htable',2),('imc_members',1),('imc_rooms',1),('lcr_gw',3),('lcr_rule',3),('lcr_rule_target',1),('location',9),('location_attrs',1),('missed_calls',4),('mohqcalls',1),('mohqueues',1),('mtree',1),('mtrees',2),('pdt',1),('pl_pipes',1),('presentity',4),('pua',7),('purplemap',1),('re_grp',1),('rls_presentity',1),('rls_watchers',3),('rtpengine',1),('rtpproxy',1),('sca_subscriptions',2),('silo',8),('sip_trace',4),('speed_dial',2),('subscriber',7),('topos_d',1),('topos_t',1),('trusted',6),('uacreg',3),('uid_credentials',7),('uid_domain',2),('uid_domain_attrs',1),('uid_global_attrs',1),('uid_uri',3),('uid_uri_attrs',2),('uid_user_attrs',3),('uri',1),('userblacklist',1),('usr_preferences',2),('version',1),('watchers',3),('xcap',4);\n/*!40000 ALTER TABLE `version` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `watchers`\n--\n\nDROP TABLE IF EXISTS `watchers`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `watchers` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `presentity_uri` varchar(128) NOT NULL,\n  `watcher_username` varchar(64) NOT NULL,\n  `watcher_domain` varchar(64) NOT NULL,\n  `event` varchar(64) NOT NULL DEFAULT 'presence',\n  `status` int(11) NOT NULL,\n  `reason` varchar(64) DEFAULT NULL,\n  `inserted_time` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `watcher_idx` (`presentity_uri`,`watcher_username`,`watcher_domain`,`event`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `watchers`\n--\n\nLOCK TABLES `watchers` WRITE;\n/*!40000 ALTER TABLE `watchers` DISABLE KEYS */;\n/*!40000 ALTER TABLE `watchers` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `xcap`\n--\n\nDROP TABLE IF EXISTS `xcap`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `xcap` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL,\n  `doc` mediumblob NOT NULL,\n  `doc_type` int(11) NOT NULL,\n  `etag` varchar(64) NOT NULL,\n  `source` int(11) NOT NULL,\n  `doc_uri` varchar(255) NOT NULL,\n  `port` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `doc_uri_idx` (`doc_uri`),\n  KEY `account_doc_type_idx` (`username`,`domain`,`doc_type`),\n  KEY `account_doc_type_uri_idx` (`username`,`domain`,`doc_type`,`doc_uri`),\n  KEY `account_doc_uri_idx` (`username`,`domain`,`doc_uri`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `xcap`\n--\n\nLOCK TABLES `xcap` WRITE;\n/*!40000 ALTER TABLE `xcap` DISABLE KEYS */;\n/*!40000 ALTER TABLE `xcap` ENABLE KEYS */;\nUNLOCK TABLES;\n/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;\n\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n\n-- Dump completed on 2019-08-20 21:30:19\n"
  },
  {
    "path": "testing/sql/v0.523+ent/grants.sql",
    "content": "-- Grants dumped for users: kamailio,kamailioro\nGRANT USAGE ON *.* TO 'kamailio'@'%' IDENTIFIED BY PASSWORD '*2870144497941F902294EDABA260A0A4A15078E4';\nGRANT ALL PRIVILEGES ON `kamailio`.* TO 'kamailio'@'%';\nGRANT USAGE ON *.* TO 'kamailio'@'localhost' IDENTIFIED BY PASSWORD '*2870144497941F902294EDABA260A0A4A15078E4';\nGRANT ALL PRIVILEGES ON `kamailio`.* TO 'kamailio'@'localhost';\nGRANT USAGE ON *.* TO 'kamailioro'@'%' IDENTIFIED BY PASSWORD '*CF29822D11D49E1965F110105EACFBE9202F1A31';\nGRANT SELECT ON `kamailio`.* TO 'kamailioro'@'%';\nGRANT USAGE ON *.* TO 'kamailioro'@'localhost' IDENTIFIED BY PASSWORD '*CF29822D11D49E1965F110105EACFBE9202F1A31';\nGRANT SELECT ON `kamailio`.* TO 'kamailioro'@'localhost';\nFLUSH PRIVILEGES;\n"
  },
  {
    "path": "testing/sql/v0.523+ent/kamailio.sql",
    "content": "-- MySQL dump 10.16  Distrib 10.1.41-MariaDB, for debian-linux-gnu (x86_64)\n--\n-- Host: localhost    Database: kamailio\n-- ------------------------------------------------------\n-- Server version\t10.1.41-MariaDB-0+deb9u1\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!40101 SET NAMES utf8mb4 */;\n/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n/*!40103 SET TIME_ZONE='+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n--\n-- Table structure for table `acc`\n--\n\nDROP TABLE IF EXISTS `acc`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `acc` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `method` varchar(16) NOT NULL DEFAULT '',\n  `from_tag` varchar(64) NOT NULL DEFAULT '',\n  `to_tag` varchar(64) NOT NULL DEFAULT '',\n  `callid` varchar(128) NOT NULL DEFAULT '',\n  `sip_code` char(3) NOT NULL DEFAULT '',\n  `sip_reason` varchar(32) NOT NULL DEFAULT '',\n  `time` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',\n  `src_ip` varchar(64) NOT NULL DEFAULT '',\n  `dst_ouser` varchar(64) NOT NULL DEFAULT '',\n  `dst_user` varchar(64) NOT NULL DEFAULT '',\n  `dst_domain` varchar(128) NOT NULL DEFAULT '',\n  `src_user` varchar(64) NOT NULL DEFAULT '',\n  `src_domain` varchar(128) NOT NULL DEFAULT '',\n  `cdr_id` int(11) NOT NULL DEFAULT '0',\n  `calltype` varchar(20) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `acc_callid` (`callid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `acc`\n--\n\nLOCK TABLES `acc` WRITE;\n/*!40000 ALTER TABLE `acc` DISABLE KEYS */;\n/*!40000 ALTER TABLE `acc` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `acc_cdrs`\n--\n\nDROP TABLE IF EXISTS `acc_cdrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `acc_cdrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `start_time` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',\n  `end_time` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',\n  `duration` float(10,3) NOT NULL DEFAULT '0.000',\n  PRIMARY KEY (`id`),\n  KEY `start_time_idx` (`start_time`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `acc_cdrs`\n--\n\nLOCK TABLES `acc_cdrs` WRITE;\n/*!40000 ALTER TABLE `acc_cdrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `acc_cdrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `active_watchers`\n--\n\nDROP TABLE IF EXISTS `active_watchers`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `active_watchers` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `presentity_uri` varchar(128) NOT NULL,\n  `watcher_username` varchar(64) NOT NULL,\n  `watcher_domain` varchar(64) NOT NULL,\n  `to_user` varchar(64) NOT NULL,\n  `to_domain` varchar(64) NOT NULL,\n  `event` varchar(64) NOT NULL DEFAULT 'presence',\n  `event_id` varchar(64) DEFAULT NULL,\n  `to_tag` varchar(64) NOT NULL,\n  `from_tag` varchar(64) NOT NULL,\n  `callid` varchar(255) NOT NULL,\n  `local_cseq` int(11) NOT NULL,\n  `remote_cseq` int(11) NOT NULL,\n  `contact` varchar(128) NOT NULL,\n  `record_route` text,\n  `expires` int(11) NOT NULL,\n  `status` int(11) NOT NULL DEFAULT '2',\n  `reason` varchar(64) DEFAULT NULL,\n  `version` int(11) NOT NULL DEFAULT '0',\n  `socket_info` varchar(64) NOT NULL,\n  `local_contact` varchar(128) NOT NULL,\n  `from_user` varchar(64) NOT NULL,\n  `from_domain` varchar(64) NOT NULL,\n  `updated` int(11) NOT NULL,\n  `updated_winfo` int(11) NOT NULL,\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `user_agent` varchar(255) DEFAULT '',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `active_watchers_idx` (`callid`,`to_tag`,`from_tag`),\n  KEY `active_watchers_expires` (`expires`),\n  KEY `active_watchers_pres` (`presentity_uri`,`event`),\n  KEY `updated_idx` (`updated`),\n  KEY `updated_winfo_idx` (`updated_winfo`,`presentity_uri`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `active_watchers`\n--\n\nLOCK TABLES `active_watchers` WRITE;\n/*!40000 ALTER TABLE `active_watchers` DISABLE KEYS */;\n/*!40000 ALTER TABLE `active_watchers` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `address`\n--\n\nDROP TABLE IF EXISTS `address`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `address` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `grp` int(11) unsigned NOT NULL DEFAULT '1',\n  `ip_addr` varchar(50) NOT NULL,\n  `mask` int(11) NOT NULL DEFAULT '32',\n  `port` smallint(5) unsigned NOT NULL DEFAULT '0',\n  `tag` varchar(64) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=72 DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `address`\n--\n\nLOCK TABLES `address` WRITE;\n/*!40000 ALTER TABLE `address` DISABLE KEYS */;\nINSERT INTO `address` VALUES (1,8,'52.41.52.34',32,0,'name:Skyetel North West Inbound,gwgroup:1'),(2,8,'52.8.201.128',32,0,'name:Skyetel South West Inbound,gwgroup:1'),(3,8,'52.60.138.31',32,0,'name:Skyetel North East Inbound,gwgroup:1'),(4,8,'50.17.48.216',32,0,'name:Skyetel South East Inbound,gwgroup:1'),(5,8,'35.156.192.164',32,0,'name:Skyetel Europe Inbound,gwgroup:1'),(6,8,'term.skyetel.com',32,0,'name:Skyetel 1st Priority Outbound Call,gwgroup:1'),(7,8,'52.41.52.34',32,0,'name:Skyetel 2nd Priority Outbound Call,gwgroup:1'),(8,8,'52.8.201.128',32,0,'name:Skyetel 3rd Priority Outbound Call,gwgroup:1'),(9,8,'50.17.48.216',32,0,'name:Skyetel 4rd Priority Outbound Call,gwgroup:1'),(10,8,'52.32.223.28',32,0,'name:Skyetel North West High Cost Outbound Traffic,gwgroup:1'),(11,8,'52.4.178.107',32,0,'name:Skyetel South East High Cost Outbound Traffic,gwgroup:1'),(12,8,'147.75.60.160',32,0,'name:Flowroute US-West-WA,gwgroup:2'),(13,8,'34.210.91.112',32,0,'name:Flowroute US-West-OR,gwgroup:2'),(14,8,'147.75.65.192',32,0,'name:Flowroute US-East-NJ,gwgroup:2'),(15,8,'34.226.36.32',32,0,'name:Flowroute US-East-VA,gwgroup:2'),(16,8,'81.201.82.45',32,0,'name:Voxbone Belgium,gwgroup:3'),(17,8,'81.201.84.195',32,0,'name:Voxbone LA,gwgroup:3'),(18,8,'81.201.85.45',32,0,'name:Voxbone NYC,gwgroup:3'),(19,8,'81.201.83.45',32,0,'name:Voxbone Germany,gwgroup:3'),(20,8,'81.201.86.45',32,0,'name:Voxbone Hong Kong,gwgroup:3'),(21,8,'81.201.84.195',32,0,'name:Voxbone Australia,gwgroup:3'),(22,8,'64.136.174.30',32,0,'name:VI Carrier,gwgroup:4'),(23,8,'64.136.173.22',32,0,'name:VI Carrier,gwgroup:4'),(24,8,'209.166.128.200',32,0,'name:VI Carrier,gwgroup:4'),(25,8,'192.240.151.100',32,0,'name:VI Carrier,gwgroup:4'),(26,8,'64.136.173.31',32,0,'name:VI Carrier,gwgroup:4'),(27,8,'64.136.174.30',32,0,'name:VI Carrier,gwgroup:4'),(28,8,'64.136.174.20',32,0,'name:VI Carrier,gwgroup:4'),(29,8,'209.166.154.70',32,0,'name:VI Carrier,gwgroup:4'),(30,8,'64.136.174.65',32,0,'name:VI Carrier,gwgroup:4'),(31,8,'64.136.173.23',32,0,'name:VI Carrier,gwgroup:4'),(32,8,'209.166.128.201',32,0,'name:VI Carrier,gwgroup:4'),(33,8,'192.240.151.101',32,0,'name:VI Carrier,gwgroup:4'),(34,8,'64.136.173.65',32,0,'name:VI Carrier,gwgroup:4'),(35,8,'64.136.174.65',32,0,'name:VI Carrier,gwgroup:4'),(36,8,'64.136.174.21',32,0,'name:VI Carrier,gwgroup:4'),(37,8,'209.166.154.71',32,0,'name:VI Carrier,gwgroup:4'),(38,8,'72.15.219.140',32,0,'name:Thinq Carrier,gwgroup:5'),(39,8,'216.147.191.157',32,0,'name:Voxtelesys Carrier,gwgroup:6'),(40,8,'64.34.181.47',32,0,'name:Les.net Carrier,gwgroup:7'),(64,8,'10.10.10.89',32,0,'name:vpn conn,gwgroup:8'),(65,9,'127.0.0.2',32,0,'name:,gwgroup:9'),(66,9,'127.0.0.3',32,0,'name:,gwgroup:10'),(67,9,'127.0.0.4',32,0,'name:,gwgroup:11'),(68,9,'127.0.0.4',32,0,'name:,gwgroup:12'),(69,8,'50.253.243.17',32,0,'name:vpn ext,gwgroup:8'),(70,9,'127.0.0.6',32,0,'name:,gwgroup:13'),(71,9,'127.0.0.6',32,0,'name:,gwgroup:14');\n/*!40000 ALTER TABLE `address` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `aliases`\n--\n\nDROP TABLE IF EXISTS `aliases`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `aliases` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `ruid` varchar(64) NOT NULL DEFAULT '',\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) DEFAULT NULL,\n  `contact` varchar(255) NOT NULL DEFAULT '',\n  `received` varchar(128) DEFAULT NULL,\n  `path` varchar(512) DEFAULT NULL,\n  `expires` datetime NOT NULL DEFAULT '2030-05-28 21:32:15',\n  `q` float(10,2) NOT NULL DEFAULT '1.00',\n  `callid` varchar(255) NOT NULL DEFAULT 'Default-Call-ID',\n  `cseq` int(11) NOT NULL DEFAULT '1',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `cflags` int(11) NOT NULL DEFAULT '0',\n  `user_agent` varchar(255) NOT NULL DEFAULT '',\n  `socket` varchar(64) DEFAULT NULL,\n  `methods` int(11) DEFAULT NULL,\n  `instance` varchar(255) DEFAULT NULL,\n  `reg_id` int(11) NOT NULL DEFAULT '0',\n  `server_id` int(11) NOT NULL DEFAULT '0',\n  `connection_id` int(11) NOT NULL DEFAULT '0',\n  `keepalive` int(11) NOT NULL DEFAULT '0',\n  `partition` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `ruid_idx` (`ruid`),\n  KEY `account_contact_idx` (`username`,`domain`,`contact`),\n  KEY `expires_idx` (`expires`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `aliases`\n--\n\nLOCK TABLES `aliases` WRITE;\n/*!40000 ALTER TABLE `aliases` DISABLE KEYS */;\n/*!40000 ALTER TABLE `aliases` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `carrier_name`\n--\n\nDROP TABLE IF EXISTS `carrier_name`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `carrier_name` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `carrier` varchar(64) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `carrier_name`\n--\n\nLOCK TABLES `carrier_name` WRITE;\n/*!40000 ALTER TABLE `carrier_name` DISABLE KEYS */;\n/*!40000 ALTER TABLE `carrier_name` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `carrierfailureroute`\n--\n\nDROP TABLE IF EXISTS `carrierfailureroute`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `carrierfailureroute` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `carrier` int(10) unsigned NOT NULL DEFAULT '0',\n  `domain` int(10) unsigned NOT NULL DEFAULT '0',\n  `scan_prefix` varchar(64) NOT NULL DEFAULT '',\n  `host_name` varchar(128) NOT NULL DEFAULT '',\n  `reply_code` varchar(3) NOT NULL DEFAULT '',\n  `flags` int(11) unsigned NOT NULL DEFAULT '0',\n  `mask` int(11) unsigned NOT NULL DEFAULT '0',\n  `next_domain` int(10) unsigned NOT NULL DEFAULT '0',\n  `description` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `carrierfailureroute`\n--\n\nLOCK TABLES `carrierfailureroute` WRITE;\n/*!40000 ALTER TABLE `carrierfailureroute` DISABLE KEYS */;\n/*!40000 ALTER TABLE `carrierfailureroute` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `carrierroute`\n--\n\nDROP TABLE IF EXISTS `carrierroute`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `carrierroute` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `carrier` int(10) unsigned NOT NULL DEFAULT '0',\n  `domain` int(10) unsigned NOT NULL DEFAULT '0',\n  `scan_prefix` varchar(64) NOT NULL DEFAULT '',\n  `flags` int(11) unsigned NOT NULL DEFAULT '0',\n  `mask` int(11) unsigned NOT NULL DEFAULT '0',\n  `prob` float NOT NULL DEFAULT '0',\n  `strip` int(11) unsigned NOT NULL DEFAULT '0',\n  `rewrite_host` varchar(128) NOT NULL DEFAULT '',\n  `rewrite_prefix` varchar(64) NOT NULL DEFAULT '',\n  `rewrite_suffix` varchar(64) NOT NULL DEFAULT '',\n  `description` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `carrierroute`\n--\n\nLOCK TABLES `carrierroute` WRITE;\n/*!40000 ALTER TABLE `carrierroute` DISABLE KEYS */;\n/*!40000 ALTER TABLE `carrierroute` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `cdrs`\n--\n\nDROP TABLE IF EXISTS `cdrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `cdrs` (\n  `cdr_id` bigint(20) NOT NULL AUTO_INCREMENT,\n  `src_username` varchar(64) NOT NULL DEFAULT '',\n  `src_domain` varchar(128) NOT NULL DEFAULT '',\n  `dst_username` varchar(64) NOT NULL DEFAULT '',\n  `dst_domain` varchar(128) NOT NULL DEFAULT '',\n  `dst_ousername` varchar(64) NOT NULL DEFAULT '',\n  `call_start_time` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',\n  `duration` int(10) unsigned NOT NULL DEFAULT '0',\n  `sip_call_id` varchar(128) NOT NULL DEFAULT '',\n  `sip_from_tag` varchar(128) NOT NULL DEFAULT '',\n  `sip_to_tag` varchar(128) NOT NULL DEFAULT '',\n  `src_ip` varchar(64) NOT NULL DEFAULT '',\n  `cost` int(11) NOT NULL DEFAULT '0',\n  `rated` int(11) NOT NULL DEFAULT '0',\n  `created` datetime NOT NULL,\n  `calltype` varchar(20) DEFAULT NULL,\n  `fraud` tinyint(1) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`cdr_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `cdrs`\n--\n\nLOCK TABLES `cdrs` WRITE;\n/*!40000 ALTER TABLE `cdrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `cdrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `cpl`\n--\n\nDROP TABLE IF EXISTS `cpl`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `cpl` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `cpl_xml` text,\n  `cpl_bin` text,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `account_idx` (`username`,`domain`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `cpl`\n--\n\nLOCK TABLES `cpl` WRITE;\n/*!40000 ALTER TABLE `cpl` DISABLE KEYS */;\n/*!40000 ALTER TABLE `cpl` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dbaliases`\n--\n\nDROP TABLE IF EXISTS `dbaliases`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dbaliases` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `alias_username` varchar(64) NOT NULL DEFAULT '',\n  `alias_domain` varchar(64) NOT NULL DEFAULT '',\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  KEY `alias_user_idx` (`alias_username`),\n  KEY `alias_idx` (`alias_username`,`alias_domain`),\n  KEY `target_idx` (`username`,`domain`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dbaliases`\n--\n\nLOCK TABLES `dbaliases` WRITE;\n/*!40000 ALTER TABLE `dbaliases` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dbaliases` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dialog`\n--\n\nDROP TABLE IF EXISTS `dialog`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dialog` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `hash_entry` int(10) unsigned NOT NULL,\n  `hash_id` int(10) unsigned NOT NULL,\n  `callid` varchar(255) NOT NULL,\n  `from_uri` varchar(128) NOT NULL,\n  `from_tag` varchar(64) NOT NULL,\n  `to_uri` varchar(128) NOT NULL,\n  `to_tag` varchar(64) NOT NULL,\n  `caller_cseq` varchar(20) NOT NULL,\n  `callee_cseq` varchar(20) NOT NULL,\n  `caller_route_set` varchar(512) DEFAULT NULL,\n  `callee_route_set` varchar(512) DEFAULT NULL,\n  `caller_contact` varchar(128) NOT NULL,\n  `callee_contact` varchar(128) NOT NULL,\n  `caller_sock` varchar(64) NOT NULL,\n  `callee_sock` varchar(64) NOT NULL,\n  `state` int(10) unsigned NOT NULL,\n  `start_time` int(10) unsigned NOT NULL,\n  `timeout` int(10) unsigned NOT NULL DEFAULT '0',\n  `sflags` int(10) unsigned NOT NULL DEFAULT '0',\n  `iflags` int(10) unsigned NOT NULL DEFAULT '0',\n  `toroute_name` varchar(32) DEFAULT NULL,\n  `req_uri` varchar(128) NOT NULL,\n  `xdata` varchar(512) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `hash_idx` (`hash_entry`,`hash_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dialog`\n--\n\nLOCK TABLES `dialog` WRITE;\n/*!40000 ALTER TABLE `dialog` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dialog` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dialog_vars`\n--\n\nDROP TABLE IF EXISTS `dialog_vars`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dialog_vars` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `hash_entry` int(10) unsigned NOT NULL,\n  `hash_id` int(10) unsigned NOT NULL,\n  `dialog_key` varchar(128) NOT NULL,\n  `dialog_value` varchar(512) NOT NULL,\n  PRIMARY KEY (`id`),\n  KEY `hash_idx` (`hash_entry`,`hash_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dialog_vars`\n--\n\nLOCK TABLES `dialog_vars` WRITE;\n/*!40000 ALTER TABLE `dialog_vars` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dialog_vars` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dialplan`\n--\n\nDROP TABLE IF EXISTS `dialplan`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dialplan` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `dpid` int(11) NOT NULL,\n  `pr` int(11) NOT NULL,\n  `match_op` int(11) NOT NULL,\n  `match_exp` varchar(64) NOT NULL,\n  `match_len` int(11) NOT NULL,\n  `subst_exp` varchar(64) NOT NULL,\n  `repl_exp` varchar(256) NOT NULL,\n  `attrs` varchar(64) NOT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dialplan`\n--\n\nLOCK TABLES `dialplan` WRITE;\n/*!40000 ALTER TABLE `dialplan` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dialplan` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dispatcher`\n--\n\nDROP TABLE IF EXISTS `dispatcher`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dispatcher` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `setid` int(11) NOT NULL DEFAULT '0',\n  `destination` varchar(192) NOT NULL DEFAULT '',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `priority` int(11) NOT NULL DEFAULT '0',\n  `attrs` varchar(128) NOT NULL DEFAULT '',\n  `description` varchar(64) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dispatcher`\n--\n\nLOCK TABLES `dispatcher` WRITE;\n/*!40000 ALTER TABLE `dispatcher` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dispatcher` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `domain`\n--\n\nDROP TABLE IF EXISTS `domain`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `domain` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `domain` varchar(64) NOT NULL,\n  `did` varchar(64) DEFAULT NULL,\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `domain_idx` (`domain`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `domain`\n--\n\nLOCK TABLES `domain` WRITE;\n/*!40000 ALTER TABLE `domain` DISABLE KEYS */;\n/*!40000 ALTER TABLE `domain` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `domain_attrs`\n--\n\nDROP TABLE IF EXISTS `domain_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `domain_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `did` varchar(64) NOT NULL,\n  `name` varchar(32) NOT NULL,\n  `type` int(10) unsigned NOT NULL,\n  `value` varchar(255) NOT NULL,\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  KEY `domain_attrs_idx` (`did`,`name`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `domain_attrs`\n--\n\nLOCK TABLES `domain_attrs` WRITE;\n/*!40000 ALTER TABLE `domain_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `domain_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `domain_name`\n--\n\nDROP TABLE IF EXISTS `domain_name`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `domain_name` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `domain` varchar(64) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `domain_name`\n--\n\nLOCK TABLES `domain_name` WRITE;\n/*!40000 ALTER TABLE `domain_name` DISABLE KEYS */;\n/*!40000 ALTER TABLE `domain_name` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `domainpolicy`\n--\n\nDROP TABLE IF EXISTS `domainpolicy`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `domainpolicy` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `rule` varchar(255) NOT NULL,\n  `type` varchar(255) NOT NULL,\n  `att` varchar(255) DEFAULT NULL,\n  `val` varchar(128) DEFAULT NULL,\n  `description` varchar(255) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `rav_idx` (`rule`,`att`,`val`),\n  KEY `rule_idx` (`rule`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `domainpolicy`\n--\n\nLOCK TABLES `domainpolicy` WRITE;\n/*!40000 ALTER TABLE `domainpolicy` DISABLE KEYS */;\n/*!40000 ALTER TABLE `domainpolicy` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dr_custom_rules`\n--\n\nDROP TABLE IF EXISTS `dr_custom_rules`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dr_custom_rules` (\n  `dr_ruleid` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `locality` varchar(64) NOT NULL DEFAULT '',\n  `ppm` decimal(10,2) NOT NULL DEFAULT '0.00',\n  `description` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`dr_ruleid`),\n  CONSTRAINT `dr_custom_rules_ibfk_1` FOREIGN KEY (`dr_ruleid`) REFERENCES `dr_rules` (`ruleid`) ON DELETE CASCADE ON UPDATE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dr_custom_rules`\n--\n\nLOCK TABLES `dr_custom_rules` WRITE;\n/*!40000 ALTER TABLE `dr_custom_rules` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dr_custom_rules` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dr_gateways`\n--\n\nDROP TABLE IF EXISTS `dr_gateways`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dr_gateways` (\n  `gwid` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `type` int(11) unsigned NOT NULL DEFAULT '0',\n  `address` varchar(128) NOT NULL,\n  `strip` int(11) unsigned NOT NULL DEFAULT '0',\n  `pri_prefix` varchar(64) DEFAULT NULL,\n  `attrs` varchar(255) DEFAULT NULL,\n  `description` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`gwid`)\n) ENGINE=InnoDB AUTO_INCREMENT=73 DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dr_gateways`\n--\n\nLOCK TABLES `dr_gateways` WRITE;\n/*!40000 ALTER TABLE `dr_gateways` DISABLE KEYS */;\nINSERT INTO `dr_gateways` VALUES (1,8,'52.41.52.34',0,'','1,8','name:Skyetel North West Inbound,gwgroup:1'),(2,8,'52.8.201.128',0,'','2,8','name:Skyetel South West Inbound,gwgroup:1'),(3,8,'52.60.138.31',0,'','3,8','name:Skyetel North East Inbound,gwgroup:1'),(4,8,'50.17.48.216',0,'','4,8','name:Skyetel South East Inbound,gwgroup:1'),(5,8,'35.156.192.164',0,'','5,8','name:Skyetel Europe Inbound,gwgroup:1'),(6,8,'term.skyetel.com',0,'','6,8','name:Skyetel 1st Priority Outbound Call,gwgroup:1'),(7,8,'52.41.52.34',0,'','7,8','name:Skyetel 2nd Priority Outbound Call,gwgroup:1'),(8,8,'52.8.201.128',0,'','8,8','name:Skyetel 3rd Priority Outbound Call,gwgroup:1'),(9,8,'50.17.48.216',0,'','9,8','name:Skyetel 4rd Priority Outbound Call,gwgroup:1'),(10,8,'52.32.223.28',0,'','10,8','name:Skyetel North West High Cost Outbound Traffic,gwgroup:1'),(11,8,'52.4.178.107',0,'','11,8','name:Skyetel South East High Cost Outbound Traffic,gwgroup:1'),(12,8,'147.75.60.160',0,'','12,8','name:Flowroute US-West-WA,gwgroup:2'),(13,8,'34.210.91.112',0,'','13,8','name:Flowroute US-West-OR,gwgroup:2'),(14,8,'147.75.65.192',0,'','14,8','name:Flowroute US-East-NJ,gwgroup:2'),(15,8,'34.226.36.32',0,'','15,8','name:Flowroute US-East-VA,gwgroup:2'),(16,8,'81.201.82.45',0,'','16,8','name:Voxbone Belgium,gwgroup:3'),(17,8,'81.201.84.195',0,'','17,8','name:Voxbone LA,gwgroup:3'),(18,8,'81.201.85.45',0,'','18,8','name:Voxbone NYC,gwgroup:3'),(19,8,'81.201.83.45',0,'','19,8','name:Voxbone Germany,gwgroup:3'),(20,8,'81.201.86.45',0,'','20,8','name:Voxbone Hong Kong,gwgroup:3'),(21,8,'81.201.84.195',0,'','21,8','name:Voxbone Australia,gwgroup:3'),(22,8,'64.136.174.30',0,'','22,8','name:VI Carrier,gwgroup:4'),(23,8,'64.136.173.22',0,'','23,8','name:VI Carrier,gwgroup:4'),(24,8,'209.166.128.200',0,'','24,8','name:VI Carrier,gwgroup:4'),(25,8,'192.240.151.100',0,'','25,8','name:VI Carrier,gwgroup:4'),(26,8,'64.136.173.31',0,'','26,8','name:VI Carrier,gwgroup:4'),(27,8,'64.136.174.30',0,'','27,8','name:VI Carrier,gwgroup:4'),(28,8,'64.136.174.20',0,'','28,8','name:VI Carrier,gwgroup:4'),(29,8,'209.166.154.70',0,'','29,8','name:VI Carrier,gwgroup:4'),(30,8,'64.136.174.65',0,'','30,8','name:VI Carrier,gwgroup:4'),(31,8,'64.136.173.23',0,'','31,8','name:VI Carrier,gwgroup:4'),(32,8,'209.166.128.201',0,'','32,8','name:VI Carrier,gwgroup:4'),(33,8,'192.240.151.101',0,'','33,8','name:VI Carrier,gwgroup:4'),(34,8,'64.136.173.65',0,'','34,8','name:VI Carrier,gwgroup:4'),(35,8,'64.136.174.65',0,'','35,8','name:VI Carrier,gwgroup:4'),(36,8,'64.136.174.21',0,'','36,8','name:VI Carrier,gwgroup:4'),(37,8,'209.166.154.71',0,'','37,8','name:VI Carrier,gwgroup:4'),(38,8,'72.15.219.140',0,'','38,8','name:Thinq Carrier,gwgroup:5'),(39,8,'216.147.191.157',0,'','39,8','name:Voxtelesys Carrier,gwgroup:6'),(40,8,'64.34.181.47',0,'','40,8','name:Les.net Carrier,gwgroup:7'),(64,8,'10.10.10.89',0,'','64,8','name:vpn conn,gwgroup:8'),(65,9,'127.0.0.2',0,'','65,9','name:,gwgroup:9'),(66,9,'127.0.0.3',0,'','66,9','name:,gwgroup:10'),(67,9,'127.0.0.4',0,'','67,9','name:,gwgroup:11'),(68,9,'127.0.0.5',0,'','68,9','gwgroup:12,name:,type:9'),(69,8,'50.253.243.17',0,'','69,8','name:vpn ext,gwgroup:8');\n/*!40000 ALTER TABLE `dr_gateways` ENABLE KEYS */;\nUNLOCK TABLES;\n/*!50003 SET @saved_cs_client      = @@character_set_client */ ;\n/*!50003 SET @saved_cs_results     = @@character_set_results */ ;\n/*!50003 SET @saved_col_connection = @@collation_connection */ ;\n/*!50003 SET character_set_client  = utf8mb4 */ ;\n/*!50003 SET character_set_results = utf8mb4 */ ;\n/*!50003 SET collation_connection  = utf8mb4_general_ci */ ;\n/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;\n/*!50003 SET sql_mode              = 'NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;\nDELIMITER ;;\n/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`localhost`*/ /*!50003 TRIGGER insert_dr_gateways\n  BEFORE INSERT\n  ON dr_gateways\n  FOR EACH ROW\nBEGIN\n\n  DECLARE new_gwid int;\n  SET new_gwid := (\n    SELECT auto_increment\n    FROM information_schema.tables\n    WHERE table_name = 'dr_gateways' AND table_schema = DATABASE());\n\n  SET NEW.attrs = CONCAT(CAST(new_gwid AS char), ',', CAST(NEW.type AS char));\n\nEND */;;\nDELIMITER ;\n/*!50003 SET sql_mode              = @saved_sql_mode */ ;\n/*!50003 SET character_set_client  = @saved_cs_client */ ;\n/*!50003 SET character_set_results = @saved_cs_results */ ;\n/*!50003 SET collation_connection  = @saved_col_connection */ ;\n/*!50003 SET @saved_cs_client      = @@character_set_client */ ;\n/*!50003 SET @saved_cs_results     = @@character_set_results */ ;\n/*!50003 SET @saved_col_connection = @@collation_connection */ ;\n/*!50003 SET character_set_client  = utf8mb4 */ ;\n/*!50003 SET character_set_results = utf8mb4 */ ;\n/*!50003 SET collation_connection  = utf8mb4_general_ci */ ;\n/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;\n/*!50003 SET sql_mode              = 'NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;\nDELIMITER ;;\n/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`localhost`*/ /*!50003 TRIGGER update_dr_gateways\n  BEFORE UPDATE\n  ON dr_gateways\n  FOR EACH ROW\nBEGIN\n\n  IF NOT (NEW.gwid <=> OLD.gwid) THEN\n    SET NEW.attrs = CONCAT(CAST(NEW.gwid AS char), ',', CAST(NEW.type AS char));\n  END IF;\n\nEND */;;\nDELIMITER ;\n/*!50003 SET sql_mode              = @saved_sql_mode */ ;\n/*!50003 SET character_set_client  = @saved_cs_client */ ;\n/*!50003 SET character_set_results = @saved_cs_results */ ;\n/*!50003 SET collation_connection  = @saved_col_connection */ ;\n\n--\n-- Table structure for table `dr_groups`\n--\n\nDROP TABLE IF EXISTS `dr_groups`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dr_groups` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `domain` varchar(128) NOT NULL DEFAULT '',\n  `groupid` int(11) unsigned NOT NULL DEFAULT '0',\n  `description` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dr_groups`\n--\n\nLOCK TABLES `dr_groups` WRITE;\n/*!40000 ALTER TABLE `dr_groups` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dr_groups` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dr_gw_lists`\n--\n\nDROP TABLE IF EXISTS `dr_gw_lists`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dr_gw_lists` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `gwlist` varchar(255) NOT NULL,\n  `description` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dr_gw_lists`\n--\n\nLOCK TABLES `dr_gw_lists` WRITE;\n/*!40000 ALTER TABLE `dr_gw_lists` DISABLE KEYS */;\nINSERT INTO `dr_gw_lists` VALUES (1,'1,2,3,4,5,6,7,8,9,10,11','name:Skyetel CarrierGroup,type:8'),(2,'12,13,14,15','name:Flowroute CarrierGroup,type:8'),(3,'16,17,18,19,20,21','name:Voxbone CarrierGroup,type:8'),(4,'22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37','name:VI CarrierGroup,type:8'),(5,'38','name:Thinq CarrierGroup,type:8'),(6,'39','name:Voxtelesys CarrierGroup,type:8'),(7,'40','name:Les.net CarrierGroup,type:8'),(8,'64,69','name:tyler testing,type:8'),(9,'65','name:test no fwd,type:9'),(10,'66','name:test hard fwd,type:9'),(11,'67','name:test fail fwd,type:9'),(12,'68','name:test call limit,type:9');\n/*!40000 ALTER TABLE `dr_gw_lists` ENABLE KEYS */;\nUNLOCK TABLES;\n/*!50003 SET @saved_cs_client      = @@character_set_client */ ;\n/*!50003 SET @saved_cs_results     = @@character_set_results */ ;\n/*!50003 SET @saved_col_connection = @@collation_connection */ ;\n/*!50003 SET character_set_client  = utf8mb4 */ ;\n/*!50003 SET character_set_results = utf8mb4 */ ;\n/*!50003 SET collation_connection  = utf8mb4_general_ci */ ;\n/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;\n/*!50003 SET sql_mode              = 'NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;\nDELIMITER ;;\n/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`localhost`*/ /*!50003 TRIGGER insert_gw2gwgroup\n  AFTER INSERT\n  ON dr_gw_lists\n  FOR EACH ROW\nBEGIN\n\n  DECLARE num_gws int DEFAULT 0;\n  DECLARE gw_index int DEFAULT 1;\n\n  IF CHAR_LENGTH(NEW.gwlist) > 0 THEN\n    SET num_gws := (CHAR_LENGTH(NEW.gwlist) - CHAR_LENGTH(REPLACE(NEW.gwlist, ',', '')) + 1);\n\n    \n    WHILE gw_index <= num_gws\n      DO\n        INSERT IGNORE INTO dsip_gw2gwgroup\n        VALUES (SUBSTRING_INDEX(SUBSTRING_INDEX(NEW.gwlist, ',', gw_index), ',', -1), cast(NEW.id AS char(64)), DEFAULT,\n                DEFAULT);\n        SET gw_index := gw_index + 1;\n      END WHILE;\n  END IF;\n\nEND */;;\nDELIMITER ;\n/*!50003 SET sql_mode              = @saved_sql_mode */ ;\n/*!50003 SET character_set_client  = @saved_cs_client */ ;\n/*!50003 SET character_set_results = @saved_cs_results */ ;\n/*!50003 SET collation_connection  = @saved_col_connection */ ;\n/*!50003 SET @saved_cs_client      = @@character_set_client */ ;\n/*!50003 SET @saved_cs_results     = @@character_set_results */ ;\n/*!50003 SET @saved_col_connection = @@collation_connection */ ;\n/*!50003 SET character_set_client  = utf8mb4 */ ;\n/*!50003 SET character_set_results = utf8mb4 */ ;\n/*!50003 SET collation_connection  = utf8mb4_general_ci */ ;\n/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;\n/*!50003 SET sql_mode              = 'NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;\nDELIMITER ;;\n/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`localhost`*/ /*!50003 TRIGGER update_gw2gwgroup\n  AFTER UPDATE\n  ON dr_gw_lists\n  FOR EACH ROW\nBEGIN\n\n  DECLARE num_gws int DEFAULT 0;\n  DECLARE gw_index int DEFAULT 1;\n\n  \n  IF NOT (NEW.gwlist <=> OLD.gwlist) THEN\n    DELETE FROM dsip_gw2gwgroup WHERE gwgroupid = cast(OLD.id AS char(64));\n\n    IF CHAR_LENGTH(NEW.gwlist) > 0 THEN\n      SET num_gws := (CHAR_LENGTH(NEW.gwlist) - CHAR_LENGTH(REPLACE(NEW.gwlist, ',', '')) + 1);\n\n      \n      WHILE gw_index <= num_gws\n        DO\n          INSERT IGNORE INTO dsip_gw2gwgroup\n          VALUES (SUBSTRING_INDEX(SUBSTRING_INDEX(NEW.gwlist, ',', gw_index), ',', -1), cast(NEW.id AS char(64)),\n                  DEFAULT,\n                  DEFAULT);\n          SET gw_index := gw_index + 1;\n        END WHILE;\n    END IF;\n\n    \n  ELSEIF NOT (NEW.id <=> OLD.id) THEN\n    UPDATE dsip_gw2gwgroup SET gwgroupid = cast(NEW.id AS char(64)) WHERE gwgroupid = cast(OLD.id AS char(64));\n  END IF;\n\nEND */;;\nDELIMITER ;\n/*!50003 SET sql_mode              = @saved_sql_mode */ ;\n/*!50003 SET character_set_client  = @saved_cs_client */ ;\n/*!50003 SET character_set_results = @saved_cs_results */ ;\n/*!50003 SET collation_connection  = @saved_col_connection */ ;\n/*!50003 SET @saved_cs_client      = @@character_set_client */ ;\n/*!50003 SET @saved_cs_results     = @@character_set_results */ ;\n/*!50003 SET @saved_col_connection = @@collation_connection */ ;\n/*!50003 SET character_set_client  = utf8mb4 */ ;\n/*!50003 SET character_set_results = utf8mb4 */ ;\n/*!50003 SET collation_connection  = utf8mb4_general_ci */ ;\n/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;\n/*!50003 SET sql_mode              = 'NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;\nDELIMITER ;;\n/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`localhost`*/ /*!50003 TRIGGER delete_gw2gwgroup\n  AFTER DELETE\n  ON dr_gw_lists\n  FOR EACH ROW\nBEGIN\n\n  DELETE FROM dsip_gw2gwgroup WHERE gwgroupid = cast(OLD.id AS char(64));\n\nEND */;;\nDELIMITER ;\n/*!50003 SET sql_mode              = @saved_sql_mode */ ;\n/*!50003 SET character_set_client  = @saved_cs_client */ ;\n/*!50003 SET character_set_results = @saved_cs_results */ ;\n/*!50003 SET collation_connection  = @saved_col_connection */ ;\n\n--\n-- Table structure for table `dr_rules`\n--\n\nDROP TABLE IF EXISTS `dr_rules`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dr_rules` (\n  `ruleid` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `groupid` varchar(255) NOT NULL,\n  `prefix` varchar(64) NOT NULL,\n  `timerec` varchar(255) NOT NULL,\n  `priority` int(11) NOT NULL DEFAULT '0',\n  `routeid` varchar(64) NOT NULL,\n  `gwlist` varchar(255) NOT NULL,\n  `description` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`ruleid`)\n) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dr_rules`\n--\n\nLOCK TABLES `dr_rules` WRITE;\n/*!40000 ALTER TABLE `dr_rules` DISABLE KEYS */;\nINSERT INTO `dr_rules` VALUES (1,'8000','','',0,'','1,2','name:Default Outbound Route'),(2,'9000','123456','',0,'','#9','name:test no fwd'),(3,'9000','1234567','',0,'','','name:test hard fwd'),(4,'9000','12345678','',0,'','#9','name:test fail fwd'),(5,'9000','123456789','',0,'','','name:test fail fwd + hard fwd'),(6,'9000','012345','',0,'','','name:test hard fwd + gwgroup'),(7,'20000','','',0,'','#10','name:Hard Forward from 012345 to DID 5555'),(8,'9000','0123456','',0,'','#9','name:test fail fwd + gwgroup'),(9,'20001','','',0,'','#11','name:Failover Forward from 0123456 to DID 6666'),(10,'9000','01234567','',0,'','','name:test hard fwd + fail fwd + gwgroups'),(11,'20002','','',0,'','#10','name:Hard Forward from 01234567 to DID 5555'),(12,'20003','','',0,'','#11','name:Failover Forward from 01234567 to DID 6666'),(17,'9000','012345678','',0,'','#12','name:test call limit');\n/*!40000 ALTER TABLE `dr_rules` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_calllimit`\n--\n\nDROP TABLE IF EXISTS `dsip_calllimit`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_calllimit` (\n  `gwgroupid` varchar(64) NOT NULL,\n  `limit` varchar(64) NOT NULL DEFAULT '0',\n  `status` tinyint(1) NOT NULL DEFAULT '1',\n  `key_type` varchar(64) NOT NULL DEFAULT '0',\n  `value_type` varchar(64) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`gwgroupid`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_calllimit`\n--\n\nLOCK TABLES `dsip_calllimit` WRITE;\n/*!40000 ALTER TABLE `dsip_calllimit` DISABLE KEYS */;\nINSERT INTO `dsip_calllimit` VALUES ('12','0',1,'0','0');\n/*!40000 ALTER TABLE `dsip_calllimit` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_domain_mapping`\n--\n\nDROP TABLE IF EXISTS `dsip_domain_mapping`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_domain_mapping` (\n  `id` int(10) NOT NULL AUTO_INCREMENT,\n  `pbx_id` int(10) NOT NULL,\n  `domain_id` int(10) NOT NULL,\n  `attr_list` varchar(255) NOT NULL,\n  `type` tinyint(3) NOT NULL DEFAULT '0',\n  `enabled` tinyint(1) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_domain_mapping`\n--\n\nLOCK TABLES `dsip_domain_mapping` WRITE;\n/*!40000 ALTER TABLE `dsip_domain_mapping` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_domain_mapping` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_endpoint_lease`\n--\n\nDROP TABLE IF EXISTS `dsip_endpoint_lease`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_endpoint_lease` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `gwid` int(10) unsigned NOT NULL,\n  `sid` int(10) unsigned NOT NULL,\n  `expiration` datetime NOT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_endpoint_lease`\n--\n\nLOCK TABLES `dsip_endpoint_lease` WRITE;\n/*!40000 ALTER TABLE `dsip_endpoint_lease` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_endpoint_lease` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_failfwd`\n--\n\nDROP TABLE IF EXISTS `dsip_failfwd`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_failfwd` (\n  `dr_ruleid` varchar(64) NOT NULL,\n  `did` varchar(64) NOT NULL,\n  `dr_groupid` varchar(64) NOT NULL,\n  `key_type` varchar(64) NOT NULL DEFAULT '0',\n  `value_type` varchar(64) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`dr_ruleid`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_failfwd`\n--\n\nLOCK TABLES `dsip_failfwd` WRITE;\n/*!40000 ALTER TABLE `dsip_failfwd` DISABLE KEYS */;\nINSERT INTO `dsip_failfwd` VALUES ('10','6666','20003','0','0'),('4','6666','8000','0','0'),('5','6666','8000','0','0'),('8','6666','20001','0','0');\n/*!40000 ALTER TABLE `dsip_failfwd` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_gw2gwgroup`\n--\n\nDROP TABLE IF EXISTS `dsip_gw2gwgroup`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_gw2gwgroup` (\n  `gwid` varchar(64) NOT NULL,\n  `gwgroupid` varchar(64) NOT NULL,\n  `key_type` varchar(64) NOT NULL DEFAULT '0',\n  `value_type` varchar(64) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`gwid`,`gwgroupid`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_gw2gwgroup`\n--\n\nLOCK TABLES `dsip_gw2gwgroup` WRITE;\n/*!40000 ALTER TABLE `dsip_gw2gwgroup` DISABLE KEYS */;\nINSERT INTO `dsip_gw2gwgroup` VALUES ('1','1','0','0'),('10','1','0','0'),('11','1','0','0'),('12','2','0','0'),('13','2','0','0'),('14','2','0','0'),('15','2','0','0'),('16','3','0','0'),('17','3','0','0'),('18','3','0','0'),('19','3','0','0'),('2','1','0','0'),('20','3','0','0'),('21','3','0','0'),('22','4','0','0'),('23','4','0','0'),('24','4','0','0'),('25','4','0','0'),('26','4','0','0'),('27','4','0','0'),('28','4','0','0'),('29','4','0','0'),('3','1','0','0'),('30','4','0','0'),('31','4','0','0'),('32','4','0','0'),('33','4','0','0'),('34','4','0','0'),('35','4','0','0'),('36','4','0','0'),('37','4','0','0'),('38','5','0','0'),('39','6','0','0'),('4','1','0','0'),('40','7','0','0'),('5','1','0','0'),('6','1','0','0'),('64','8','0','0'),('65','9','0','0'),('66','10','0','0'),('67','11','0','0'),('68','12','0','0'),('69','8','0','0'),('7','1','0','0'),('8','1','0','0'),('9','1','0','0');\n/*!40000 ALTER TABLE `dsip_gw2gwgroup` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_hardfwd`\n--\n\nDROP TABLE IF EXISTS `dsip_hardfwd`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_hardfwd` (\n  `dr_ruleid` varchar(64) NOT NULL,\n  `did` varchar(64) NOT NULL,\n  `dr_groupid` varchar(64) NOT NULL,\n  `key_type` varchar(64) NOT NULL DEFAULT '0',\n  `value_type` varchar(64) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`dr_ruleid`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_hardfwd`\n--\n\nLOCK TABLES `dsip_hardfwd` WRITE;\n/*!40000 ALTER TABLE `dsip_hardfwd` DISABLE KEYS */;\nINSERT INTO `dsip_hardfwd` VALUES ('10','5555','20002','0','0'),('3','5555','8000','0','0'),('5','5555','8000','0','0'),('6','5555','20000','0','0');\n/*!40000 ALTER TABLE `dsip_hardfwd` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_lcr`\n--\n\nDROP TABLE IF EXISTS `dsip_lcr`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_lcr` (\n  `pattern` varchar(64) NOT NULL DEFAULT '',\n  `key_type` varchar(64) NOT NULL DEFAULT '0',\n  `dr_groupid` varchar(64) NOT NULL DEFAULT '',\n  `value_type` varchar(64) NOT NULL DEFAULT '0',\n  `cost` decimal(3,2) NOT NULL DEFAULT '0.00',\n  `from_prefix` varchar(64) NOT NULL DEFAULT '',\n  `expires` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`pattern`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_lcr`\n--\n\nLOCK TABLES `dsip_lcr` WRITE;\n/*!40000 ALTER TABLE `dsip_lcr` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_lcr` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_maintmode`\n--\n\nDROP TABLE IF EXISTS `dsip_maintmode`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_maintmode` (\n  `ipaddr` varchar(64) NOT NULL DEFAULT '',\n  `key_type` varchar(64) NOT NULL DEFAULT '0',\n  `gwid` varchar(64) NOT NULL DEFAULT '',\n  `value_type` varchar(64) NOT NULL DEFAULT '0',\n  `status` tinyint(4) NOT NULL DEFAULT '1',\n  PRIMARY KEY (`ipaddr`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_maintmode`\n--\n\nLOCK TABLES `dsip_maintmode` WRITE;\n/*!40000 ALTER TABLE `dsip_maintmode` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_maintmode` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_multidomain_mapping`\n--\n\nDROP TABLE IF EXISTS `dsip_multidomain_mapping`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_multidomain_mapping` (\n  `id` int(10) NOT NULL AUTO_INCREMENT,\n  `pbx_id` int(10) NOT NULL,\n  `db_host` varchar(20) NOT NULL,\n  `db_username` varchar(40) NOT NULL,\n  `db_password` varchar(40) NOT NULL,\n  `domain_list` varchar(255) NOT NULL DEFAULT '',\n  `domain_list_hash` varchar(255) NOT NULL DEFAULT '',\n  `attr_list` varchar(255) NOT NULL DEFAULT '',\n  `type` tinyint(3) NOT NULL DEFAULT '0',\n  `enabled` tinyint(1) NOT NULL DEFAULT '0',\n  `lastsync` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n  `syncstatus` tinyint(1) NOT NULL DEFAULT '0',\n  `syncerror` varchar(200) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_multidomain_mapping`\n--\n\nLOCK TABLES `dsip_multidomain_mapping` WRITE;\n/*!40000 ALTER TABLE `dsip_multidomain_mapping` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_multidomain_mapping` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_notification`\n--\n\nDROP TABLE IF EXISTS `dsip_notification`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_notification` (\n  `gwgroupid` int(11) NOT NULL,\n  `type` int(11) NOT NULL,\n  `method` int(11) DEFAULT NULL,\n  `value` varchar(255) DEFAULT NULL,\n  `createdate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n  PRIMARY KEY (`gwgroupid`,`type`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_notification`\n--\n\nLOCK TABLES `dsip_notification` WRITE;\n/*!40000 ALTER TABLE `dsip_notification` DISABLE KEYS */;\nINSERT INTO `dsip_notification` VALUES (9,0,0,'','2019-09-30 19:07:46'),(9,1,0,'','2019-09-30 19:07:46'),(10,0,0,'','2019-09-30 19:08:23'),(10,1,0,'','2019-09-30 19:08:23'),(11,0,0,'','2019-09-30 19:08:33'),(11,1,0,'','2019-09-30 19:08:33'),(12,0,0,'tmoore@goflyball.com','2019-09-30 23:20:24'),(12,1,0,'tmoore@goflyball.com','2019-09-30 23:20:24');\n/*!40000 ALTER TABLE `dsip_notification` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Temporary table structure for view `dsip_prefix_mapping`\n--\n\nDROP TABLE IF EXISTS `dsip_prefix_mapping`;\n/*!50001 DROP VIEW IF EXISTS `dsip_prefix_mapping`*/;\nSET @saved_cs_client     = @@character_set_client;\nSET character_set_client = utf8;\n/*!50001 CREATE TABLE `dsip_prefix_mapping` (\n  `prefix` tinyint NOT NULL,\n  `ruleid` tinyint NOT NULL,\n  `key_type` tinyint NOT NULL,\n  `value_type` tinyint NOT NULL\n) ENGINE=MyISAM */;\nSET character_set_client = @saved_cs_client;\n\n--\n-- Table structure for table `dsip_settings`\n--\n\nDROP TABLE IF EXISTS `dsip_settings`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_settings` (\n  `DSIP_ID` int(10) unsigned NOT NULL DEFAULT '1',\n  `FLT_CARRIER` int(11) NOT NULL DEFAULT '8',\n  `FLT_PBX` int(11) NOT NULL DEFAULT '9',\n  `FLT_OUTBOUND` int(11) NOT NULL DEFAULT '8000',\n  `FLT_INBOUND` int(11) NOT NULL DEFAULT '9000',\n  `FLT_LCR_MIN` int(11) NOT NULL DEFAULT '10000',\n  `FLT_FWD_MIN` int(11) NOT NULL DEFAULT '20000',\n  PRIMARY KEY (`DSIP_ID`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8 MIN_ROWS=1 MAX_ROWS=1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_settings`\n--\n\nLOCK TABLES `dsip_settings` WRITE;\n/*!40000 ALTER TABLE `dsip_settings` DISABLE KEYS */;\nINSERT INTO `dsip_settings` VALUES (1,8,9,8000,9000,10000,20000);\n/*!40000 ALTER TABLE `dsip_settings` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `globalblacklist`\n--\n\nDROP TABLE IF EXISTS `globalblacklist`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `globalblacklist` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `prefix` varchar(64) NOT NULL DEFAULT '',\n  `whitelist` tinyint(1) NOT NULL DEFAULT '0',\n  `description` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `globalblacklist_idx` (`prefix`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `globalblacklist`\n--\n\nLOCK TABLES `globalblacklist` WRITE;\n/*!40000 ALTER TABLE `globalblacklist` DISABLE KEYS */;\n/*!40000 ALTER TABLE `globalblacklist` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `grp`\n--\n\nDROP TABLE IF EXISTS `grp`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `grp` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `grp` varchar(64) NOT NULL DEFAULT '',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `account_group_idx` (`username`,`domain`,`grp`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `grp`\n--\n\nLOCK TABLES `grp` WRITE;\n/*!40000 ALTER TABLE `grp` DISABLE KEYS */;\n/*!40000 ALTER TABLE `grp` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `htable`\n--\n\nDROP TABLE IF EXISTS `htable`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `htable` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `key_name` varchar(64) NOT NULL DEFAULT '',\n  `key_type` int(11) NOT NULL DEFAULT '0',\n  `value_type` int(11) NOT NULL DEFAULT '0',\n  `key_value` varchar(128) NOT NULL DEFAULT '',\n  `expires` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `htable`\n--\n\nLOCK TABLES `htable` WRITE;\n/*!40000 ALTER TABLE `htable` DISABLE KEYS */;\n/*!40000 ALTER TABLE `htable` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `imc_members`\n--\n\nDROP TABLE IF EXISTS `imc_members`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `imc_members` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL,\n  `room` varchar(64) NOT NULL,\n  `flag` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `account_room_idx` (`username`,`domain`,`room`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `imc_members`\n--\n\nLOCK TABLES `imc_members` WRITE;\n/*!40000 ALTER TABLE `imc_members` DISABLE KEYS */;\n/*!40000 ALTER TABLE `imc_members` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `imc_rooms`\n--\n\nDROP TABLE IF EXISTS `imc_rooms`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `imc_rooms` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `name` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL,\n  `flag` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `name_domain_idx` (`name`,`domain`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `imc_rooms`\n--\n\nLOCK TABLES `imc_rooms` WRITE;\n/*!40000 ALTER TABLE `imc_rooms` DISABLE KEYS */;\n/*!40000 ALTER TABLE `imc_rooms` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `lcr_gw`\n--\n\nDROP TABLE IF EXISTS `lcr_gw`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `lcr_gw` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `lcr_id` smallint(5) unsigned NOT NULL,\n  `gw_name` varchar(128) DEFAULT NULL,\n  `ip_addr` varchar(50) DEFAULT NULL,\n  `hostname` varchar(64) DEFAULT NULL,\n  `port` smallint(5) unsigned DEFAULT NULL,\n  `params` varchar(64) DEFAULT NULL,\n  `uri_scheme` tinyint(3) unsigned DEFAULT NULL,\n  `transport` tinyint(3) unsigned DEFAULT NULL,\n  `strip` tinyint(3) unsigned DEFAULT NULL,\n  `prefix` varchar(16) DEFAULT NULL,\n  `tag` varchar(64) DEFAULT NULL,\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  `defunct` int(10) unsigned DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `lcr_id_idx` (`lcr_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `lcr_gw`\n--\n\nLOCK TABLES `lcr_gw` WRITE;\n/*!40000 ALTER TABLE `lcr_gw` DISABLE KEYS */;\n/*!40000 ALTER TABLE `lcr_gw` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `lcr_rule`\n--\n\nDROP TABLE IF EXISTS `lcr_rule`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `lcr_rule` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `lcr_id` smallint(5) unsigned NOT NULL,\n  `prefix` varchar(16) DEFAULT NULL,\n  `from_uri` varchar(64) DEFAULT NULL,\n  `request_uri` varchar(64) DEFAULT NULL,\n  `mt_tvalue` varchar(128) DEFAULT NULL,\n  `stopper` int(10) unsigned NOT NULL DEFAULT '0',\n  `enabled` int(10) unsigned NOT NULL DEFAULT '1',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `lcr_id_prefix_from_uri_idx` (`lcr_id`,`prefix`,`from_uri`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `lcr_rule`\n--\n\nLOCK TABLES `lcr_rule` WRITE;\n/*!40000 ALTER TABLE `lcr_rule` DISABLE KEYS */;\n/*!40000 ALTER TABLE `lcr_rule` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `lcr_rule_target`\n--\n\nDROP TABLE IF EXISTS `lcr_rule_target`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `lcr_rule_target` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `lcr_id` smallint(5) unsigned NOT NULL,\n  `rule_id` int(10) unsigned NOT NULL,\n  `gw_id` int(10) unsigned NOT NULL,\n  `priority` tinyint(3) unsigned NOT NULL,\n  `weight` int(10) unsigned NOT NULL DEFAULT '1',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `rule_id_gw_id_idx` (`rule_id`,`gw_id`),\n  KEY `lcr_id_idx` (`lcr_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `lcr_rule_target`\n--\n\nLOCK TABLES `lcr_rule_target` WRITE;\n/*!40000 ALTER TABLE `lcr_rule_target` DISABLE KEYS */;\n/*!40000 ALTER TABLE `lcr_rule_target` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `locale_lookup`\n--\n\nDROP TABLE IF EXISTS `locale_lookup`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `locale_lookup` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `locale` varchar(64) NOT NULL DEFAULT '',\n  `fprefix` varchar(64) NOT NULL DEFAULT '0',\n  `tprefix` varchar(64) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `locale_lookup`\n--\n\nLOCK TABLES `locale_lookup` WRITE;\n/*!40000 ALTER TABLE `locale_lookup` DISABLE KEYS */;\n/*!40000 ALTER TABLE `locale_lookup` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `location`\n--\n\nDROP TABLE IF EXISTS `location`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `location` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `ruid` varchar(64) NOT NULL DEFAULT '',\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) DEFAULT NULL,\n  `contact` varchar(512) NOT NULL DEFAULT '',\n  `received` varchar(128) DEFAULT NULL,\n  `path` varchar(512) DEFAULT NULL,\n  `expires` datetime NOT NULL DEFAULT '2030-05-28 21:32:15',\n  `q` float(10,2) NOT NULL DEFAULT '1.00',\n  `callid` varchar(255) NOT NULL DEFAULT 'Default-Call-ID',\n  `cseq` int(11) NOT NULL DEFAULT '1',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `cflags` int(11) NOT NULL DEFAULT '0',\n  `user_agent` varchar(255) NOT NULL DEFAULT '',\n  `socket` varchar(64) DEFAULT NULL,\n  `methods` int(11) DEFAULT NULL,\n  `instance` varchar(255) DEFAULT NULL,\n  `reg_id` int(11) NOT NULL DEFAULT '0',\n  `server_id` int(11) NOT NULL DEFAULT '0',\n  `connection_id` int(11) NOT NULL DEFAULT '0',\n  `keepalive` int(11) NOT NULL DEFAULT '0',\n  `partition` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `ruid_idx` (`ruid`),\n  KEY `account_contact_idx` (`username`,`domain`,`contact`),\n  KEY `expires_idx` (`expires`),\n  KEY `connection_idx` (`server_id`,`connection_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `location`\n--\n\nLOCK TABLES `location` WRITE;\n/*!40000 ALTER TABLE `location` DISABLE KEYS */;\n/*!40000 ALTER TABLE `location` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `location_attrs`\n--\n\nDROP TABLE IF EXISTS `location_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `location_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `ruid` varchar(64) NOT NULL DEFAULT '',\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) DEFAULT NULL,\n  `aname` varchar(64) NOT NULL DEFAULT '',\n  `atype` int(11) NOT NULL DEFAULT '0',\n  `avalue` varchar(255) NOT NULL DEFAULT '',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  KEY `account_record_idx` (`username`,`domain`,`ruid`),\n  KEY `last_modified_idx` (`last_modified`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `location_attrs`\n--\n\nLOCK TABLES `location_attrs` WRITE;\n/*!40000 ALTER TABLE `location_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `location_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `missed_calls`\n--\n\nDROP TABLE IF EXISTS `missed_calls`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `missed_calls` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `method` varchar(16) NOT NULL DEFAULT '',\n  `from_tag` varchar(64) NOT NULL DEFAULT '',\n  `to_tag` varchar(64) NOT NULL DEFAULT '',\n  `callid` varchar(255) NOT NULL DEFAULT '',\n  `sip_code` varchar(3) NOT NULL DEFAULT '',\n  `sip_reason` varchar(128) NOT NULL DEFAULT '',\n  `time` datetime NOT NULL,\n  PRIMARY KEY (`id`),\n  KEY `callid_idx` (`callid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `missed_calls`\n--\n\nLOCK TABLES `missed_calls` WRITE;\n/*!40000 ALTER TABLE `missed_calls` DISABLE KEYS */;\n/*!40000 ALTER TABLE `missed_calls` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `mohqcalls`\n--\n\nDROP TABLE IF EXISTS `mohqcalls`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `mohqcalls` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `mohq_id` int(10) unsigned NOT NULL,\n  `call_id` varchar(100) NOT NULL,\n  `call_status` int(10) unsigned NOT NULL,\n  `call_from` varchar(100) NOT NULL,\n  `call_contact` varchar(100) DEFAULT NULL,\n  `call_time` datetime NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `mohqcalls_idx` (`call_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `mohqcalls`\n--\n\nLOCK TABLES `mohqcalls` WRITE;\n/*!40000 ALTER TABLE `mohqcalls` DISABLE KEYS */;\n/*!40000 ALTER TABLE `mohqcalls` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `mohqueues`\n--\n\nDROP TABLE IF EXISTS `mohqueues`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `mohqueues` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `name` varchar(25) NOT NULL,\n  `uri` varchar(100) NOT NULL,\n  `mohdir` varchar(100) DEFAULT NULL,\n  `mohfile` varchar(100) NOT NULL,\n  `debug` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `mohqueue_uri_idx` (`uri`),\n  UNIQUE KEY `mohqueue_name_idx` (`name`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `mohqueues`\n--\n\nLOCK TABLES `mohqueues` WRITE;\n/*!40000 ALTER TABLE `mohqueues` DISABLE KEYS */;\n/*!40000 ALTER TABLE `mohqueues` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `mtree`\n--\n\nDROP TABLE IF EXISTS `mtree`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `mtree` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `tprefix` varchar(32) NOT NULL DEFAULT '',\n  `tvalue` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `tprefix_idx` (`tprefix`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `mtree`\n--\n\nLOCK TABLES `mtree` WRITE;\n/*!40000 ALTER TABLE `mtree` DISABLE KEYS */;\n/*!40000 ALTER TABLE `mtree` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `mtrees`\n--\n\nDROP TABLE IF EXISTS `mtrees`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `mtrees` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `tname` varchar(128) NOT NULL DEFAULT '',\n  `tprefix` varchar(32) NOT NULL DEFAULT '',\n  `tvalue` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `tname_tprefix_tvalue_idx` (`tname`,`tprefix`,`tvalue`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `mtrees`\n--\n\nLOCK TABLES `mtrees` WRITE;\n/*!40000 ALTER TABLE `mtrees` DISABLE KEYS */;\n/*!40000 ALTER TABLE `mtrees` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `pdt`\n--\n\nDROP TABLE IF EXISTS `pdt`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `pdt` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `sdomain` varchar(128) NOT NULL,\n  `prefix` varchar(32) NOT NULL,\n  `domain` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `sdomain_prefix_idx` (`sdomain`,`prefix`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `pdt`\n--\n\nLOCK TABLES `pdt` WRITE;\n/*!40000 ALTER TABLE `pdt` DISABLE KEYS */;\n/*!40000 ALTER TABLE `pdt` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `pl_pipes`\n--\n\nDROP TABLE IF EXISTS `pl_pipes`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `pl_pipes` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `pipeid` varchar(64) NOT NULL DEFAULT '',\n  `algorithm` varchar(32) NOT NULL DEFAULT '',\n  `plimit` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `pl_pipes`\n--\n\nLOCK TABLES `pl_pipes` WRITE;\n/*!40000 ALTER TABLE `pl_pipes` DISABLE KEYS */;\n/*!40000 ALTER TABLE `pl_pipes` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `presentity`\n--\n\nDROP TABLE IF EXISTS `presentity`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `presentity` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL,\n  `event` varchar(64) NOT NULL,\n  `etag` varchar(64) NOT NULL,\n  `expires` int(11) NOT NULL,\n  `received_time` int(11) NOT NULL,\n  `body` blob NOT NULL,\n  `sender` varchar(128) NOT NULL,\n  `priority` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `presentity_idx` (`username`,`domain`,`event`,`etag`),\n  KEY `presentity_expires` (`expires`),\n  KEY `account_idx` (`username`,`domain`,`event`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `presentity`\n--\n\nLOCK TABLES `presentity` WRITE;\n/*!40000 ALTER TABLE `presentity` DISABLE KEYS */;\n/*!40000 ALTER TABLE `presentity` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `pua`\n--\n\nDROP TABLE IF EXISTS `pua`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `pua` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `pres_uri` varchar(128) NOT NULL,\n  `pres_id` varchar(255) NOT NULL,\n  `event` int(11) NOT NULL,\n  `expires` int(11) NOT NULL,\n  `desired_expires` int(11) NOT NULL,\n  `flag` int(11) NOT NULL,\n  `etag` varchar(64) NOT NULL,\n  `tuple_id` varchar(64) DEFAULT NULL,\n  `watcher_uri` varchar(128) NOT NULL,\n  `call_id` varchar(255) NOT NULL,\n  `to_tag` varchar(64) NOT NULL,\n  `from_tag` varchar(64) NOT NULL,\n  `cseq` int(11) NOT NULL,\n  `record_route` text,\n  `contact` varchar(128) NOT NULL,\n  `remote_contact` varchar(128) NOT NULL,\n  `version` int(11) NOT NULL,\n  `extra_headers` text NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `pua_idx` (`etag`,`tuple_id`,`call_id`,`from_tag`),\n  KEY `expires_idx` (`expires`),\n  KEY `dialog1_idx` (`pres_id`,`pres_uri`),\n  KEY `dialog2_idx` (`call_id`,`from_tag`),\n  KEY `record_idx` (`pres_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `pua`\n--\n\nLOCK TABLES `pua` WRITE;\n/*!40000 ALTER TABLE `pua` DISABLE KEYS */;\n/*!40000 ALTER TABLE `pua` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `purplemap`\n--\n\nDROP TABLE IF EXISTS `purplemap`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `purplemap` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `sip_user` varchar(128) NOT NULL,\n  `ext_user` varchar(128) NOT NULL,\n  `ext_prot` varchar(16) NOT NULL,\n  `ext_pass` varchar(64) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `purplemap`\n--\n\nLOCK TABLES `purplemap` WRITE;\n/*!40000 ALTER TABLE `purplemap` DISABLE KEYS */;\n/*!40000 ALTER TABLE `purplemap` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `re_grp`\n--\n\nDROP TABLE IF EXISTS `re_grp`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `re_grp` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `reg_exp` varchar(128) NOT NULL DEFAULT '',\n  `group_id` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  KEY `group_idx` (`group_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `re_grp`\n--\n\nLOCK TABLES `re_grp` WRITE;\n/*!40000 ALTER TABLE `re_grp` DISABLE KEYS */;\n/*!40000 ALTER TABLE `re_grp` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `rls_presentity`\n--\n\nDROP TABLE IF EXISTS `rls_presentity`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `rls_presentity` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `rlsubs_did` varchar(255) NOT NULL,\n  `resource_uri` varchar(128) NOT NULL,\n  `content_type` varchar(255) NOT NULL,\n  `presence_state` blob NOT NULL,\n  `expires` int(11) NOT NULL,\n  `updated` int(11) NOT NULL,\n  `auth_state` int(11) NOT NULL,\n  `reason` varchar(64) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `rls_presentity_idx` (`rlsubs_did`,`resource_uri`),\n  KEY `rlsubs_idx` (`rlsubs_did`),\n  KEY `updated_idx` (`updated`),\n  KEY `expires_idx` (`expires`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `rls_presentity`\n--\n\nLOCK TABLES `rls_presentity` WRITE;\n/*!40000 ALTER TABLE `rls_presentity` DISABLE KEYS */;\n/*!40000 ALTER TABLE `rls_presentity` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `rls_watchers`\n--\n\nDROP TABLE IF EXISTS `rls_watchers`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `rls_watchers` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `presentity_uri` varchar(128) NOT NULL,\n  `to_user` varchar(64) NOT NULL,\n  `to_domain` varchar(64) NOT NULL,\n  `watcher_username` varchar(64) NOT NULL,\n  `watcher_domain` varchar(64) NOT NULL,\n  `event` varchar(64) NOT NULL DEFAULT 'presence',\n  `event_id` varchar(64) DEFAULT NULL,\n  `to_tag` varchar(64) NOT NULL,\n  `from_tag` varchar(64) NOT NULL,\n  `callid` varchar(255) NOT NULL,\n  `local_cseq` int(11) NOT NULL,\n  `remote_cseq` int(11) NOT NULL,\n  `contact` varchar(128) NOT NULL,\n  `record_route` text,\n  `expires` int(11) NOT NULL,\n  `status` int(11) NOT NULL DEFAULT '2',\n  `reason` varchar(64) NOT NULL,\n  `version` int(11) NOT NULL DEFAULT '0',\n  `socket_info` varchar(64) NOT NULL,\n  `local_contact` varchar(128) NOT NULL,\n  `from_user` varchar(64) NOT NULL,\n  `from_domain` varchar(64) NOT NULL,\n  `updated` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `rls_watcher_idx` (`callid`,`to_tag`,`from_tag`),\n  KEY `rls_watchers_update` (`watcher_username`,`watcher_domain`,`event`),\n  KEY `rls_watchers_expires` (`expires`),\n  KEY `updated_idx` (`updated`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `rls_watchers`\n--\n\nLOCK TABLES `rls_watchers` WRITE;\n/*!40000 ALTER TABLE `rls_watchers` DISABLE KEYS */;\n/*!40000 ALTER TABLE `rls_watchers` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `rtpengine`\n--\n\nDROP TABLE IF EXISTS `rtpengine`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `rtpengine` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `setid` int(10) unsigned NOT NULL DEFAULT '0',\n  `url` varchar(64) NOT NULL,\n  `weight` int(10) unsigned NOT NULL DEFAULT '1',\n  `disabled` int(1) NOT NULL DEFAULT '0',\n  `stamp` datetime NOT NULL DEFAULT '1900-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `rtpengine_nodes` (`setid`,`url`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `rtpengine`\n--\n\nLOCK TABLES `rtpengine` WRITE;\n/*!40000 ALTER TABLE `rtpengine` DISABLE KEYS */;\n/*!40000 ALTER TABLE `rtpengine` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `rtpproxy`\n--\n\nDROP TABLE IF EXISTS `rtpproxy`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `rtpproxy` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `setid` varchar(32) NOT NULL DEFAULT '0',\n  `url` varchar(64) NOT NULL DEFAULT '',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `weight` int(11) NOT NULL DEFAULT '1',\n  `description` varchar(64) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `rtpproxy`\n--\n\nLOCK TABLES `rtpproxy` WRITE;\n/*!40000 ALTER TABLE `rtpproxy` DISABLE KEYS */;\n/*!40000 ALTER TABLE `rtpproxy` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `sca_subscriptions`\n--\n\nDROP TABLE IF EXISTS `sca_subscriptions`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `sca_subscriptions` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `subscriber` varchar(255) NOT NULL,\n  `aor` varchar(255) NOT NULL,\n  `event` int(11) NOT NULL DEFAULT '0',\n  `expires` int(11) NOT NULL DEFAULT '0',\n  `state` int(11) NOT NULL DEFAULT '0',\n  `app_idx` int(11) NOT NULL DEFAULT '0',\n  `call_id` varchar(255) NOT NULL,\n  `from_tag` varchar(64) NOT NULL,\n  `to_tag` varchar(64) NOT NULL,\n  `record_route` text,\n  `notify_cseq` int(11) NOT NULL,\n  `subscribe_cseq` int(11) NOT NULL,\n  `server_id` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `sca_subscriptions_idx` (`subscriber`,`call_id`,`from_tag`,`to_tag`),\n  KEY `sca_expires_idx` (`server_id`,`expires`),\n  KEY `sca_subscribers_idx` (`subscriber`,`event`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `sca_subscriptions`\n--\n\nLOCK TABLES `sca_subscriptions` WRITE;\n/*!40000 ALTER TABLE `sca_subscriptions` DISABLE KEYS */;\n/*!40000 ALTER TABLE `sca_subscriptions` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `silo`\n--\n\nDROP TABLE IF EXISTS `silo`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `silo` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `src_addr` varchar(128) NOT NULL DEFAULT '',\n  `dst_addr` varchar(128) NOT NULL DEFAULT '',\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `inc_time` int(11) NOT NULL DEFAULT '0',\n  `exp_time` int(11) NOT NULL DEFAULT '0',\n  `snd_time` int(11) NOT NULL DEFAULT '0',\n  `ctype` varchar(32) NOT NULL DEFAULT 'text/plain',\n  `body` blob,\n  `extra_hdrs` text,\n  `callid` varchar(128) NOT NULL DEFAULT '',\n  `status` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  KEY `account_idx` (`username`,`domain`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `silo`\n--\n\nLOCK TABLES `silo` WRITE;\n/*!40000 ALTER TABLE `silo` DISABLE KEYS */;\n/*!40000 ALTER TABLE `silo` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `sip_trace`\n--\n\nDROP TABLE IF EXISTS `sip_trace`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `sip_trace` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `time_stamp` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  `time_us` int(10) unsigned NOT NULL DEFAULT '0',\n  `callid` varchar(255) NOT NULL DEFAULT '',\n  `traced_user` varchar(128) NOT NULL DEFAULT '',\n  `msg` mediumtext NOT NULL,\n  `method` varchar(50) NOT NULL DEFAULT '',\n  `status` varchar(128) NOT NULL DEFAULT '',\n  `fromip` varchar(50) NOT NULL DEFAULT '',\n  `toip` varchar(50) NOT NULL DEFAULT '',\n  `fromtag` varchar(64) NOT NULL DEFAULT '',\n  `totag` varchar(64) NOT NULL DEFAULT '',\n  `direction` varchar(4) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  KEY `traced_user_idx` (`traced_user`),\n  KEY `date_idx` (`time_stamp`),\n  KEY `fromip_idx` (`fromip`),\n  KEY `callid_idx` (`callid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `sip_trace`\n--\n\nLOCK TABLES `sip_trace` WRITE;\n/*!40000 ALTER TABLE `sip_trace` DISABLE KEYS */;\n/*!40000 ALTER TABLE `sip_trace` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `speed_dial`\n--\n\nDROP TABLE IF EXISTS `speed_dial`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `speed_dial` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `sd_username` varchar(64) NOT NULL DEFAULT '',\n  `sd_domain` varchar(64) NOT NULL DEFAULT '',\n  `new_uri` varchar(128) NOT NULL DEFAULT '',\n  `fname` varchar(64) NOT NULL DEFAULT '',\n  `lname` varchar(64) NOT NULL DEFAULT '',\n  `description` varchar(64) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `speed_dial_idx` (`username`,`domain`,`sd_domain`,`sd_username`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `speed_dial`\n--\n\nLOCK TABLES `speed_dial` WRITE;\n/*!40000 ALTER TABLE `speed_dial` DISABLE KEYS */;\n/*!40000 ALTER TABLE `speed_dial` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `subscriber`\n--\n\nDROP TABLE IF EXISTS `subscriber`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `subscriber` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `password` varchar(64) NOT NULL DEFAULT '',\n  `ha1` varchar(128) NOT NULL DEFAULT '',\n  `ha1b` varchar(128) NOT NULL DEFAULT '',\n  `email_address` varchar(128) DEFAULT NULL,\n  `rpid` varchar(128) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `account_idx` (`username`,`domain`),\n  KEY `username_idx` (`username`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `subscriber`\n--\n\nLOCK TABLES `subscriber` WRITE;\n/*!40000 ALTER TABLE `subscriber` DISABLE KEYS */;\n/*!40000 ALTER TABLE `subscriber` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `topos_d`\n--\n\nDROP TABLE IF EXISTS `topos_d`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `topos_d` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `rectime` datetime NOT NULL,\n  `s_method` varchar(64) NOT NULL DEFAULT '',\n  `s_cseq` varchar(64) NOT NULL DEFAULT '',\n  `a_callid` varchar(255) NOT NULL DEFAULT '',\n  `a_uuid` varchar(255) NOT NULL DEFAULT '',\n  `b_uuid` varchar(255) NOT NULL DEFAULT '',\n  `a_contact` varchar(128) NOT NULL DEFAULT '',\n  `b_contact` varchar(128) NOT NULL DEFAULT '',\n  `as_contact` varchar(128) NOT NULL DEFAULT '',\n  `bs_contact` varchar(128) NOT NULL DEFAULT '',\n  `a_tag` varchar(255) NOT NULL DEFAULT '',\n  `b_tag` varchar(255) NOT NULL DEFAULT '',\n  `a_rr` mediumtext,\n  `b_rr` mediumtext,\n  `s_rr` mediumtext,\n  `iflags` int(10) unsigned NOT NULL DEFAULT '0',\n  `a_uri` varchar(128) NOT NULL DEFAULT '',\n  `b_uri` varchar(128) NOT NULL DEFAULT '',\n  `r_uri` varchar(128) NOT NULL DEFAULT '',\n  `a_srcaddr` varchar(128) NOT NULL DEFAULT '',\n  `b_srcaddr` varchar(128) NOT NULL DEFAULT '',\n  `a_socket` varchar(128) NOT NULL DEFAULT '',\n  `b_socket` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  KEY `rectime_idx` (`rectime`),\n  KEY `a_callid_idx` (`a_callid`),\n  KEY `a_uuid_idx` (`a_uuid`),\n  KEY `b_uuid_idx` (`b_uuid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `topos_d`\n--\n\nLOCK TABLES `topos_d` WRITE;\n/*!40000 ALTER TABLE `topos_d` DISABLE KEYS */;\n/*!40000 ALTER TABLE `topos_d` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `topos_t`\n--\n\nDROP TABLE IF EXISTS `topos_t`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `topos_t` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `rectime` datetime NOT NULL,\n  `s_method` varchar(64) NOT NULL DEFAULT '',\n  `s_cseq` varchar(64) NOT NULL DEFAULT '',\n  `a_callid` varchar(255) NOT NULL DEFAULT '',\n  `a_uuid` varchar(255) NOT NULL DEFAULT '',\n  `b_uuid` varchar(255) NOT NULL DEFAULT '',\n  `direction` int(11) NOT NULL DEFAULT '0',\n  `x_via` mediumtext,\n  `x_vbranch` varchar(255) NOT NULL DEFAULT '',\n  `x_rr` mediumtext,\n  `y_rr` mediumtext,\n  `s_rr` mediumtext,\n  `x_uri` varchar(128) NOT NULL DEFAULT '',\n  `a_contact` varchar(128) NOT NULL DEFAULT '',\n  `b_contact` varchar(128) NOT NULL DEFAULT '',\n  `as_contact` varchar(128) NOT NULL DEFAULT '',\n  `bs_contact` varchar(128) NOT NULL DEFAULT '',\n  `x_tag` varchar(255) NOT NULL DEFAULT '',\n  `a_tag` varchar(255) NOT NULL DEFAULT '',\n  `b_tag` varchar(255) NOT NULL DEFAULT '',\n  `a_srcaddr` varchar(128) NOT NULL DEFAULT '',\n  `b_srcaddr` varchar(128) NOT NULL DEFAULT '',\n  `a_socket` varchar(128) NOT NULL DEFAULT '',\n  `b_socket` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  KEY `rectime_idx` (`rectime`),\n  KEY `a_callid_idx` (`a_callid`),\n  KEY `x_vbranch_idx` (`x_vbranch`),\n  KEY `a_uuid_idx` (`a_uuid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `topos_t`\n--\n\nLOCK TABLES `topos_t` WRITE;\n/*!40000 ALTER TABLE `topos_t` DISABLE KEYS */;\n/*!40000 ALTER TABLE `topos_t` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `trusted`\n--\n\nDROP TABLE IF EXISTS `trusted`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `trusted` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `src_ip` varchar(50) NOT NULL,\n  `proto` varchar(4) NOT NULL,\n  `from_pattern` varchar(64) DEFAULT NULL,\n  `ruri_pattern` varchar(64) DEFAULT NULL,\n  `tag` varchar(64) DEFAULT NULL,\n  `priority` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  KEY `peer_idx` (`src_ip`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `trusted`\n--\n\nLOCK TABLES `trusted` WRITE;\n/*!40000 ALTER TABLE `trusted` DISABLE KEYS */;\n/*!40000 ALTER TABLE `trusted` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uacreg`\n--\n\nDROP TABLE IF EXISTS `uacreg`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uacreg` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `l_uuid` varchar(64) NOT NULL DEFAULT '',\n  `l_username` varchar(64) NOT NULL DEFAULT '',\n  `l_domain` varchar(64) NOT NULL DEFAULT '',\n  `r_username` varchar(64) NOT NULL DEFAULT '',\n  `r_domain` varchar(64) NOT NULL DEFAULT '',\n  `realm` varchar(64) NOT NULL DEFAULT '',\n  `auth_username` varchar(64) NOT NULL DEFAULT '',\n  `auth_password` varchar(64) NOT NULL DEFAULT '',\n  `auth_ha1` varchar(128) NOT NULL DEFAULT '',\n  `auth_proxy` varchar(128) NOT NULL DEFAULT '',\n  `expires` int(11) NOT NULL DEFAULT '0',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `reg_delay` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `l_uuid_idx` (`l_uuid`)\n) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uacreg`\n--\n\nLOCK TABLES `uacreg` WRITE;\n/*!40000 ALTER TABLE `uacreg` DISABLE KEYS */;\nINSERT INTO `uacreg` VALUES (1,'1','1','50.253.243.17','','','','','','','',60,1,0),(2,'2','2','50.253.243.17','','','','','','','',60,1,0),(3,'3','3','50.253.243.17','','','','','','','',60,1,0),(4,'4','4','50.253.243.17','','','','','','','',60,1,0),(5,'5','5','50.253.243.17','','','','','','','',60,1,0),(6,'6','6','50.253.243.17','','','','','','','',60,1,0),(7,'7','7','50.253.243.17','','','','','','','',60,1,0),(8,'8','8','50.253.243.17','','','','','','','',60,1,0);\n/*!40000 ALTER TABLE `uacreg` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_credentials`\n--\n\nDROP TABLE IF EXISTS `uid_credentials`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_credentials` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `auth_username` varchar(64) NOT NULL,\n  `did` varchar(64) NOT NULL DEFAULT '_default',\n  `realm` varchar(64) NOT NULL,\n  `password` varchar(28) NOT NULL DEFAULT '',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `ha1` varchar(32) NOT NULL,\n  `ha1b` varchar(32) NOT NULL DEFAULT '',\n  `uid` varchar(64) NOT NULL,\n  PRIMARY KEY (`id`),\n  KEY `cred_idx` (`auth_username`,`did`),\n  KEY `uid` (`uid`),\n  KEY `did_idx` (`did`),\n  KEY `realm_idx` (`realm`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_credentials`\n--\n\nLOCK TABLES `uid_credentials` WRITE;\n/*!40000 ALTER TABLE `uid_credentials` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_credentials` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_domain`\n--\n\nDROP TABLE IF EXISTS `uid_domain`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_domain` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `did` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL,\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `domain_idx` (`domain`),\n  KEY `did_idx` (`did`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_domain`\n--\n\nLOCK TABLES `uid_domain` WRITE;\n/*!40000 ALTER TABLE `uid_domain` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_domain` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_domain_attrs`\n--\n\nDROP TABLE IF EXISTS `uid_domain_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_domain_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `did` varchar(64) DEFAULT NULL,\n  `name` varchar(32) NOT NULL,\n  `type` int(11) NOT NULL DEFAULT '0',\n  `value` varchar(128) DEFAULT NULL,\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `domain_attr_idx` (`did`,`name`,`value`),\n  KEY `domain_did` (`did`,`flags`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_domain_attrs`\n--\n\nLOCK TABLES `uid_domain_attrs` WRITE;\n/*!40000 ALTER TABLE `uid_domain_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_domain_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_global_attrs`\n--\n\nDROP TABLE IF EXISTS `uid_global_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_global_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `name` varchar(32) NOT NULL,\n  `type` int(11) NOT NULL DEFAULT '0',\n  `value` varchar(128) DEFAULT NULL,\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `global_attrs_idx` (`name`,`value`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_global_attrs`\n--\n\nLOCK TABLES `uid_global_attrs` WRITE;\n/*!40000 ALTER TABLE `uid_global_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_global_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_uri`\n--\n\nDROP TABLE IF EXISTS `uid_uri`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_uri` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `uid` varchar(64) NOT NULL,\n  `did` varchar(64) NOT NULL,\n  `username` varchar(64) NOT NULL,\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  `scheme` varchar(8) NOT NULL DEFAULT 'sip',\n  PRIMARY KEY (`id`),\n  KEY `uri_idx1` (`username`,`did`,`scheme`),\n  KEY `uri_uid` (`uid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_uri`\n--\n\nLOCK TABLES `uid_uri` WRITE;\n/*!40000 ALTER TABLE `uid_uri` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_uri` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_uri_attrs`\n--\n\nDROP TABLE IF EXISTS `uid_uri_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_uri_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `did` varchar(64) NOT NULL,\n  `name` varchar(32) NOT NULL,\n  `value` varchar(128) DEFAULT NULL,\n  `type` int(11) NOT NULL DEFAULT '0',\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  `scheme` varchar(8) NOT NULL DEFAULT 'sip',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `uriattrs_idx` (`username`,`did`,`name`,`value`,`scheme`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_uri_attrs`\n--\n\nLOCK TABLES `uid_uri_attrs` WRITE;\n/*!40000 ALTER TABLE `uid_uri_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_uri_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_user_attrs`\n--\n\nDROP TABLE IF EXISTS `uid_user_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_user_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `uid` varchar(64) NOT NULL,\n  `name` varchar(32) NOT NULL,\n  `value` varchar(128) DEFAULT NULL,\n  `type` int(11) NOT NULL DEFAULT '0',\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `userattrs_idx` (`uid`,`name`,`value`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_user_attrs`\n--\n\nLOCK TABLES `uid_user_attrs` WRITE;\n/*!40000 ALTER TABLE `uid_user_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_user_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uri`\n--\n\nDROP TABLE IF EXISTS `uri`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uri` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `uri_user` varchar(64) NOT NULL DEFAULT '',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `account_idx` (`username`,`domain`,`uri_user`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uri`\n--\n\nLOCK TABLES `uri` WRITE;\n/*!40000 ALTER TABLE `uri` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uri` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `userblacklist`\n--\n\nDROP TABLE IF EXISTS `userblacklist`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `userblacklist` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `prefix` varchar(64) NOT NULL DEFAULT '',\n  `whitelist` tinyint(1) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  KEY `userblacklist_idx` (`username`,`domain`,`prefix`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `userblacklist`\n--\n\nLOCK TABLES `userblacklist` WRITE;\n/*!40000 ALTER TABLE `userblacklist` DISABLE KEYS */;\n/*!40000 ALTER TABLE `userblacklist` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `usr_preferences`\n--\n\nDROP TABLE IF EXISTS `usr_preferences`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `usr_preferences` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `uuid` varchar(64) NOT NULL DEFAULT '',\n  `username` varchar(128) NOT NULL DEFAULT '0',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `attribute` varchar(32) NOT NULL DEFAULT '',\n  `type` int(11) NOT NULL DEFAULT '0',\n  `value` varchar(128) NOT NULL DEFAULT '',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  KEY `ua_idx` (`uuid`,`attribute`),\n  KEY `uda_idx` (`username`,`domain`,`attribute`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `usr_preferences`\n--\n\nLOCK TABLES `usr_preferences` WRITE;\n/*!40000 ALTER TABLE `usr_preferences` DISABLE KEYS */;\n/*!40000 ALTER TABLE `usr_preferences` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `version`\n--\n\nDROP TABLE IF EXISTS `version`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `version` (\n  `table_name` varchar(32) NOT NULL,\n  `table_version` int(10) unsigned NOT NULL DEFAULT '0',\n  UNIQUE KEY `table_name_idx` (`table_name`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `version`\n--\n\nLOCK TABLES `version` WRITE;\n/*!40000 ALTER TABLE `version` DISABLE KEYS */;\nINSERT INTO `version` VALUES ('acc',5),('acc_cdrs',2),('active_watchers',12),('address',6),('aliases',8),('carrierfailureroute',2),('carrierroute',3),('carrier_name',1),('cpl',1),('dbaliases',1),('dialog',7),('dialog_vars',1),('dialplan',2),('dispatcher',4),('domain',2),('domainpolicy',2),('domain_attrs',1),('domain_name',1),('dr_gateways',3),('dr_groups',2),('dr_gw_lists',1),('dr_rules',3),('globalblacklist',1),('grp',2),('htable',2),('imc_members',1),('imc_rooms',1),('lcr_gw',3),('lcr_rule',3),('lcr_rule_target',1),('location',9),('location_attrs',1),('missed_calls',4),('mohqcalls',1),('mohqueues',1),('mtree',1),('mtrees',2),('pdt',1),('pl_pipes',1),('presentity',4),('pua',7),('purplemap',1),('re_grp',1),('rls_presentity',1),('rls_watchers',3),('rtpengine',1),('rtpproxy',1),('sca_subscriptions',2),('silo',8),('sip_trace',4),('speed_dial',2),('subscriber',7),('topos_d',1),('topos_t',1),('trusted',6),('uacreg',3),('uid_credentials',7),('uid_domain',2),('uid_domain_attrs',1),('uid_global_attrs',1),('uid_uri',3),('uid_uri_attrs',2),('uid_user_attrs',3),('uri',1),('userblacklist',1),('usr_preferences',2),('version',1),('watchers',3),('xcap',4);\n/*!40000 ALTER TABLE `version` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `watchers`\n--\n\nDROP TABLE IF EXISTS `watchers`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `watchers` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `presentity_uri` varchar(128) NOT NULL,\n  `watcher_username` varchar(64) NOT NULL,\n  `watcher_domain` varchar(64) NOT NULL,\n  `event` varchar(64) NOT NULL DEFAULT 'presence',\n  `status` int(11) NOT NULL,\n  `reason` varchar(64) DEFAULT NULL,\n  `inserted_time` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `watcher_idx` (`presentity_uri`,`watcher_username`,`watcher_domain`,`event`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `watchers`\n--\n\nLOCK TABLES `watchers` WRITE;\n/*!40000 ALTER TABLE `watchers` DISABLE KEYS */;\n/*!40000 ALTER TABLE `watchers` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `xcap`\n--\n\nDROP TABLE IF EXISTS `xcap`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `xcap` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL,\n  `doc` mediumblob NOT NULL,\n  `doc_type` int(11) NOT NULL,\n  `etag` varchar(64) NOT NULL,\n  `source` int(11) NOT NULL,\n  `doc_uri` varchar(255) NOT NULL,\n  `port` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `doc_uri_idx` (`doc_uri`),\n  KEY `account_doc_type_idx` (`username`,`domain`,`doc_type`),\n  KEY `account_doc_type_uri_idx` (`username`,`domain`,`doc_type`,`doc_uri`),\n  KEY `account_doc_uri_idx` (`username`,`domain`,`doc_uri`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `xcap`\n--\n\nLOCK TABLES `xcap` WRITE;\n/*!40000 ALTER TABLE `xcap` DISABLE KEYS */;\n/*!40000 ALTER TABLE `xcap` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Dumping events for database 'kamailio'\n--\n\n--\n-- Dumping routines for database 'kamailio'\n--\n/*!50003 DROP PROCEDURE IF EXISTS `kamailio_cdrs` */;\n/*!50003 SET @saved_cs_client      = @@character_set_client */ ;\n/*!50003 SET @saved_cs_results     = @@character_set_results */ ;\n/*!50003 SET @saved_col_connection = @@collation_connection */ ;\n/*!50003 SET character_set_client  = utf8 */ ;\n/*!50003 SET character_set_results = utf8 */ ;\n/*!50003 SET collation_connection  = utf8_general_ci */ ;\n/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;\n/*!50003 SET sql_mode              = '' */ ;\nDELIMITER ;;\nCREATE DEFINER=`root`@`localhost` PROCEDURE `kamailio_cdrs`()\nBEGIN\n  DECLARE done INT DEFAULT 0;\n  DECLARE bye_record INT DEFAULT 0;\n  DECLARE v_src_user,v_src_domain,v_dst_user,v_dst_domain,v_callid,v_from_tag,\n     v_to_tag,v_src_ip,v_calltype VARCHAR(64);\n  DECLARE v_inv_time, v_bye_time DATETIME;\n  DECLARE inv_cursor CURSOR FOR SELECT src_user, src_domain, dst_user,\n     dst_domain, time, callid,from_tag, to_tag, src_ip, calltype FROM acc\n     where method='INVITE' and cdr_id='0';\n  DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;\n  OPEN inv_cursor;\n  REPEAT\n    FETCH inv_cursor INTO v_src_user, v_src_domain, v_dst_user, v_dst_domain,\n            v_inv_time, v_callid, v_from_tag, v_to_tag, v_src_ip, v_calltype;\n    IF NOT done THEN\n      SET bye_record = 0;\n      SELECT 1, time INTO bye_record, v_bye_time FROM acc WHERE\n                 method='BYE' AND callid=v_callid AND ((from_tag=v_from_tag\n                 AND to_tag=v_to_tag)\n                 OR (from_tag=v_to_tag AND to_tag=v_from_tag))\n                 ORDER BY time ASC LIMIT 1;\n      IF bye_record = 1 THEN\n        INSERT INTO cdrs (src_username,src_domain,dst_username,\n                 dst_domain,call_start_time,duration,sip_call_id,sip_from_tag,\n                 sip_to_tag,src_ip,created,calltype) VALUES (v_src_user,v_src_domain,\n                 v_dst_user,v_dst_domain,v_inv_time,\n                 UNIX_TIMESTAMP(v_bye_time)-UNIX_TIMESTAMP(v_inv_time),\n                 v_callid,v_from_tag,v_to_tag,v_src_ip,NOW(),v_calltype);\n        UPDATE acc SET cdr_id=last_insert_id() WHERE callid=v_callid\n                 AND from_tag=v_from_tag AND to_tag=v_to_tag;\n      END IF;\n      SET done = 0;\n    END IF;\n  UNTIL done END REPEAT;\nEND ;;\nDELIMITER ;\n/*!50003 SET sql_mode              = @saved_sql_mode */ ;\n/*!50003 SET character_set_client  = @saved_cs_client */ ;\n/*!50003 SET character_set_results = @saved_cs_results */ ;\n/*!50003 SET collation_connection  = @saved_col_connection */ ;\n/*!50003 DROP PROCEDURE IF EXISTS `kamailio_rating` */;\n/*!50003 SET @saved_cs_client      = @@character_set_client */ ;\n/*!50003 SET @saved_cs_results     = @@character_set_results */ ;\n/*!50003 SET @saved_col_connection = @@collation_connection */ ;\n/*!50003 SET character_set_client  = utf8 */ ;\n/*!50003 SET character_set_results = utf8 */ ;\n/*!50003 SET collation_connection  = utf8_general_ci */ ;\n/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;\n/*!50003 SET sql_mode              = '' */ ;\nDELIMITER ;;\nCREATE DEFINER=`kamailio`@`localhost` PROCEDURE `kamailio_rating`(`rgroup` varchar(64))\nBEGIN\n  DECLARE done, rate_record, vx_cost INT DEFAULT 0;\n  DECLARE v_cdr_id BIGINT DEFAULT 0;\n  DECLARE v_duration, v_rate_unit, v_time_unit INT DEFAULT 0;\n  DECLARE v_dst_username VARCHAR(64);\n  DECLARE cdrs_cursor CURSOR FOR SELECT cdr_id, dst_username, duration\n     FROM cdrs WHERE rated=0;\n  DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;\n  OPEN cdrs_cursor;\n  REPEAT\n    FETCH cdrs_cursor INTO v_cdr_id, v_dst_username, v_duration;\n    IF NOT done THEN\n      SET rate_record = 0;\n      SELECT 1, rate_unit, time_unit INTO rate_record, v_rate_unit, v_time_unit\n             FROM billing_rates\n             WHERE rate_group=rgroup AND v_dst_username LIKE concat(prefix, '%')\n             ORDER BY prefix DESC LIMIT 1;\n      IF rate_record = 1 THEN\n        SET vx_cost = v_rate_unit * CEIL(v_duration/v_time_unit);\n        UPDATE cdrs SET rated=1, cost=vx_cost WHERE cdr_id=v_cdr_id;\n      END IF;\n      SET done = 0;\n    END IF;\n  UNTIL done END REPEAT;\nEND ;;\nDELIMITER ;\n/*!50003 SET sql_mode              = @saved_sql_mode */ ;\n/*!50003 SET character_set_client  = @saved_cs_client */ ;\n/*!50003 SET character_set_results = @saved_cs_results */ ;\n/*!50003 SET collation_connection  = @saved_col_connection */ ;\n\n--\n-- Final view structure for view `dsip_prefix_mapping`\n--\n\n/*!50001 DROP TABLE IF EXISTS `dsip_prefix_mapping`*/;\n/*!50001 DROP VIEW IF EXISTS `dsip_prefix_mapping`*/;\n/*!50001 SET @saved_cs_client          = @@character_set_client */;\n/*!50001 SET @saved_cs_results         = @@character_set_results */;\n/*!50001 SET @saved_col_connection     = @@collation_connection */;\n/*!50001 SET character_set_client      = utf8 */;\n/*!50001 SET character_set_results     = utf8 */;\n/*!50001 SET collation_connection      = utf8mb4_general_ci */;\n/*!50001 CREATE ALGORITHM=UNDEFINED */\n/*!50013 DEFINER=`root`@`localhost` SQL SECURITY DEFINER */\n/*!50001 VIEW `dsip_prefix_mapping` AS select `dr_rules`.`prefix` AS `prefix`,cast(`dr_rules`.`ruleid` as char(64) charset utf8mb4) AS `ruleid`,'0' AS `key_type`,'0' AS `value_type` from `dr_rules` where (`dr_rules`.`groupid` = (select `dsip_settings`.`FLT_INBOUND` from `dsip_settings` where (`dsip_settings`.`DSIP_ID` = 1) limit 1)) */;\n/*!50001 SET character_set_client      = @saved_cs_client */;\n/*!50001 SET character_set_results     = @saved_cs_results */;\n/*!50001 SET collation_connection      = @saved_col_connection */;\n/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;\n\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n\n-- Dump completed on 2019-09-30 19:43:05\n"
  },
  {
    "path": "testing/sql/v0.60+ent/grants.sql",
    "content": "GRANT USAGE ON *.* TO 'kamailio'@'localhost' IDENTIFIED BY PASSWORD '*2870144497941F902294EDABA260A0A4A15078E4';\nGRANT ALL PRIVILEGES ON `kamailio`.* TO 'kamailio'@'localhost';\nGRANT USAGE ON *.* TO 'kamailioro'@'localhost' IDENTIFIED BY PASSWORD '*CF29822D11D49E1965F110105EACFBE9202F1A31';\nGRANT SELECT ON `kamailio`.* TO 'kamailioro'@'localhost';\nGRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED VIA unix_socket WITH GRANT OPTION;\nGRANT PROXY ON ''@'%' TO 'root'@'localhost' WITH GRANT OPTION;\n"
  },
  {
    "path": "testing/sql/v0.60+ent/kamailio.sql",
    "content": "-- MySQL dump 10.16  Distrib 10.1.41-MariaDB, for debian-linux-gnu (x86_64)\n--\n-- Host: localhost    Database: kamailio\n-- ------------------------------------------------------\n-- Server version\t10.1.41-MariaDB-0+deb9u1\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!40101 SET NAMES utf8mb4 */;\n/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n/*!40103 SET TIME_ZONE='+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n--\n-- Table structure for table `acc`\n--\n\nDROP TABLE IF EXISTS `acc`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `acc` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `method` varchar(16) NOT NULL DEFAULT '',\n  `from_tag` varchar(64) NOT NULL DEFAULT '',\n  `to_tag` varchar(64) NOT NULL DEFAULT '',\n  `callid` varchar(128) NOT NULL DEFAULT '',\n  `sip_code` char(3) NOT NULL DEFAULT '',\n  `sip_reason` varchar(32) NOT NULL DEFAULT '',\n  `time` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',\n  `src_ip` varchar(64) NOT NULL DEFAULT '',\n  `dst_ouser` varchar(64) NOT NULL DEFAULT '',\n  `dst_user` varchar(64) NOT NULL DEFAULT '',\n  `dst_domain` varchar(128) NOT NULL DEFAULT '',\n  `src_user` varchar(64) NOT NULL DEFAULT '',\n  `src_domain` varchar(128) NOT NULL DEFAULT '',\n  `cdr_id` int(11) NOT NULL DEFAULT '0',\n  `calltype` varchar(20) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `acc_callid` (`callid`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `acc`\n--\n\nLOCK TABLES `acc` WRITE;\n/*!40000 ALTER TABLE `acc` DISABLE KEYS */;\n/*!40000 ALTER TABLE `acc` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `acc_cdrs`\n--\n\nDROP TABLE IF EXISTS `acc_cdrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `acc_cdrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `start_time` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',\n  `end_time` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',\n  `duration` float(10,3) NOT NULL DEFAULT '0.000',\n  PRIMARY KEY (`id`),\n  KEY `start_time_idx` (`start_time`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `acc_cdrs`\n--\n\nLOCK TABLES `acc_cdrs` WRITE;\n/*!40000 ALTER TABLE `acc_cdrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `acc_cdrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `active_watchers`\n--\n\nDROP TABLE IF EXISTS `active_watchers`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `active_watchers` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `presentity_uri` varchar(128) NOT NULL,\n  `watcher_username` varchar(64) NOT NULL,\n  `watcher_domain` varchar(64) NOT NULL,\n  `to_user` varchar(64) NOT NULL,\n  `to_domain` varchar(64) NOT NULL,\n  `event` varchar(64) NOT NULL DEFAULT 'presence',\n  `event_id` varchar(64) DEFAULT NULL,\n  `to_tag` varchar(64) NOT NULL,\n  `from_tag` varchar(64) NOT NULL,\n  `callid` varchar(255) NOT NULL,\n  `local_cseq` int(11) NOT NULL,\n  `remote_cseq` int(11) NOT NULL,\n  `contact` varchar(128) NOT NULL,\n  `record_route` text,\n  `expires` int(11) NOT NULL,\n  `status` int(11) NOT NULL DEFAULT '2',\n  `reason` varchar(64) DEFAULT NULL,\n  `version` int(11) NOT NULL DEFAULT '0',\n  `socket_info` varchar(64) NOT NULL,\n  `local_contact` varchar(128) NOT NULL,\n  `from_user` varchar(64) NOT NULL,\n  `from_domain` varchar(64) NOT NULL,\n  `updated` int(11) NOT NULL,\n  `updated_winfo` int(11) NOT NULL,\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `user_agent` varchar(255) DEFAULT '',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `active_watchers_idx` (`callid`,`to_tag`,`from_tag`),\n  KEY `active_watchers_expires` (`expires`),\n  KEY `active_watchers_pres` (`presentity_uri`,`event`),\n  KEY `updated_idx` (`updated`),\n  KEY `updated_winfo_idx` (`updated_winfo`,`presentity_uri`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `active_watchers`\n--\n\nLOCK TABLES `active_watchers` WRITE;\n/*!40000 ALTER TABLE `active_watchers` DISABLE KEYS */;\n/*!40000 ALTER TABLE `active_watchers` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `address`\n--\n\nDROP TABLE IF EXISTS `address`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `address` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `grp` int(11) unsigned NOT NULL DEFAULT '1',\n  `ip_addr` varchar(50) NOT NULL,\n  `mask` int(11) NOT NULL DEFAULT '32',\n  `port` smallint(5) unsigned NOT NULL DEFAULT '0',\n  `tag` varchar(64) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=190 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `address`\n--\n\nLOCK TABLES `address` WRITE;\n/*!40000 ALTER TABLE `address` DISABLE KEYS */;\nINSERT INTO `address` VALUES (127,8,'52.41.52.34',32,0,'name:Skyetel North West Inbound,gwgroup:1'),(128,8,'52.8.201.128',32,0,'name:Skyetel South West Inbound,gwgroup:1'),(129,8,'52.60.138.31',32,0,'name:Skyetel North East Inbound,gwgroup:1'),(130,8,'50.17.48.216',32,0,'name:Skyetel South East Inbound,gwgroup:1'),(131,8,'35.156.192.164',32,0,'name:Skyetel Europe Inbound,gwgroup:1'),(132,8,'term.skyetel.com',32,0,'name:Skyetel 1st Priority Outbound Call,gwgroup:1'),(133,8,'52.41.52.34',32,0,'name:Skyetel 2nd Priority Outbound Call,gwgroup:1'),(134,8,'52.8.201.128',32,0,'name:Skyetel 3rd Priority Outbound Call,gwgroup:1'),(135,8,'50.17.48.216',32,0,'name:Skyetel 4rd Priority Outbound Call,gwgroup:1'),(136,8,'52.32.223.28',32,0,'name:Skyetel North West High Cost Outbound Traffic,gwgroup:1'),(137,8,'52.4.178.107',32,0,'name:Skyetel South East High Cost Outbound Traffic,gwgroup:1'),(138,8,'147.75.60.160',32,0,'name:Flowroute US-West-WA,gwgroup:2'),(139,8,'34.210.91.112',32,0,'name:Flowroute US-West-OR,gwgroup:2'),(140,8,'147.75.65.192',32,0,'name:Flowroute US-East-NJ,gwgroup:2'),(141,8,'34.226.36.32',32,0,'name:Flowroute US-East-VA,gwgroup:2'),(142,8,'81.201.82.45',32,0,'name:Voxbone Belgium,gwgroup:3'),(143,8,'81.201.84.195',32,0,'name:Voxbone LA,gwgroup:3'),(144,8,'81.201.85.45',32,0,'name:Voxbone NYC,gwgroup:3'),(145,8,'81.201.83.45',32,0,'name:Voxbone Germany,gwgroup:3'),(146,8,'81.201.86.45',32,0,'name:Voxbone Hong Kong,gwgroup:3'),(147,8,'81.201.84.195',32,0,'name:Voxbone Australia,gwgroup:3'),(148,8,'64.136.174.30',32,0,'name:VI Carrier,gwgroup:4'),(149,8,'64.136.173.22',32,0,'name:VI Carrier,gwgroup:4'),(150,8,'209.166.128.200',32,0,'name:VI Carrier,gwgroup:4'),(151,8,'192.240.151.100',32,0,'name:VI Carrier,gwgroup:4'),(152,8,'64.136.173.31',32,0,'name:VI Carrier,gwgroup:4'),(153,8,'64.136.174.30',32,0,'name:VI Carrier,gwgroup:4'),(154,8,'64.136.174.20',32,0,'name:VI Carrier,gwgroup:4'),(155,8,'209.166.154.70',32,0,'name:VI Carrier,gwgroup:4'),(156,8,'64.136.174.65',32,0,'name:VI Carrier,gwgroup:4'),(157,8,'64.136.173.23',32,0,'name:VI Carrier,gwgroup:4'),(158,8,'209.166.128.201',32,0,'name:VI Carrier,gwgroup:4'),(159,8,'192.240.151.101',32,0,'name:VI Carrier,gwgroup:4'),(160,8,'64.136.173.65',32,0,'name:VI Carrier,gwgroup:4'),(161,8,'64.136.174.65',32,0,'name:VI Carrier,gwgroup:4'),(162,8,'64.136.174.21',32,0,'name:VI Carrier,gwgroup:4'),(163,8,'209.166.154.71',32,0,'name:VI Carrier,gwgroup:4'),(164,8,'72.15.219.140',32,0,'name:Thinq Carrier,gwgroup:5'),(165,8,'216.147.191.157',32,0,'name:Voxtelesys Carrier,gwgroup:6'),(166,8,'64.34.181.47',32,0,'name:Les.net Carrier,gwgroup:7');\n/*!40000 ALTER TABLE `address` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `aliases`\n--\n\nDROP TABLE IF EXISTS `aliases`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `aliases` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `ruid` varchar(64) NOT NULL DEFAULT '',\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) DEFAULT NULL,\n  `contact` varchar(255) NOT NULL DEFAULT '',\n  `received` varchar(128) DEFAULT NULL,\n  `path` varchar(512) DEFAULT NULL,\n  `expires` datetime NOT NULL DEFAULT '2030-05-28 21:32:15',\n  `q` float(10,2) NOT NULL DEFAULT '1.00',\n  `callid` varchar(255) NOT NULL DEFAULT 'Default-Call-ID',\n  `cseq` int(11) NOT NULL DEFAULT '1',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `cflags` int(11) NOT NULL DEFAULT '0',\n  `user_agent` varchar(255) NOT NULL DEFAULT '',\n  `socket` varchar(64) DEFAULT NULL,\n  `methods` int(11) DEFAULT NULL,\n  `instance` varchar(255) DEFAULT NULL,\n  `reg_id` int(11) NOT NULL DEFAULT '0',\n  `server_id` int(11) NOT NULL DEFAULT '0',\n  `connection_id` int(11) NOT NULL DEFAULT '0',\n  `keepalive` int(11) NOT NULL DEFAULT '0',\n  `partition` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `ruid_idx` (`ruid`),\n  KEY `account_contact_idx` (`username`,`domain`,`contact`),\n  KEY `expires_idx` (`expires`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `aliases`\n--\n\nLOCK TABLES `aliases` WRITE;\n/*!40000 ALTER TABLE `aliases` DISABLE KEYS */;\n/*!40000 ALTER TABLE `aliases` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `carrier_name`\n--\n\nDROP TABLE IF EXISTS `carrier_name`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `carrier_name` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `carrier` varchar(64) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `carrier_name`\n--\n\nLOCK TABLES `carrier_name` WRITE;\n/*!40000 ALTER TABLE `carrier_name` DISABLE KEYS */;\n/*!40000 ALTER TABLE `carrier_name` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `carrierfailureroute`\n--\n\nDROP TABLE IF EXISTS `carrierfailureroute`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `carrierfailureroute` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `carrier` int(10) unsigned NOT NULL DEFAULT '0',\n  `domain` int(10) unsigned NOT NULL DEFAULT '0',\n  `scan_prefix` varchar(64) NOT NULL DEFAULT '',\n  `host_name` varchar(128) NOT NULL DEFAULT '',\n  `reply_code` varchar(3) NOT NULL DEFAULT '',\n  `flags` int(11) unsigned NOT NULL DEFAULT '0',\n  `mask` int(11) unsigned NOT NULL DEFAULT '0',\n  `next_domain` int(10) unsigned NOT NULL DEFAULT '0',\n  `description` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `carrierfailureroute`\n--\n\nLOCK TABLES `carrierfailureroute` WRITE;\n/*!40000 ALTER TABLE `carrierfailureroute` DISABLE KEYS */;\n/*!40000 ALTER TABLE `carrierfailureroute` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `carrierroute`\n--\n\nDROP TABLE IF EXISTS `carrierroute`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `carrierroute` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `carrier` int(10) unsigned NOT NULL DEFAULT '0',\n  `domain` int(10) unsigned NOT NULL DEFAULT '0',\n  `scan_prefix` varchar(64) NOT NULL DEFAULT '',\n  `flags` int(11) unsigned NOT NULL DEFAULT '0',\n  `mask` int(11) unsigned NOT NULL DEFAULT '0',\n  `prob` float NOT NULL DEFAULT '0',\n  `strip` int(11) unsigned NOT NULL DEFAULT '0',\n  `rewrite_host` varchar(128) NOT NULL DEFAULT '',\n  `rewrite_prefix` varchar(64) NOT NULL DEFAULT '',\n  `rewrite_suffix` varchar(64) NOT NULL DEFAULT '',\n  `description` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `carrierroute`\n--\n\nLOCK TABLES `carrierroute` WRITE;\n/*!40000 ALTER TABLE `carrierroute` DISABLE KEYS */;\n/*!40000 ALTER TABLE `carrierroute` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `cdrs`\n--\n\nDROP TABLE IF EXISTS `cdrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `cdrs` (\n  `cdr_id` bigint(20) NOT NULL AUTO_INCREMENT,\n  `src_username` varchar(64) NOT NULL DEFAULT '',\n  `src_domain` varchar(128) NOT NULL DEFAULT '',\n  `dst_username` varchar(64) NOT NULL DEFAULT '',\n  `dst_domain` varchar(128) NOT NULL DEFAULT '',\n  `dst_ousername` varchar(64) NOT NULL DEFAULT '',\n  `call_start_time` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',\n  `duration` int(10) unsigned NOT NULL DEFAULT '0',\n  `sip_call_id` varchar(128) NOT NULL DEFAULT '',\n  `sip_from_tag` varchar(128) NOT NULL DEFAULT '',\n  `sip_to_tag` varchar(128) NOT NULL DEFAULT '',\n  `src_ip` varchar(64) NOT NULL DEFAULT '',\n  `cost` int(11) NOT NULL DEFAULT '0',\n  `rated` int(11) NOT NULL DEFAULT '0',\n  `created` datetime NOT NULL,\n  `calltype` varchar(20) DEFAULT NULL,\n  `fraud` tinyint(1) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`cdr_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `cdrs`\n--\n\nLOCK TABLES `cdrs` WRITE;\n/*!40000 ALTER TABLE `cdrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `cdrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `cpl`\n--\n\nDROP TABLE IF EXISTS `cpl`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `cpl` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `cpl_xml` text,\n  `cpl_bin` text,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `account_idx` (`username`,`domain`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `cpl`\n--\n\nLOCK TABLES `cpl` WRITE;\n/*!40000 ALTER TABLE `cpl` DISABLE KEYS */;\n/*!40000 ALTER TABLE `cpl` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dbaliases`\n--\n\nDROP TABLE IF EXISTS `dbaliases`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dbaliases` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `alias_username` varchar(64) NOT NULL DEFAULT '',\n  `alias_domain` varchar(64) NOT NULL DEFAULT '',\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  KEY `alias_user_idx` (`alias_username`),\n  KEY `alias_idx` (`alias_username`,`alias_domain`),\n  KEY `target_idx` (`username`,`domain`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dbaliases`\n--\n\nLOCK TABLES `dbaliases` WRITE;\n/*!40000 ALTER TABLE `dbaliases` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dbaliases` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dialog`\n--\n\nDROP TABLE IF EXISTS `dialog`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dialog` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `hash_entry` int(10) unsigned NOT NULL,\n  `hash_id` int(10) unsigned NOT NULL,\n  `callid` varchar(255) NOT NULL,\n  `from_uri` varchar(128) NOT NULL,\n  `from_tag` varchar(64) NOT NULL,\n  `to_uri` varchar(128) NOT NULL,\n  `to_tag` varchar(64) NOT NULL,\n  `caller_cseq` varchar(20) NOT NULL,\n  `callee_cseq` varchar(20) NOT NULL,\n  `caller_route_set` varchar(512) DEFAULT NULL,\n  `callee_route_set` varchar(512) DEFAULT NULL,\n  `caller_contact` varchar(128) NOT NULL,\n  `callee_contact` varchar(128) NOT NULL,\n  `caller_sock` varchar(64) NOT NULL,\n  `callee_sock` varchar(64) NOT NULL,\n  `state` int(10) unsigned NOT NULL,\n  `start_time` int(10) unsigned NOT NULL,\n  `timeout` int(10) unsigned NOT NULL DEFAULT '0',\n  `sflags` int(10) unsigned NOT NULL DEFAULT '0',\n  `iflags` int(10) unsigned NOT NULL DEFAULT '0',\n  `toroute_name` varchar(32) DEFAULT NULL,\n  `req_uri` varchar(128) NOT NULL,\n  `xdata` varchar(512) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `hash_idx` (`hash_entry`,`hash_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dialog`\n--\n\nLOCK TABLES `dialog` WRITE;\n/*!40000 ALTER TABLE `dialog` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dialog` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dialog_vars`\n--\n\nDROP TABLE IF EXISTS `dialog_vars`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dialog_vars` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `hash_entry` int(10) unsigned NOT NULL,\n  `hash_id` int(10) unsigned NOT NULL,\n  `dialog_key` varchar(128) NOT NULL,\n  `dialog_value` varchar(512) NOT NULL,\n  PRIMARY KEY (`id`),\n  KEY `hash_idx` (`hash_entry`,`hash_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dialog_vars`\n--\n\nLOCK TABLES `dialog_vars` WRITE;\n/*!40000 ALTER TABLE `dialog_vars` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dialog_vars` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dialplan`\n--\n\nDROP TABLE IF EXISTS `dialplan`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dialplan` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `dpid` int(11) NOT NULL,\n  `pr` int(11) NOT NULL,\n  `match_op` int(11) NOT NULL,\n  `match_exp` varchar(64) NOT NULL,\n  `match_len` int(11) NOT NULL,\n  `subst_exp` varchar(64) NOT NULL,\n  `repl_exp` varchar(256) NOT NULL,\n  `attrs` varchar(64) NOT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dialplan`\n--\n\nLOCK TABLES `dialplan` WRITE;\n/*!40000 ALTER TABLE `dialplan` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dialplan` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dispatcher`\n--\n\nDROP TABLE IF EXISTS `dispatcher`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dispatcher` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `setid` int(11) NOT NULL DEFAULT '0',\n  `destination` varchar(192) NOT NULL DEFAULT '',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `priority` int(11) NOT NULL DEFAULT '0',\n  `attrs` varchar(128) NOT NULL DEFAULT '',\n  `description` varchar(64) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dispatcher`\n--\n\nLOCK TABLES `dispatcher` WRITE;\n/*!40000 ALTER TABLE `dispatcher` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dispatcher` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `domain`\n--\n\nDROP TABLE IF EXISTS `domain`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `domain` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `domain` varchar(64) NOT NULL,\n  `did` varchar(64) DEFAULT NULL,\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `domain_idx` (`domain`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `domain`\n--\n\nLOCK TABLES `domain` WRITE;\n/*!40000 ALTER TABLE `domain` DISABLE KEYS */;\n/*!40000 ALTER TABLE `domain` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `domain_attrs`\n--\n\nDROP TABLE IF EXISTS `domain_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `domain_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `did` varchar(64) NOT NULL,\n  `name` varchar(32) NOT NULL,\n  `type` int(10) unsigned NOT NULL,\n  `value` varchar(255) NOT NULL,\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  KEY `domain_attrs_idx` (`did`,`name`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `domain_attrs`\n--\n\nLOCK TABLES `domain_attrs` WRITE;\n/*!40000 ALTER TABLE `domain_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `domain_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `domain_name`\n--\n\nDROP TABLE IF EXISTS `domain_name`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `domain_name` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `domain` varchar(64) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `domain_name`\n--\n\nLOCK TABLES `domain_name` WRITE;\n/*!40000 ALTER TABLE `domain_name` DISABLE KEYS */;\n/*!40000 ALTER TABLE `domain_name` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `domainpolicy`\n--\n\nDROP TABLE IF EXISTS `domainpolicy`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `domainpolicy` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `rule` varchar(255) NOT NULL,\n  `type` varchar(255) NOT NULL,\n  `att` varchar(255) DEFAULT NULL,\n  `val` varchar(128) DEFAULT NULL,\n  `description` varchar(255) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `rav_idx` (`rule`,`att`,`val`),\n  KEY `rule_idx` (`rule`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `domainpolicy`\n--\n\nLOCK TABLES `domainpolicy` WRITE;\n/*!40000 ALTER TABLE `domainpolicy` DISABLE KEYS */;\n/*!40000 ALTER TABLE `domainpolicy` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dr_gateways`\n--\n\nDROP TABLE IF EXISTS `dr_gateways`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dr_gateways` (\n  `gwid` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `type` int(11) unsigned NOT NULL DEFAULT '0',\n  `address` varchar(128) NOT NULL,\n  `strip` int(11) unsigned NOT NULL DEFAULT '0',\n  `pri_prefix` varchar(64) DEFAULT NULL,\n  `attrs` varchar(255) DEFAULT NULL,\n  `description` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`gwid`)\n) ENGINE=InnoDB AUTO_INCREMENT=64 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dr_gateways`\n--\n\nLOCK TABLES `dr_gateways` WRITE;\n/*!40000 ALTER TABLE `dr_gateways` DISABLE KEYS */;\nINSERT INTO `dr_gateways` VALUES (1,8,'52.41.52.34',0,'','','name:Skyetel North West Inbound,gwgroup:1'),(2,8,'52.8.201.128',0,'','','name:Skyetel South West Inbound,gwgroup:1'),(3,8,'52.60.138.31',0,'','','name:Skyetel North East Inbound,gwgroup:1'),(4,8,'50.17.48.216',0,'','','name:Skyetel South East Inbound,gwgroup:1'),(5,8,'35.156.192.164',0,'','','name:Skyetel Europe Inbound,gwgroup:1'),(6,8,'term.skyetel.com',0,'','','name:Skyetel 1st Priority Outbound Call,gwgroup:1'),(7,8,'52.41.52.34',0,'','','name:Skyetel 2nd Priority Outbound Call,gwgroup:1'),(8,8,'52.8.201.128',0,'','','name:Skyetel 3rd Priority Outbound Call,gwgroup:1'),(9,8,'50.17.48.216',0,'','','name:Skyetel 4rd Priority Outbound Call,gwgroup:1'),(10,8,'52.32.223.28',0,'','','name:Skyetel North West High Cost Outbound Traffic,gwgroup:1'),(11,8,'52.4.178.107',0,'','','name:Skyetel South East High Cost Outbound Traffic,gwgroup:1'),(12,8,'147.75.60.160',0,'','','name:Flowroute US-West-WA,gwgroup:2'),(13,8,'34.210.91.112',0,'','','name:Flowroute US-West-OR,gwgroup:2'),(14,8,'147.75.65.192',0,'','','name:Flowroute US-East-NJ,gwgroup:2'),(15,8,'34.226.36.32',0,'','','name:Flowroute US-East-VA,gwgroup:2'),(16,8,'81.201.82.45',0,'','','name:Voxbone Belgium,gwgroup:3'),(17,8,'81.201.84.195',0,'','','name:Voxbone LA,gwgroup:3'),(18,8,'81.201.85.45',0,'','','name:Voxbone NYC,gwgroup:3'),(19,8,'81.201.83.45',0,'','','name:Voxbone Germany,gwgroup:3'),(20,8,'81.201.86.45',0,'','','name:Voxbone Hong Kong,gwgroup:3'),(21,8,'81.201.84.195',0,'','','name:Voxbone Australia,gwgroup:3'),(22,8,'64.136.174.30',0,'','','name:VI Carrier,gwgroup:4'),(23,8,'64.136.173.22',0,'','','name:VI Carrier,gwgroup:4'),(24,8,'209.166.128.200',0,'','','name:VI Carrier,gwgroup:4'),(25,8,'192.240.151.100',0,'','','name:VI Carrier,gwgroup:4'),(26,8,'64.136.173.31',0,'','','name:VI Carrier,gwgroup:4'),(27,8,'64.136.174.30',0,'','','name:VI Carrier,gwgroup:4'),(28,8,'64.136.174.20',0,'','','name:VI Carrier,gwgroup:4'),(29,8,'209.166.154.70',0,'','','name:VI Carrier,gwgroup:4'),(30,8,'64.136.174.65',0,'','','name:VI Carrier,gwgroup:4'),(31,8,'64.136.173.23',0,'','','name:VI Carrier,gwgroup:4'),(32,8,'209.166.128.201',0,'','','name:VI Carrier,gwgroup:4'),(33,8,'192.240.151.101',0,'','','name:VI Carrier,gwgroup:4'),(34,8,'64.136.173.65',0,'','','name:VI Carrier,gwgroup:4'),(35,8,'64.136.174.65',0,'','','name:VI Carrier,gwgroup:4'),(36,8,'64.136.174.21',0,'','','name:VI Carrier,gwgroup:4'),(37,8,'209.166.154.71',0,'','','name:VI Carrier,gwgroup:4'),(38,8,'72.15.219.140',0,'','','name:Thinq Carrier,gwgroup:5'),(39,8,'216.147.191.157',0,'','','name:Voxtelesys Carrier,gwgroup:6'),(40,8,'64.34.181.47',0,'','','name:Les.net Carrier,gwgroup:7');\n/*!40000 ALTER TABLE `dr_gateways` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dr_groups`\n--\n\nDROP TABLE IF EXISTS `dr_groups`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dr_groups` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `domain` varchar(128) NOT NULL DEFAULT '',\n  `groupid` int(11) unsigned NOT NULL DEFAULT '0',\n  `description` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dr_groups`\n--\n\nLOCK TABLES `dr_groups` WRITE;\n/*!40000 ALTER TABLE `dr_groups` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dr_groups` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dr_gw_lists`\n--\n\nDROP TABLE IF EXISTS `dr_gw_lists`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dr_gw_lists` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `gwlist` varchar(255) NOT NULL,\n  `description` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dr_gw_lists`\n--\n\nLOCK TABLES `dr_gw_lists` WRITE;\n/*!40000 ALTER TABLE `dr_gw_lists` DISABLE KEYS */;\nINSERT INTO `dr_gw_lists` VALUES (1,'1,2,3,4,5,6,7,8,9,10,11','name:Skyetel CarrierGroup,type:8'),(2,'12,13,14,15','name:Flowroute CarrierGroup,type:8'),(3,'16,17,18,19,20,21','name:Voxbone CarrierGroup,type:8'),(4,'22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37','name:VI CarrierGroup,type:8'),(5,'38','name:Thinq CarrierGroup,type:8'),(6,'39','name:Voxtelesys CarrierGroup,type:8'),(7,'40','name:Les.net CarrierGroup,type:8');\n/*!40000 ALTER TABLE `dr_gw_lists` ENABLE KEYS */;\nUNLOCK TABLES;\n/*!50003 SET @saved_cs_client      = @@character_set_client */ ;\n/*!50003 SET @saved_cs_results     = @@character_set_results */ ;\n/*!50003 SET @saved_col_connection = @@collation_connection */ ;\n/*!50003 SET character_set_client  = utf8mb4 */ ;\n/*!50003 SET character_set_results = utf8mb4 */ ;\n/*!50003 SET collation_connection  = utf8mb4_general_ci */ ;\n/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;\n/*!50003 SET sql_mode              = 'NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;\nDELIMITER ;;\n/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`localhost`*/ /*!50003 TRIGGER insert_gw2gwgroup\n  AFTER INSERT\n  ON dr_gw_lists\n  FOR EACH ROW\nBEGIN\n\n  DECLARE num_gws int DEFAULT 0;\n  DECLARE gw_index int DEFAULT 1;\n\n  IF CHAR_LENGTH(NEW.gwlist) > 0 THEN\n    SET num_gws := (CHAR_LENGTH(NEW.gwlist) - CHAR_LENGTH(REPLACE(NEW.gwlist, ',', '')) + 1);\n\n\n    WHILE gw_index <= num_gws DO\n    INSERT IGNORE INTO dsip_gw2gwgroup\n    VALUES (SUBSTRING_INDEX(SUBSTRING_INDEX(new.gwlist, ',', gw_index), ',', -1), cast(new.id AS char(64)), DEFAULT,\n            DEFAULT);\n    SET gw_index := gw_index + 1;\n    END WHILE;\n  END IF;\n\nEND */;;\nDELIMITER ;\n/*!50003 SET sql_mode              = @saved_sql_mode */ ;\n/*!50003 SET character_set_client  = @saved_cs_client */ ;\n/*!50003 SET character_set_results = @saved_cs_results */ ;\n/*!50003 SET collation_connection  = @saved_col_connection */ ;\n/*!50003 SET @saved_cs_client      = @@character_set_client */ ;\n/*!50003 SET @saved_cs_results     = @@character_set_results */ ;\n/*!50003 SET @saved_col_connection = @@collation_connection */ ;\n/*!50003 SET character_set_client  = utf8mb4 */ ;\n/*!50003 SET character_set_results = utf8mb4 */ ;\n/*!50003 SET collation_connection  = utf8mb4_general_ci */ ;\n/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;\n/*!50003 SET sql_mode              = 'NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;\nDELIMITER ;;\n/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`localhost`*/ /*!50003 TRIGGER update_gw2gwgroup\n  AFTER UPDATE\n  ON dr_gw_lists\n  FOR EACH ROW\nBEGIN\n\n  DECLARE num_gws int DEFAULT 0;\n  DECLARE gw_index int DEFAULT 1;\n\n\n  IF NOT (NEW.gwlist <=> OLD.gwlist) THEN\n    DELETE FROM dsip_gw2gwgroup WHERE gwgroupid = cast(old.id AS char(64));\n\n    IF CHAR_LENGTH(NEW.gwlist) > 0 THEN\n      SET num_gws := (CHAR_LENGTH(NEW.gwlist) - CHAR_LENGTH(REPLACE(NEW.gwlist, ',', '')) + 1);\n\n\n      WHILE gw_index <= num_gws DO\n      INSERT IGNORE INTO dsip_gw2gwgroup\n      VALUES (SUBSTRING_INDEX(SUBSTRING_INDEX(new.gwlist, ',', gw_index), ',', -1), cast(new.id AS char(64)), DEFAULT,\n              DEFAULT);\n      SET gw_index := gw_index + 1;\n      END WHILE;\n    END IF;\n\n\n  ELSEIF NOT (NEW.id <=> OLD.id) THEN\n    UPDATE dsip_gw2gwgroup SET gwgroupid = cast(new.id AS char(64)) WHERE gwgroupid = cast(old.id AS char(64));\n  END IF;\n\nEND */;;\nDELIMITER ;\n/*!50003 SET sql_mode              = @saved_sql_mode */ ;\n/*!50003 SET character_set_client  = @saved_cs_client */ ;\n/*!50003 SET character_set_results = @saved_cs_results */ ;\n/*!50003 SET collation_connection  = @saved_col_connection */ ;\n/*!50003 SET @saved_cs_client      = @@character_set_client */ ;\n/*!50003 SET @saved_cs_results     = @@character_set_results */ ;\n/*!50003 SET @saved_col_connection = @@collation_connection */ ;\n/*!50003 SET character_set_client  = utf8mb4 */ ;\n/*!50003 SET character_set_results = utf8mb4 */ ;\n/*!50003 SET collation_connection  = utf8mb4_general_ci */ ;\n/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;\n/*!50003 SET sql_mode              = 'NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;\nDELIMITER ;;\n/*!50003 CREATE*/ /*!50017 DEFINER=`root`@`localhost`*/ /*!50003 TRIGGER delete_gw2gwgroup\n  AFTER DELETE\n  ON dr_gw_lists\n  FOR EACH ROW\nBEGIN\n\n  DELETE FROM dsip_gw2gwgroup WHERE gwgroupid = cast(old.id AS char(64));\n\nEND */;;\nDELIMITER ;\n/*!50003 SET sql_mode              = @saved_sql_mode */ ;\n/*!50003 SET character_set_client  = @saved_cs_client */ ;\n/*!50003 SET character_set_results = @saved_cs_results */ ;\n/*!50003 SET collation_connection  = @saved_col_connection */ ;\n\n--\n-- Table structure for table `dr_rules`\n--\n\nDROP TABLE IF EXISTS `dr_rules`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dr_rules` (\n  `ruleid` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `groupid` varchar(255) NOT NULL,\n  `prefix` varchar(64) NOT NULL,\n  `timerec` varchar(255) NOT NULL,\n  `priority` int(11) NOT NULL DEFAULT '0',\n  `routeid` varchar(64) NOT NULL,\n  `gwlist` varchar(255) NOT NULL,\n  `description` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`ruleid`)\n) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dr_rules`\n--\n\nLOCK TABLES `dr_rules` WRITE;\n/*!40000 ALTER TABLE `dr_rules` DISABLE KEYS */;\nINSERT INTO `dr_rules` VALUES (1,'8000','','',0,'','1,2','name:Default Outbound Route');\n/*!40000 ALTER TABLE `dr_rules` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_calllimit`\n--\n\nDROP TABLE IF EXISTS `dsip_calllimit`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_calllimit` (\n  `gwgroupid` varchar(64) NOT NULL,\n  `limit` varchar(64) NOT NULL DEFAULT '0',\n  `status` tinyint(1) NOT NULL DEFAULT '1',\n  `key_type` varchar(64) NOT NULL DEFAULT '0',\n  `value_type` varchar(64) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`gwgroupid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_calllimit`\n--\n\nLOCK TABLES `dsip_calllimit` WRITE;\n/*!40000 ALTER TABLE `dsip_calllimit` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_calllimit` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_domain_mapping`\n--\n\nDROP TABLE IF EXISTS `dsip_domain_mapping`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_domain_mapping` (\n  `id` int(10) NOT NULL AUTO_INCREMENT,\n  `pbx_id` int(10) NOT NULL,\n  `domain_id` int(10) NOT NULL,\n  `attr_list` varchar(255) NOT NULL,\n  `type` tinyint(3) NOT NULL DEFAULT '0',\n  `enabled` tinyint(1) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_domain_mapping`\n--\n\nLOCK TABLES `dsip_domain_mapping` WRITE;\n/*!40000 ALTER TABLE `dsip_domain_mapping` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_domain_mapping` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_endpoint_lease`\n--\n\nDROP TABLE IF EXISTS `dsip_endpoint_lease`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_endpoint_lease` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `gwid` int(10) unsigned NOT NULL,\n  `sid` int(10) unsigned NOT NULL,\n  `expiration` datetime NOT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_endpoint_lease`\n--\n\nLOCK TABLES `dsip_endpoint_lease` WRITE;\n/*!40000 ALTER TABLE `dsip_endpoint_lease` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_endpoint_lease` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_failfwd`\n--\n\nDROP TABLE IF EXISTS `dsip_failfwd`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_failfwd` (\n  `dr_ruleid` varchar(64) NOT NULL,\n  `did` varchar(64) NOT NULL,\n  `dr_groupid` varchar(64) NOT NULL,\n  `key_type` varchar(64) NOT NULL DEFAULT '0',\n  `value_type` varchar(64) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`dr_ruleid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_failfwd`\n--\n\nLOCK TABLES `dsip_failfwd` WRITE;\n/*!40000 ALTER TABLE `dsip_failfwd` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_failfwd` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_gw2gwgroup`\n--\n\nDROP TABLE IF EXISTS `dsip_gw2gwgroup`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_gw2gwgroup` (\n  `gwid` varchar(64) NOT NULL,\n  `gwgroupid` varchar(64) NOT NULL,\n  `key_type` varchar(64) NOT NULL DEFAULT '0',\n  `value_type` varchar(64) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`gwid`,`gwgroupid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_gw2gwgroup`\n--\n\nLOCK TABLES `dsip_gw2gwgroup` WRITE;\n/*!40000 ALTER TABLE `dsip_gw2gwgroup` DISABLE KEYS */;\nINSERT INTO `dsip_gw2gwgroup` VALUES ('1','1','0','0'),('10','1','0','0'),('11','1','0','0'),('12','2','0','0'),('13','2','0','0'),('14','2','0','0'),('15','2','0','0'),('16','3','0','0'),('17','3','0','0'),('18','3','0','0'),('19','3','0','0'),('2','1','0','0'),('20','3','0','0'),('21','3','0','0'),('22','4','0','0'),('23','4','0','0'),('24','4','0','0'),('25','4','0','0'),('26','4','0','0'),('27','4','0','0'),('28','4','0','0'),('29','4','0','0'),('3','1','0','0'),('30','4','0','0'),('31','4','0','0'),('32','4','0','0'),('33','4','0','0'),('34','4','0','0'),('35','4','0','0'),('36','4','0','0'),('37','4','0','0'),('38','5','0','0'),('39','6','0','0'),('4','1','0','0'),('40','7','0','0'),('5','1','0','0'),('6','1','0','0'),('7','1','0','0'),('8','1','0','0'),('9','1','0','0');\n/*!40000 ALTER TABLE `dsip_gw2gwgroup` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_hardfwd`\n--\n\nDROP TABLE IF EXISTS `dsip_hardfwd`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_hardfwd` (\n  `dr_ruleid` varchar(64) NOT NULL,\n  `did` varchar(64) NOT NULL,\n  `dr_groupid` varchar(64) NOT NULL,\n  `key_type` varchar(64) NOT NULL DEFAULT '0',\n  `value_type` varchar(64) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`dr_ruleid`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_hardfwd`\n--\n\nLOCK TABLES `dsip_hardfwd` WRITE;\n/*!40000 ALTER TABLE `dsip_hardfwd` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_hardfwd` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_lcr`\n--\n\nDROP TABLE IF EXISTS `dsip_lcr`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_lcr` (\n  `pattern` varchar(64) NOT NULL DEFAULT '',\n  `key_type` varchar(64) NOT NULL DEFAULT '0',\n  `dr_groupid` varchar(64) NOT NULL DEFAULT '',\n  `value_type` varchar(64) NOT NULL DEFAULT '0',\n  `cost` decimal(3,2) NOT NULL DEFAULT '0.00',\n  `from_prefix` varchar(64) NOT NULL DEFAULT '',\n  `expires` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`pattern`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_lcr`\n--\n\nLOCK TABLES `dsip_lcr` WRITE;\n/*!40000 ALTER TABLE `dsip_lcr` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_lcr` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_maintmode`\n--\n\nDROP TABLE IF EXISTS `dsip_maintmode`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_maintmode` (\n  `ipaddr` varchar(64) NOT NULL DEFAULT '',\n  `key_type` varchar(64) NOT NULL DEFAULT '0',\n  `gwid` varchar(64) NOT NULL DEFAULT '',\n  `value_type` varchar(64) NOT NULL DEFAULT '0',\n  `status` tinyint(4) NOT NULL DEFAULT '1',\n  PRIMARY KEY (`ipaddr`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_maintmode`\n--\n\nLOCK TABLES `dsip_maintmode` WRITE;\n/*!40000 ALTER TABLE `dsip_maintmode` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_maintmode` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_multidomain_mapping`\n--\n\nDROP TABLE IF EXISTS `dsip_multidomain_mapping`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_multidomain_mapping` (\n  `id` int(10) NOT NULL AUTO_INCREMENT,\n  `pbx_id` int(10) NOT NULL,\n  `db_host` varchar(20) NOT NULL,\n  `db_username` varchar(40) NOT NULL,\n  `db_password` varchar(40) NOT NULL,\n  `domain_list` varchar(255) NOT NULL DEFAULT '',\n  `domain_list_hash` varchar(255) NOT NULL DEFAULT '',\n  `attr_list` varchar(255) NOT NULL DEFAULT '',\n  `type` tinyint(3) NOT NULL DEFAULT '0',\n  `enabled` tinyint(1) NOT NULL DEFAULT '0',\n  `lastsync` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n  `syncstatus` tinyint(1) NOT NULL DEFAULT '0',\n  `syncerror` varchar(200) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_multidomain_mapping`\n--\n\nLOCK TABLES `dsip_multidomain_mapping` WRITE;\n/*!40000 ALTER TABLE `dsip_multidomain_mapping` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_multidomain_mapping` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dsip_notification`\n--\n\nDROP TABLE IF EXISTS `dsip_notification`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_notification` (\n  `gwgroupid` int(11) NOT NULL,\n  `type` int(11) NOT NULL,\n  `method` int(11) DEFAULT NULL,\n  `value` varchar(255) DEFAULT NULL,\n  `createdate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n  PRIMARY KEY (`gwgroupid`,`type`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_notification`\n--\n\nLOCK TABLES `dsip_notification` WRITE;\n/*!40000 ALTER TABLE `dsip_notification` DISABLE KEYS */;\n/*!40000 ALTER TABLE `dsip_notification` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Temporary table structure for view `dsip_prefix_mapping`\n--\n\nDROP TABLE IF EXISTS `dsip_prefix_mapping`;\n/*!50001 DROP VIEW IF EXISTS `dsip_prefix_mapping`*/;\nSET @saved_cs_client     = @@character_set_client;\nSET character_set_client = utf8;\n/*!50001 CREATE TABLE `dsip_prefix_mapping` (\n  `prefix` tinyint NOT NULL,\n  `ruleid` tinyint NOT NULL,\n  `key_type` tinyint NOT NULL,\n  `value_type` tinyint NOT NULL\n) ENGINE=MyISAM */;\nSET character_set_client = @saved_cs_client;\n\n--\n-- Table structure for table `dsip_settings`\n--\n\nDROP TABLE IF EXISTS `dsip_settings`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `dsip_settings` (\n  `DSIP_ID` int(10) unsigned NOT NULL DEFAULT '1',\n  `DSIP_PROTO` varchar(255) NOT NULL DEFAULT 'http',\n  `DSIP_HOST` varchar(255) NOT NULL DEFAULT '0.0.0.0',\n  `DSIP_PORT` int(11) NOT NULL DEFAULT '5000',\n  `DSIP_USERNAME` varchar(255) NOT NULL DEFAULT 'admin',\n  `DSIP_PASSWORD` binary(128) NOT NULL,\n  `DSIP_SALT` binary(128) NOT NULL,\n  `DSIP_API_PROTO` varchar(255) NOT NULL DEFAULT 'http',\n  `DSIP_API_HOST` varchar(255) NOT NULL DEFAULT '127.0.0.1',\n  `DSIP_API_PORT` int(11) NOT NULL DEFAULT '5000',\n  `DSIP_API_TOKEN` varbinary(160) NOT NULL,\n  `DSIP_LOG_LEVEL` int(11) NOT NULL DEFAULT '3',\n  `DSIP_LOG_FACILITY` int(11) NOT NULL DEFAULT '18',\n  `DSIP_SSL_KEY` varchar(255) NOT NULL DEFAULT '',\n  `DSIP_SSL_CERT` varchar(255) NOT NULL DEFAULT '',\n  `DSIP_SSL_EMAIL` varchar(255) NOT NULL DEFAULT '',\n  `VERSION` varchar(255) NOT NULL DEFAULT '0.523+ent',\n  `DEBUG` tinyint(1) NOT NULL DEFAULT '0',\n  `ROLE` varchar(255) NOT NULL DEFAULT '',\n  `KAM_DB_HOST` varchar(255) NOT NULL DEFAULT 'localhost',\n  `KAM_DB_DRIVER` varchar(255) NOT NULL DEFAULT '',\n  `KAM_DB_TYPE` varchar(255) NOT NULL DEFAULT 'mysql',\n  `KAM_DB_PORT` varchar(255) NOT NULL DEFAULT '3306',\n  `KAM_DB_NAME` varchar(255) NOT NULL DEFAULT 'kamailio',\n  `KAM_DB_USER` varchar(255) NOT NULL DEFAULT 'kamailio',\n  `KAM_DB_PASS` varbinary(160) NOT NULL,\n  `KAM_KAMCMD_PATH` varchar(255) NOT NULL DEFAULT '/usr/sbin/kamcmd',\n  `KAM_CFG_PATH` varchar(255) NOT NULL DEFAULT '/etc/kamailio/kamailio.cfg',\n  `RTP_CFG_PATH` varchar(255) NOT NULL DEFAULT '/etc/kamailio/kamailio.cfg',\n  `FLT_CARRIER` int(11) NOT NULL DEFAULT '8',\n  `FLT_PBX` int(11) NOT NULL DEFAULT '9',\n  `FLT_OUTBOUND` int(11) NOT NULL DEFAULT '8000',\n  `FLT_INBOUND` int(11) NOT NULL DEFAULT '9000',\n  `FLT_LCR_MIN` int(11) NOT NULL DEFAULT '10000',\n  `FLT_FWD_MIN` int(11) NOT NULL DEFAULT '20000',\n  `DEFAULT_AUTH_DOMAIN` varchar(255) NOT NULL DEFAULT 'sip.dsiprouter.org',\n  `TELEBLOCK_GW_ENABLED` int(11) NOT NULL DEFAULT '0',\n  `TELEBLOCK_GW_IP` varchar(255) NOT NULL DEFAULT '62.34.24.22',\n  `TELEBLOCK_GW_PORT` varchar(255) NOT NULL DEFAULT '5066',\n  `TELEBLOCK_MEDIA_IP` varchar(255) NOT NULL DEFAULT '',\n  `TELEBLOCK_MEDIA_PORT` varchar(255) NOT NULL DEFAULT '',\n  `FLOWROUTE_ACCESS_KEY` varchar(255) NOT NULL DEFAULT '',\n  `FLOWROUTE_SECRET_KEY` varchar(255) NOT NULL DEFAULT '',\n  `FLOWROUTE_API_ROOT_URL` varchar(255) NOT NULL DEFAULT 'https://api.flowroute.com/v2',\n  `UPLOAD_FOLDER` varchar(255) NOT NULL DEFAULT '/tmp',\n  `CLOUD_PLATFORM` varchar(255) NOT NULL DEFAULT '',\n  `MAIL_SERVER` varchar(255) NOT NULL DEFAULT 'smtp.gmail.com',\n  `MAIL_PORT` int(11) NOT NULL DEFAULT '587',\n  `MAIL_USE_TLS` tinyint(1) NOT NULL DEFAULT '1',\n  `MAIL_USERNAME` varchar(255) NOT NULL DEFAULT '',\n  `MAIL_PASSWORD` varbinary(160) NOT NULL,\n  `MAIL_ASCII_ATTACHMENTS` tinyint(1) NOT NULL DEFAULT '0',\n  `MAIL_DEFAULT_SENDER` varchar(255) NOT NULL DEFAULT 'DoNotReply@smtp.gmail.com',\n  `MAIL_DEFAULT_SUBJECT` varchar(255) NOT NULL DEFAULT 'dSIPRouter System Notification',\n  PRIMARY KEY (`DSIP_ID`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1 MIN_ROWS=1 MAX_ROWS=1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dsip_settings`\n--\n\nLOCK TABLES `dsip_settings` WRITE;\n/*!40000 ALTER TABLE `dsip_settings` DISABLE KEYS */;\nINSERT INTO `dsip_settings` VALUES (1,'http','0.0.0.0',5000,'admin','97652880df95b6fc581f24d5f61189bbc40d9684da22efce32f4161140001bc78f6dceb3e3d95630a420d0fc8388c9fc9914830fff3777792da0c3a4467893c8','f8d5442c876afda0cf72c7f0e29b7815325c94a712788bb570857c28d8132ac99ffead3c6a01667e12f9463d380888e6567aa0f30f9878e2fc68d0b968d25425','http','127.0.0.1',5000,'accf0cc8bffbc746dc4b5a03ec4174dea0818a6eb7e7aa26b799b0c1c1ea8f29',3,18,'','','','0.60+ent',0,'','localhost','','mysql','3306','kamailio','kamailio','d67225cd123dffb2cd4acf6a49a53d55','/usr/sbin/kamcmd','/etc/kamailio/kamailio.cfg','/etc/kamailio/kamailio.cfg',0,0,8,9,8000,9000,10000,20000,'sip.dsiprouter.org',0,'62.34.24.22','5066','','','','','https://api.flowroute.com/v2','/tmp','','smtp.gmail.com',587,1,'','',0,'DoNotReply@smtp.gmail.com','dSIPRouter System Notification');\n/*!40000 ALTER TABLE `dsip_settings` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `globalblacklist`\n--\n\nDROP TABLE IF EXISTS `globalblacklist`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `globalblacklist` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `prefix` varchar(64) NOT NULL DEFAULT '',\n  `whitelist` tinyint(1) NOT NULL DEFAULT '0',\n  `description` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `globalblacklist_idx` (`prefix`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `globalblacklist`\n--\n\nLOCK TABLES `globalblacklist` WRITE;\n/*!40000 ALTER TABLE `globalblacklist` DISABLE KEYS */;\n/*!40000 ALTER TABLE `globalblacklist` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `grp`\n--\n\nDROP TABLE IF EXISTS `grp`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `grp` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `grp` varchar(64) NOT NULL DEFAULT '',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `account_group_idx` (`username`,`domain`,`grp`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `grp`\n--\n\nLOCK TABLES `grp` WRITE;\n/*!40000 ALTER TABLE `grp` DISABLE KEYS */;\n/*!40000 ALTER TABLE `grp` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `htable`\n--\n\nDROP TABLE IF EXISTS `htable`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `htable` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `key_name` varchar(64) NOT NULL DEFAULT '',\n  `key_type` int(11) NOT NULL DEFAULT '0',\n  `value_type` int(11) NOT NULL DEFAULT '0',\n  `key_value` varchar(128) NOT NULL DEFAULT '',\n  `expires` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `htable`\n--\n\nLOCK TABLES `htable` WRITE;\n/*!40000 ALTER TABLE `htable` DISABLE KEYS */;\n/*!40000 ALTER TABLE `htable` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `imc_members`\n--\n\nDROP TABLE IF EXISTS `imc_members`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `imc_members` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL,\n  `room` varchar(64) NOT NULL,\n  `flag` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `account_room_idx` (`username`,`domain`,`room`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `imc_members`\n--\n\nLOCK TABLES `imc_members` WRITE;\n/*!40000 ALTER TABLE `imc_members` DISABLE KEYS */;\n/*!40000 ALTER TABLE `imc_members` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `imc_rooms`\n--\n\nDROP TABLE IF EXISTS `imc_rooms`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `imc_rooms` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `name` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL,\n  `flag` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `name_domain_idx` (`name`,`domain`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `imc_rooms`\n--\n\nLOCK TABLES `imc_rooms` WRITE;\n/*!40000 ALTER TABLE `imc_rooms` DISABLE KEYS */;\n/*!40000 ALTER TABLE `imc_rooms` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `lcr_gw`\n--\n\nDROP TABLE IF EXISTS `lcr_gw`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `lcr_gw` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `lcr_id` smallint(5) unsigned NOT NULL,\n  `gw_name` varchar(128) DEFAULT NULL,\n  `ip_addr` varchar(50) DEFAULT NULL,\n  `hostname` varchar(64) DEFAULT NULL,\n  `port` smallint(5) unsigned DEFAULT NULL,\n  `params` varchar(64) DEFAULT NULL,\n  `uri_scheme` tinyint(3) unsigned DEFAULT NULL,\n  `transport` tinyint(3) unsigned DEFAULT NULL,\n  `strip` tinyint(3) unsigned DEFAULT NULL,\n  `prefix` varchar(16) DEFAULT NULL,\n  `tag` varchar(64) DEFAULT NULL,\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  `defunct` int(10) unsigned DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `lcr_id_idx` (`lcr_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `lcr_gw`\n--\n\nLOCK TABLES `lcr_gw` WRITE;\n/*!40000 ALTER TABLE `lcr_gw` DISABLE KEYS */;\n/*!40000 ALTER TABLE `lcr_gw` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `lcr_rule`\n--\n\nDROP TABLE IF EXISTS `lcr_rule`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `lcr_rule` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `lcr_id` smallint(5) unsigned NOT NULL,\n  `prefix` varchar(16) DEFAULT NULL,\n  `from_uri` varchar(64) DEFAULT NULL,\n  `request_uri` varchar(64) DEFAULT NULL,\n  `mt_tvalue` varchar(128) DEFAULT NULL,\n  `stopper` int(10) unsigned NOT NULL DEFAULT '0',\n  `enabled` int(10) unsigned NOT NULL DEFAULT '1',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `lcr_id_prefix_from_uri_idx` (`lcr_id`,`prefix`,`from_uri`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `lcr_rule`\n--\n\nLOCK TABLES `lcr_rule` WRITE;\n/*!40000 ALTER TABLE `lcr_rule` DISABLE KEYS */;\n/*!40000 ALTER TABLE `lcr_rule` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `lcr_rule_target`\n--\n\nDROP TABLE IF EXISTS `lcr_rule_target`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `lcr_rule_target` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `lcr_id` smallint(5) unsigned NOT NULL,\n  `rule_id` int(10) unsigned NOT NULL,\n  `gw_id` int(10) unsigned NOT NULL,\n  `priority` tinyint(3) unsigned NOT NULL,\n  `weight` int(10) unsigned NOT NULL DEFAULT '1',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `rule_id_gw_id_idx` (`rule_id`,`gw_id`),\n  KEY `lcr_id_idx` (`lcr_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `lcr_rule_target`\n--\n\nLOCK TABLES `lcr_rule_target` WRITE;\n/*!40000 ALTER TABLE `lcr_rule_target` DISABLE KEYS */;\n/*!40000 ALTER TABLE `lcr_rule_target` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `locale_lookup`\n--\n\nDROP TABLE IF EXISTS `locale_lookup`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `locale_lookup` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `locale` varchar(64) NOT NULL DEFAULT '',\n  `fprefix` varchar(64) NOT NULL DEFAULT '0',\n  `tprefix` varchar(64) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=latin1;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `locale_lookup`\n--\n\nLOCK TABLES `locale_lookup` WRITE;\n/*!40000 ALTER TABLE `locale_lookup` DISABLE KEYS */;\n/*!40000 ALTER TABLE `locale_lookup` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `location`\n--\n\nDROP TABLE IF EXISTS `location`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `location` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `ruid` varchar(64) NOT NULL DEFAULT '',\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) DEFAULT NULL,\n  `contact` varchar(512) NOT NULL DEFAULT '',\n  `received` varchar(128) DEFAULT NULL,\n  `path` varchar(512) DEFAULT NULL,\n  `expires` datetime NOT NULL DEFAULT '2030-05-28 21:32:15',\n  `q` float(10,2) NOT NULL DEFAULT '1.00',\n  `callid` varchar(255) NOT NULL DEFAULT 'Default-Call-ID',\n  `cseq` int(11) NOT NULL DEFAULT '1',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `cflags` int(11) NOT NULL DEFAULT '0',\n  `user_agent` varchar(255) NOT NULL DEFAULT '',\n  `socket` varchar(64) DEFAULT NULL,\n  `methods` int(11) DEFAULT NULL,\n  `instance` varchar(255) DEFAULT NULL,\n  `reg_id` int(11) NOT NULL DEFAULT '0',\n  `server_id` int(11) NOT NULL DEFAULT '0',\n  `connection_id` int(11) NOT NULL DEFAULT '0',\n  `keepalive` int(11) NOT NULL DEFAULT '0',\n  `partition` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `ruid_idx` (`ruid`),\n  KEY `account_contact_idx` (`username`,`domain`,`contact`(255)),\n  KEY `expires_idx` (`expires`),\n  KEY `connection_idx` (`server_id`,`connection_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `location`\n--\n\nLOCK TABLES `location` WRITE;\n/*!40000 ALTER TABLE `location` DISABLE KEYS */;\n/*!40000 ALTER TABLE `location` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `location_attrs`\n--\n\nDROP TABLE IF EXISTS `location_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `location_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `ruid` varchar(64) NOT NULL DEFAULT '',\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) DEFAULT NULL,\n  `aname` varchar(64) NOT NULL DEFAULT '',\n  `atype` int(11) NOT NULL DEFAULT '0',\n  `avalue` varchar(255) NOT NULL DEFAULT '',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  KEY `account_record_idx` (`username`,`domain`,`ruid`),\n  KEY `last_modified_idx` (`last_modified`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `location_attrs`\n--\n\nLOCK TABLES `location_attrs` WRITE;\n/*!40000 ALTER TABLE `location_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `location_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `missed_calls`\n--\n\nDROP TABLE IF EXISTS `missed_calls`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `missed_calls` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `method` varchar(16) NOT NULL DEFAULT '',\n  `from_tag` varchar(64) NOT NULL DEFAULT '',\n  `to_tag` varchar(64) NOT NULL DEFAULT '',\n  `callid` varchar(255) NOT NULL DEFAULT '',\n  `sip_code` varchar(3) NOT NULL DEFAULT '',\n  `sip_reason` varchar(128) NOT NULL DEFAULT '',\n  `time` datetime NOT NULL,\n  PRIMARY KEY (`id`),\n  KEY `callid_idx` (`callid`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `missed_calls`\n--\n\nLOCK TABLES `missed_calls` WRITE;\n/*!40000 ALTER TABLE `missed_calls` DISABLE KEYS */;\n/*!40000 ALTER TABLE `missed_calls` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `mohqcalls`\n--\n\nDROP TABLE IF EXISTS `mohqcalls`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `mohqcalls` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `mohq_id` int(10) unsigned NOT NULL,\n  `call_id` varchar(100) NOT NULL,\n  `call_status` int(10) unsigned NOT NULL,\n  `call_from` varchar(100) NOT NULL,\n  `call_contact` varchar(100) DEFAULT NULL,\n  `call_time` datetime NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `mohqcalls_idx` (`call_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `mohqcalls`\n--\n\nLOCK TABLES `mohqcalls` WRITE;\n/*!40000 ALTER TABLE `mohqcalls` DISABLE KEYS */;\n/*!40000 ALTER TABLE `mohqcalls` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `mohqueues`\n--\n\nDROP TABLE IF EXISTS `mohqueues`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `mohqueues` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `name` varchar(25) NOT NULL,\n  `uri` varchar(100) NOT NULL,\n  `mohdir` varchar(100) DEFAULT NULL,\n  `mohfile` varchar(100) NOT NULL,\n  `debug` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `mohqueue_uri_idx` (`uri`),\n  UNIQUE KEY `mohqueue_name_idx` (`name`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `mohqueues`\n--\n\nLOCK TABLES `mohqueues` WRITE;\n/*!40000 ALTER TABLE `mohqueues` DISABLE KEYS */;\n/*!40000 ALTER TABLE `mohqueues` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `mtree`\n--\n\nDROP TABLE IF EXISTS `mtree`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `mtree` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `tprefix` varchar(32) NOT NULL DEFAULT '',\n  `tvalue` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `tprefix_idx` (`tprefix`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `mtree`\n--\n\nLOCK TABLES `mtree` WRITE;\n/*!40000 ALTER TABLE `mtree` DISABLE KEYS */;\n/*!40000 ALTER TABLE `mtree` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `mtrees`\n--\n\nDROP TABLE IF EXISTS `mtrees`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `mtrees` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `tname` varchar(128) NOT NULL DEFAULT '',\n  `tprefix` varchar(32) NOT NULL DEFAULT '',\n  `tvalue` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `tname_tprefix_tvalue_idx` (`tname`,`tprefix`,`tvalue`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `mtrees`\n--\n\nLOCK TABLES `mtrees` WRITE;\n/*!40000 ALTER TABLE `mtrees` DISABLE KEYS */;\n/*!40000 ALTER TABLE `mtrees` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `pdt`\n--\n\nDROP TABLE IF EXISTS `pdt`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `pdt` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `sdomain` varchar(128) NOT NULL,\n  `prefix` varchar(32) NOT NULL,\n  `domain` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `sdomain_prefix_idx` (`sdomain`,`prefix`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `pdt`\n--\n\nLOCK TABLES `pdt` WRITE;\n/*!40000 ALTER TABLE `pdt` DISABLE KEYS */;\n/*!40000 ALTER TABLE `pdt` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `pl_pipes`\n--\n\nDROP TABLE IF EXISTS `pl_pipes`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `pl_pipes` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `pipeid` varchar(64) NOT NULL DEFAULT '',\n  `algorithm` varchar(32) NOT NULL DEFAULT '',\n  `plimit` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `pl_pipes`\n--\n\nLOCK TABLES `pl_pipes` WRITE;\n/*!40000 ALTER TABLE `pl_pipes` DISABLE KEYS */;\n/*!40000 ALTER TABLE `pl_pipes` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `presentity`\n--\n\nDROP TABLE IF EXISTS `presentity`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `presentity` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL,\n  `event` varchar(64) NOT NULL,\n  `etag` varchar(64) NOT NULL,\n  `expires` int(11) NOT NULL,\n  `received_time` int(11) NOT NULL,\n  `body` blob NOT NULL,\n  `sender` varchar(128) NOT NULL,\n  `priority` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `presentity_idx` (`username`,`domain`,`event`,`etag`),\n  KEY `presentity_expires` (`expires`),\n  KEY `account_idx` (`username`,`domain`,`event`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `presentity`\n--\n\nLOCK TABLES `presentity` WRITE;\n/*!40000 ALTER TABLE `presentity` DISABLE KEYS */;\n/*!40000 ALTER TABLE `presentity` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `pua`\n--\n\nDROP TABLE IF EXISTS `pua`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `pua` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `pres_uri` varchar(128) NOT NULL,\n  `pres_id` varchar(255) NOT NULL,\n  `event` int(11) NOT NULL,\n  `expires` int(11) NOT NULL,\n  `desired_expires` int(11) NOT NULL,\n  `flag` int(11) NOT NULL,\n  `etag` varchar(64) NOT NULL,\n  `tuple_id` varchar(64) DEFAULT NULL,\n  `watcher_uri` varchar(128) NOT NULL,\n  `call_id` varchar(255) NOT NULL,\n  `to_tag` varchar(64) NOT NULL,\n  `from_tag` varchar(64) NOT NULL,\n  `cseq` int(11) NOT NULL,\n  `record_route` text,\n  `contact` varchar(128) NOT NULL,\n  `remote_contact` varchar(128) NOT NULL,\n  `version` int(11) NOT NULL,\n  `extra_headers` text NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `pua_idx` (`etag`,`tuple_id`,`call_id`,`from_tag`),\n  KEY `expires_idx` (`expires`),\n  KEY `dialog1_idx` (`pres_id`,`pres_uri`),\n  KEY `dialog2_idx` (`call_id`,`from_tag`),\n  KEY `record_idx` (`pres_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `pua`\n--\n\nLOCK TABLES `pua` WRITE;\n/*!40000 ALTER TABLE `pua` DISABLE KEYS */;\n/*!40000 ALTER TABLE `pua` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `purplemap`\n--\n\nDROP TABLE IF EXISTS `purplemap`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `purplemap` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `sip_user` varchar(128) NOT NULL,\n  `ext_user` varchar(128) NOT NULL,\n  `ext_prot` varchar(16) NOT NULL,\n  `ext_pass` varchar(64) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `purplemap`\n--\n\nLOCK TABLES `purplemap` WRITE;\n/*!40000 ALTER TABLE `purplemap` DISABLE KEYS */;\n/*!40000 ALTER TABLE `purplemap` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `re_grp`\n--\n\nDROP TABLE IF EXISTS `re_grp`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `re_grp` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `reg_exp` varchar(128) NOT NULL DEFAULT '',\n  `group_id` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  KEY `group_idx` (`group_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `re_grp`\n--\n\nLOCK TABLES `re_grp` WRITE;\n/*!40000 ALTER TABLE `re_grp` DISABLE KEYS */;\n/*!40000 ALTER TABLE `re_grp` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `rls_presentity`\n--\n\nDROP TABLE IF EXISTS `rls_presentity`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `rls_presentity` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `rlsubs_did` varchar(255) NOT NULL,\n  `resource_uri` varchar(128) NOT NULL,\n  `content_type` varchar(255) NOT NULL,\n  `presence_state` blob NOT NULL,\n  `expires` int(11) NOT NULL,\n  `updated` int(11) NOT NULL,\n  `auth_state` int(11) NOT NULL,\n  `reason` varchar(64) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `rls_presentity_idx` (`rlsubs_did`,`resource_uri`),\n  KEY `rlsubs_idx` (`rlsubs_did`),\n  KEY `updated_idx` (`updated`),\n  KEY `expires_idx` (`expires`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `rls_presentity`\n--\n\nLOCK TABLES `rls_presentity` WRITE;\n/*!40000 ALTER TABLE `rls_presentity` DISABLE KEYS */;\n/*!40000 ALTER TABLE `rls_presentity` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `rls_watchers`\n--\n\nDROP TABLE IF EXISTS `rls_watchers`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `rls_watchers` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `presentity_uri` varchar(128) NOT NULL,\n  `to_user` varchar(64) NOT NULL,\n  `to_domain` varchar(64) NOT NULL,\n  `watcher_username` varchar(64) NOT NULL,\n  `watcher_domain` varchar(64) NOT NULL,\n  `event` varchar(64) NOT NULL DEFAULT 'presence',\n  `event_id` varchar(64) DEFAULT NULL,\n  `to_tag` varchar(64) NOT NULL,\n  `from_tag` varchar(64) NOT NULL,\n  `callid` varchar(255) NOT NULL,\n  `local_cseq` int(11) NOT NULL,\n  `remote_cseq` int(11) NOT NULL,\n  `contact` varchar(128) NOT NULL,\n  `record_route` text,\n  `expires` int(11) NOT NULL,\n  `status` int(11) NOT NULL DEFAULT '2',\n  `reason` varchar(64) NOT NULL,\n  `version` int(11) NOT NULL DEFAULT '0',\n  `socket_info` varchar(64) NOT NULL,\n  `local_contact` varchar(128) NOT NULL,\n  `from_user` varchar(64) NOT NULL,\n  `from_domain` varchar(64) NOT NULL,\n  `updated` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `rls_watcher_idx` (`callid`,`to_tag`,`from_tag`),\n  KEY `rls_watchers_update` (`watcher_username`,`watcher_domain`,`event`),\n  KEY `rls_watchers_expires` (`expires`),\n  KEY `updated_idx` (`updated`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `rls_watchers`\n--\n\nLOCK TABLES `rls_watchers` WRITE;\n/*!40000 ALTER TABLE `rls_watchers` DISABLE KEYS */;\n/*!40000 ALTER TABLE `rls_watchers` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `rtpengine`\n--\n\nDROP TABLE IF EXISTS `rtpengine`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `rtpengine` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `setid` int(10) unsigned NOT NULL DEFAULT '0',\n  `url` varchar(64) NOT NULL,\n  `weight` int(10) unsigned NOT NULL DEFAULT '1',\n  `disabled` int(1) NOT NULL DEFAULT '0',\n  `stamp` datetime NOT NULL DEFAULT '1900-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `rtpengine_nodes` (`setid`,`url`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `rtpengine`\n--\n\nLOCK TABLES `rtpengine` WRITE;\n/*!40000 ALTER TABLE `rtpengine` DISABLE KEYS */;\n/*!40000 ALTER TABLE `rtpengine` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `rtpproxy`\n--\n\nDROP TABLE IF EXISTS `rtpproxy`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `rtpproxy` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `setid` varchar(32) NOT NULL DEFAULT '0',\n  `url` varchar(64) NOT NULL DEFAULT '',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `weight` int(11) NOT NULL DEFAULT '1',\n  `description` varchar(64) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `rtpproxy`\n--\n\nLOCK TABLES `rtpproxy` WRITE;\n/*!40000 ALTER TABLE `rtpproxy` DISABLE KEYS */;\n/*!40000 ALTER TABLE `rtpproxy` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `sca_subscriptions`\n--\n\nDROP TABLE IF EXISTS `sca_subscriptions`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `sca_subscriptions` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `subscriber` varchar(255) NOT NULL,\n  `aor` varchar(255) NOT NULL,\n  `event` int(11) NOT NULL DEFAULT '0',\n  `expires` int(11) NOT NULL DEFAULT '0',\n  `state` int(11) NOT NULL DEFAULT '0',\n  `app_idx` int(11) NOT NULL DEFAULT '0',\n  `call_id` varchar(255) NOT NULL,\n  `from_tag` varchar(64) NOT NULL,\n  `to_tag` varchar(64) NOT NULL,\n  `record_route` text,\n  `notify_cseq` int(11) NOT NULL,\n  `subscribe_cseq` int(11) NOT NULL,\n  `server_id` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `sca_subscriptions_idx` (`subscriber`,`call_id`,`from_tag`,`to_tag`),\n  KEY `sca_expires_idx` (`server_id`,`expires`),\n  KEY `sca_subscribers_idx` (`subscriber`,`event`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `sca_subscriptions`\n--\n\nLOCK TABLES `sca_subscriptions` WRITE;\n/*!40000 ALTER TABLE `sca_subscriptions` DISABLE KEYS */;\n/*!40000 ALTER TABLE `sca_subscriptions` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `silo`\n--\n\nDROP TABLE IF EXISTS `silo`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `silo` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `src_addr` varchar(128) NOT NULL DEFAULT '',\n  `dst_addr` varchar(128) NOT NULL DEFAULT '',\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `inc_time` int(11) NOT NULL DEFAULT '0',\n  `exp_time` int(11) NOT NULL DEFAULT '0',\n  `snd_time` int(11) NOT NULL DEFAULT '0',\n  `ctype` varchar(32) NOT NULL DEFAULT 'text/plain',\n  `body` blob,\n  `extra_hdrs` text,\n  `callid` varchar(128) NOT NULL DEFAULT '',\n  `status` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  KEY `account_idx` (`username`,`domain`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `silo`\n--\n\nLOCK TABLES `silo` WRITE;\n/*!40000 ALTER TABLE `silo` DISABLE KEYS */;\n/*!40000 ALTER TABLE `silo` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `sip_trace`\n--\n\nDROP TABLE IF EXISTS `sip_trace`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `sip_trace` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `time_stamp` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  `time_us` int(10) unsigned NOT NULL DEFAULT '0',\n  `callid` varchar(255) NOT NULL DEFAULT '',\n  `traced_user` varchar(128) NOT NULL DEFAULT '',\n  `msg` mediumtext NOT NULL,\n  `method` varchar(50) NOT NULL DEFAULT '',\n  `status` varchar(128) NOT NULL DEFAULT '',\n  `fromip` varchar(50) NOT NULL DEFAULT '',\n  `toip` varchar(50) NOT NULL DEFAULT '',\n  `fromtag` varchar(64) NOT NULL DEFAULT '',\n  `totag` varchar(64) NOT NULL DEFAULT '',\n  `direction` varchar(4) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  KEY `traced_user_idx` (`traced_user`),\n  KEY `date_idx` (`time_stamp`),\n  KEY `fromip_idx` (`fromip`),\n  KEY `callid_idx` (`callid`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `sip_trace`\n--\n\nLOCK TABLES `sip_trace` WRITE;\n/*!40000 ALTER TABLE `sip_trace` DISABLE KEYS */;\n/*!40000 ALTER TABLE `sip_trace` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `speed_dial`\n--\n\nDROP TABLE IF EXISTS `speed_dial`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `speed_dial` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `sd_username` varchar(64) NOT NULL DEFAULT '',\n  `sd_domain` varchar(64) NOT NULL DEFAULT '',\n  `new_uri` varchar(128) NOT NULL DEFAULT '',\n  `fname` varchar(64) NOT NULL DEFAULT '',\n  `lname` varchar(64) NOT NULL DEFAULT '',\n  `description` varchar(64) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `speed_dial_idx` (`username`,`domain`,`sd_domain`,`sd_username`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `speed_dial`\n--\n\nLOCK TABLES `speed_dial` WRITE;\n/*!40000 ALTER TABLE `speed_dial` DISABLE KEYS */;\n/*!40000 ALTER TABLE `speed_dial` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `subscriber`\n--\n\nDROP TABLE IF EXISTS `subscriber`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `subscriber` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `password` varchar(64) NOT NULL DEFAULT '',\n  `ha1` varchar(128) NOT NULL DEFAULT '',\n  `ha1b` varchar(128) NOT NULL DEFAULT '',\n  `email_address` varchar(128) DEFAULT NULL,\n  `rpid` varchar(128) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `account_idx` (`username`,`domain`),\n  KEY `username_idx` (`username`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `subscriber`\n--\n\nLOCK TABLES `subscriber` WRITE;\n/*!40000 ALTER TABLE `subscriber` DISABLE KEYS */;\n/*!40000 ALTER TABLE `subscriber` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `topos_d`\n--\n\nDROP TABLE IF EXISTS `topos_d`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `topos_d` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `rectime` datetime NOT NULL,\n  `s_method` varchar(64) NOT NULL DEFAULT '',\n  `s_cseq` varchar(64) NOT NULL DEFAULT '',\n  `a_callid` varchar(255) NOT NULL DEFAULT '',\n  `a_uuid` varchar(255) NOT NULL DEFAULT '',\n  `b_uuid` varchar(255) NOT NULL DEFAULT '',\n  `a_contact` varchar(128) NOT NULL DEFAULT '',\n  `b_contact` varchar(128) NOT NULL DEFAULT '',\n  `as_contact` varchar(128) NOT NULL DEFAULT '',\n  `bs_contact` varchar(128) NOT NULL DEFAULT '',\n  `a_tag` varchar(255) NOT NULL DEFAULT '',\n  `b_tag` varchar(255) NOT NULL DEFAULT '',\n  `a_rr` mediumtext,\n  `b_rr` mediumtext,\n  `s_rr` mediumtext,\n  `iflags` int(10) unsigned NOT NULL DEFAULT '0',\n  `a_uri` varchar(128) NOT NULL DEFAULT '',\n  `b_uri` varchar(128) NOT NULL DEFAULT '',\n  `r_uri` varchar(128) NOT NULL DEFAULT '',\n  `a_srcaddr` varchar(128) NOT NULL DEFAULT '',\n  `b_srcaddr` varchar(128) NOT NULL DEFAULT '',\n  `a_socket` varchar(128) NOT NULL DEFAULT '',\n  `b_socket` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  KEY `rectime_idx` (`rectime`),\n  KEY `a_callid_idx` (`a_callid`),\n  KEY `a_uuid_idx` (`a_uuid`),\n  KEY `b_uuid_idx` (`b_uuid`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `topos_d`\n--\n\nLOCK TABLES `topos_d` WRITE;\n/*!40000 ALTER TABLE `topos_d` DISABLE KEYS */;\n/*!40000 ALTER TABLE `topos_d` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `topos_t`\n--\n\nDROP TABLE IF EXISTS `topos_t`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `topos_t` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `rectime` datetime NOT NULL,\n  `s_method` varchar(64) NOT NULL DEFAULT '',\n  `s_cseq` varchar(64) NOT NULL DEFAULT '',\n  `a_callid` varchar(255) NOT NULL DEFAULT '',\n  `a_uuid` varchar(255) NOT NULL DEFAULT '',\n  `b_uuid` varchar(255) NOT NULL DEFAULT '',\n  `direction` int(11) NOT NULL DEFAULT '0',\n  `x_via` mediumtext,\n  `x_vbranch` varchar(255) NOT NULL DEFAULT '',\n  `x_rr` mediumtext,\n  `y_rr` mediumtext,\n  `s_rr` mediumtext,\n  `x_uri` varchar(128) NOT NULL DEFAULT '',\n  `a_contact` varchar(128) NOT NULL DEFAULT '',\n  `b_contact` varchar(128) NOT NULL DEFAULT '',\n  `as_contact` varchar(128) NOT NULL DEFAULT '',\n  `bs_contact` varchar(128) NOT NULL DEFAULT '',\n  `x_tag` varchar(255) NOT NULL DEFAULT '',\n  `a_tag` varchar(255) NOT NULL DEFAULT '',\n  `b_tag` varchar(255) NOT NULL DEFAULT '',\n  `a_srcaddr` varchar(128) NOT NULL DEFAULT '',\n  `b_srcaddr` varchar(128) NOT NULL DEFAULT '',\n  `a_socket` varchar(128) NOT NULL DEFAULT '',\n  `b_socket` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`),\n  KEY `rectime_idx` (`rectime`),\n  KEY `a_callid_idx` (`a_callid`),\n  KEY `x_vbranch_idx` (`x_vbranch`),\n  KEY `a_uuid_idx` (`a_uuid`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `topos_t`\n--\n\nLOCK TABLES `topos_t` WRITE;\n/*!40000 ALTER TABLE `topos_t` DISABLE KEYS */;\n/*!40000 ALTER TABLE `topos_t` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `trusted`\n--\n\nDROP TABLE IF EXISTS `trusted`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `trusted` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `src_ip` varchar(50) NOT NULL,\n  `proto` varchar(4) NOT NULL,\n  `from_pattern` varchar(64) DEFAULT NULL,\n  `ruri_pattern` varchar(64) DEFAULT NULL,\n  `tag` varchar(64) DEFAULT NULL,\n  `priority` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  KEY `peer_idx` (`src_ip`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `trusted`\n--\n\nLOCK TABLES `trusted` WRITE;\n/*!40000 ALTER TABLE `trusted` DISABLE KEYS */;\n/*!40000 ALTER TABLE `trusted` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uacreg`\n--\n\nDROP TABLE IF EXISTS `uacreg`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uacreg` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `l_uuid` varchar(64) NOT NULL DEFAULT '',\n  `l_username` varchar(64) NOT NULL DEFAULT '',\n  `l_domain` varchar(64) NOT NULL DEFAULT '',\n  `r_username` varchar(64) NOT NULL DEFAULT '',\n  `r_domain` varchar(64) NOT NULL DEFAULT '',\n  `realm` varchar(64) NOT NULL DEFAULT '',\n  `auth_username` varchar(64) NOT NULL DEFAULT '',\n  `auth_password` varchar(64) NOT NULL DEFAULT '',\n  `auth_ha1` varchar(128) NOT NULL DEFAULT '',\n  `auth_proxy` varchar(128) NOT NULL DEFAULT '',\n  `expires` int(11) NOT NULL DEFAULT '0',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `reg_delay` int(11) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `l_uuid_idx` (`l_uuid`)\n) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uacreg`\n--\n\nLOCK TABLES `uacreg` WRITE;\n/*!40000 ALTER TABLE `uacreg` DISABLE KEYS */;\nINSERT INTO `uacreg` VALUES (1,'1','1','50.253.243.17','','','','','','','',60,1,0),(2,'2','2','50.253.243.17','','','','','','','',60,1,0),(3,'3','3','50.253.243.17','','','','','','','',60,1,0),(4,'4','4','50.253.243.17','','','','','','','',60,1,0),(5,'5','5','50.253.243.17','','','','','','','',60,1,0),(6,'6','6','50.253.243.17','','','','','','','',60,1,0),(7,'7','7','50.253.243.17','','','','','','','',60,1,0);\n/*!40000 ALTER TABLE `uacreg` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_credentials`\n--\n\nDROP TABLE IF EXISTS `uid_credentials`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_credentials` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `auth_username` varchar(64) NOT NULL,\n  `did` varchar(64) NOT NULL DEFAULT '_default',\n  `realm` varchar(64) NOT NULL,\n  `password` varchar(28) NOT NULL DEFAULT '',\n  `flags` int(11) NOT NULL DEFAULT '0',\n  `ha1` varchar(32) NOT NULL,\n  `ha1b` varchar(32) NOT NULL DEFAULT '',\n  `uid` varchar(64) NOT NULL,\n  PRIMARY KEY (`id`),\n  KEY `cred_idx` (`auth_username`,`did`),\n  KEY `uid` (`uid`),\n  KEY `did_idx` (`did`),\n  KEY `realm_idx` (`realm`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_credentials`\n--\n\nLOCK TABLES `uid_credentials` WRITE;\n/*!40000 ALTER TABLE `uid_credentials` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_credentials` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_domain`\n--\n\nDROP TABLE IF EXISTS `uid_domain`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_domain` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `did` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL,\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `domain_idx` (`domain`),\n  KEY `did_idx` (`did`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_domain`\n--\n\nLOCK TABLES `uid_domain` WRITE;\n/*!40000 ALTER TABLE `uid_domain` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_domain` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_domain_attrs`\n--\n\nDROP TABLE IF EXISTS `uid_domain_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_domain_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `did` varchar(64) DEFAULT NULL,\n  `name` varchar(32) NOT NULL,\n  `type` int(11) NOT NULL DEFAULT '0',\n  `value` varchar(128) DEFAULT NULL,\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `domain_attr_idx` (`did`,`name`,`value`),\n  KEY `domain_did` (`did`,`flags`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_domain_attrs`\n--\n\nLOCK TABLES `uid_domain_attrs` WRITE;\n/*!40000 ALTER TABLE `uid_domain_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_domain_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_global_attrs`\n--\n\nDROP TABLE IF EXISTS `uid_global_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_global_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `name` varchar(32) NOT NULL,\n  `type` int(11) NOT NULL DEFAULT '0',\n  `value` varchar(128) DEFAULT NULL,\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `global_attrs_idx` (`name`,`value`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_global_attrs`\n--\n\nLOCK TABLES `uid_global_attrs` WRITE;\n/*!40000 ALTER TABLE `uid_global_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_global_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_uri`\n--\n\nDROP TABLE IF EXISTS `uid_uri`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_uri` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `uid` varchar(64) NOT NULL,\n  `did` varchar(64) NOT NULL,\n  `username` varchar(64) NOT NULL,\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  `scheme` varchar(8) NOT NULL DEFAULT 'sip',\n  PRIMARY KEY (`id`),\n  KEY `uri_idx1` (`username`,`did`,`scheme`),\n  KEY `uri_uid` (`uid`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_uri`\n--\n\nLOCK TABLES `uid_uri` WRITE;\n/*!40000 ALTER TABLE `uid_uri` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_uri` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_uri_attrs`\n--\n\nDROP TABLE IF EXISTS `uid_uri_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_uri_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `did` varchar(64) NOT NULL,\n  `name` varchar(32) NOT NULL,\n  `value` varchar(128) DEFAULT NULL,\n  `type` int(11) NOT NULL DEFAULT '0',\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  `scheme` varchar(8) NOT NULL DEFAULT 'sip',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `uriattrs_idx` (`username`,`did`,`name`,`value`,`scheme`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_uri_attrs`\n--\n\nLOCK TABLES `uid_uri_attrs` WRITE;\n/*!40000 ALTER TABLE `uid_uri_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_uri_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uid_user_attrs`\n--\n\nDROP TABLE IF EXISTS `uid_user_attrs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uid_user_attrs` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `uid` varchar(64) NOT NULL,\n  `name` varchar(32) NOT NULL,\n  `value` varchar(128) DEFAULT NULL,\n  `type` int(11) NOT NULL DEFAULT '0',\n  `flags` int(10) unsigned NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `userattrs_idx` (`uid`,`name`,`value`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uid_user_attrs`\n--\n\nLOCK TABLES `uid_user_attrs` WRITE;\n/*!40000 ALTER TABLE `uid_user_attrs` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uid_user_attrs` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `uri`\n--\n\nDROP TABLE IF EXISTS `uri`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `uri` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `uri_user` varchar(64) NOT NULL DEFAULT '',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `account_idx` (`username`,`domain`,`uri_user`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `uri`\n--\n\nLOCK TABLES `uri` WRITE;\n/*!40000 ALTER TABLE `uri` DISABLE KEYS */;\n/*!40000 ALTER TABLE `uri` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `userblacklist`\n--\n\nDROP TABLE IF EXISTS `userblacklist`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `userblacklist` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL DEFAULT '',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `prefix` varchar(64) NOT NULL DEFAULT '',\n  `whitelist` tinyint(1) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  KEY `userblacklist_idx` (`username`,`domain`,`prefix`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `userblacklist`\n--\n\nLOCK TABLES `userblacklist` WRITE;\n/*!40000 ALTER TABLE `userblacklist` DISABLE KEYS */;\n/*!40000 ALTER TABLE `userblacklist` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `usr_preferences`\n--\n\nDROP TABLE IF EXISTS `usr_preferences`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `usr_preferences` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `uuid` varchar(64) NOT NULL DEFAULT '',\n  `username` varchar(128) NOT NULL DEFAULT '0',\n  `domain` varchar(64) NOT NULL DEFAULT '',\n  `attribute` varchar(32) NOT NULL DEFAULT '',\n  `type` int(11) NOT NULL DEFAULT '0',\n  `value` varchar(128) NOT NULL DEFAULT '',\n  `last_modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:01',\n  PRIMARY KEY (`id`),\n  KEY `ua_idx` (`uuid`,`attribute`),\n  KEY `uda_idx` (`username`,`domain`,`attribute`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `usr_preferences`\n--\n\nLOCK TABLES `usr_preferences` WRITE;\n/*!40000 ALTER TABLE `usr_preferences` DISABLE KEYS */;\n/*!40000 ALTER TABLE `usr_preferences` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `version`\n--\n\nDROP TABLE IF EXISTS `version`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `version` (\n  `table_name` varchar(32) NOT NULL,\n  `table_version` int(10) unsigned NOT NULL DEFAULT '0',\n  UNIQUE KEY `table_name_idx` (`table_name`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `version`\n--\n\nLOCK TABLES `version` WRITE;\n/*!40000 ALTER TABLE `version` DISABLE KEYS */;\nINSERT INTO `version` VALUES ('acc',5),('acc_cdrs',2),('active_watchers',12),('address',6),('aliases',8),('carrierfailureroute',2),('carrierroute',3),('carrier_name',1),('cpl',1),('dbaliases',1),('dialog',7),('dialog_vars',1),('dialplan',2),('dispatcher',4),('domain',2),('domainpolicy',2),('domain_attrs',1),('domain_name',1),('dr_gateways',3),('dr_groups',2),('dr_gw_lists',1),('dr_rules',3),('globalblacklist',1),('grp',2),('htable',2),('imc_members',1),('imc_rooms',1),('lcr_gw',3),('lcr_rule',3),('lcr_rule_target',1),('location',9),('location_attrs',1),('missed_calls',4),('mohqcalls',1),('mohqueues',1),('mtree',1),('mtrees',2),('pdt',1),('pl_pipes',1),('presentity',4),('pua',7),('purplemap',1),('re_grp',1),('rls_presentity',1),('rls_watchers',3),('rtpengine',1),('rtpproxy',1),('sca_subscriptions',2),('silo',8),('sip_trace',4),('speed_dial',2),('subscriber',7),('topos_d',1),('topos_t',1),('trusted',6),('uacreg',3),('uid_credentials',7),('uid_domain',2),('uid_domain_attrs',1),('uid_global_attrs',1),('uid_uri',3),('uid_uri_attrs',2),('uid_user_attrs',3),('uri',1),('userblacklist',1),('usr_preferences',2),('version',1),('watchers',3),('xcap',4);\n/*!40000 ALTER TABLE `version` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `watchers`\n--\n\nDROP TABLE IF EXISTS `watchers`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `watchers` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `presentity_uri` varchar(128) NOT NULL,\n  `watcher_username` varchar(64) NOT NULL,\n  `watcher_domain` varchar(64) NOT NULL,\n  `event` varchar(64) NOT NULL DEFAULT 'presence',\n  `status` int(11) NOT NULL,\n  `reason` varchar(64) DEFAULT NULL,\n  `inserted_time` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `watcher_idx` (`presentity_uri`,`watcher_username`,`watcher_domain`,`event`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `watchers`\n--\n\nLOCK TABLES `watchers` WRITE;\n/*!40000 ALTER TABLE `watchers` DISABLE KEYS */;\n/*!40000 ALTER TABLE `watchers` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `xcap`\n--\n\nDROP TABLE IF EXISTS `xcap`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `xcap` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(64) NOT NULL,\n  `domain` varchar(64) NOT NULL,\n  `doc` mediumblob NOT NULL,\n  `doc_type` int(11) NOT NULL,\n  `etag` varchar(64) NOT NULL,\n  `source` int(11) NOT NULL,\n  `doc_uri` varchar(255) NOT NULL,\n  `port` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `doc_uri_idx` (`doc_uri`),\n  KEY `account_doc_type_idx` (`username`,`domain`,`doc_type`),\n  KEY `account_doc_type_uri_idx` (`username`,`domain`,`doc_type`,`doc_uri`),\n  KEY `account_doc_uri_idx` (`username`,`domain`,`doc_uri`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `xcap`\n--\n\nLOCK TABLES `xcap` WRITE;\n/*!40000 ALTER TABLE `xcap` DISABLE KEYS */;\n/*!40000 ALTER TABLE `xcap` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Dumping events for database 'kamailio'\n--\n\n--\n-- Dumping routines for database 'kamailio'\n--\n/*!50003 DROP PROCEDURE IF EXISTS `kamailio_cdrs` */;\n/*!50003 SET @saved_cs_client      = @@character_set_client */ ;\n/*!50003 SET @saved_cs_results     = @@character_set_results */ ;\n/*!50003 SET @saved_col_connection = @@collation_connection */ ;\n/*!50003 SET character_set_client  = utf8 */ ;\n/*!50003 SET character_set_results = utf8 */ ;\n/*!50003 SET collation_connection  = utf8_general_ci */ ;\n/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;\n/*!50003 SET sql_mode              = '' */ ;\nDELIMITER ;;\nCREATE DEFINER=`root`@`localhost` PROCEDURE `kamailio_cdrs`()\nBEGIN\n  DECLARE done INT DEFAULT 0;\n  DECLARE bye_record INT DEFAULT 0;\n  DECLARE v_src_user,v_src_domain,v_dst_user,v_dst_domain,v_callid,v_from_tag,\n     v_to_tag,v_src_ip,v_calltype VARCHAR(64);\n  DECLARE v_inv_time, v_bye_time DATETIME;\n  DECLARE inv_cursor CURSOR FOR SELECT src_user, src_domain, dst_user,\n     dst_domain, time, callid,from_tag, to_tag, src_ip, calltype FROM acc\n     where method='INVITE' and cdr_id='0';\n  DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;\n  OPEN inv_cursor;\n  REPEAT\n    FETCH inv_cursor INTO v_src_user, v_src_domain, v_dst_user, v_dst_domain,\n            v_inv_time, v_callid, v_from_tag, v_to_tag, v_src_ip, v_calltype;\n    IF NOT done THEN\n      SET bye_record = 0;\n      SELECT 1, time INTO bye_record, v_bye_time FROM acc WHERE\n                 method='BYE' AND callid=v_callid AND ((from_tag=v_from_tag\n                 AND to_tag=v_to_tag)\n                 OR (from_tag=v_to_tag AND to_tag=v_from_tag))\n                 ORDER BY time ASC LIMIT 1;\n      IF bye_record = 1 THEN\n        INSERT INTO cdrs (src_username,src_domain,dst_username,\n                 dst_domain,call_start_time,duration,sip_call_id,sip_from_tag,\n                 sip_to_tag,src_ip,created,calltype) VALUES (v_src_user,v_src_domain,\n                 v_dst_user,v_dst_domain,v_inv_time,\n                 UNIX_TIMESTAMP(v_bye_time)-UNIX_TIMESTAMP(v_inv_time),\n                 v_callid,v_from_tag,v_to_tag,v_src_ip,NOW(),v_calltype);\n        UPDATE acc SET cdr_id=last_insert_id() WHERE callid=v_callid\n                 AND from_tag=v_from_tag AND to_tag=v_to_tag;\n      END IF;\n      SET done = 0;\n    END IF;\n  UNTIL done END REPEAT;\nEND ;;\nDELIMITER ;\n/*!50003 SET sql_mode              = @saved_sql_mode */ ;\n/*!50003 SET character_set_client  = @saved_cs_client */ ;\n/*!50003 SET character_set_results = @saved_cs_results */ ;\n/*!50003 SET collation_connection  = @saved_col_connection */ ;\n/*!50003 DROP PROCEDURE IF EXISTS `kamailio_rating` */;\n/*!50003 SET @saved_cs_client      = @@character_set_client */ ;\n/*!50003 SET @saved_cs_results     = @@character_set_results */ ;\n/*!50003 SET @saved_col_connection = @@collation_connection */ ;\n/*!50003 SET character_set_client  = latin1 */ ;\n/*!50003 SET character_set_results = latin1 */ ;\n/*!50003 SET collation_connection  = latin1_swedish_ci */ ;\n/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;\n/*!50003 SET sql_mode              = '' */ ;\nDELIMITER ;;\nCREATE DEFINER=`kamailio`@`localhost` PROCEDURE `kamailio_rating`(`rgroup` varchar(64))\nBEGIN\n  DECLARE done, rate_record, vx_cost INT DEFAULT 0;\n  DECLARE v_cdr_id BIGINT DEFAULT 0;\n  DECLARE v_duration, v_rate_unit, v_time_unit INT DEFAULT 0;\n  DECLARE v_dst_username VARCHAR(64);\n  DECLARE cdrs_cursor CURSOR FOR SELECT cdr_id, dst_username, duration\n     FROM cdrs WHERE rated=0;\n  DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;\n  OPEN cdrs_cursor;\n  REPEAT\n    FETCH cdrs_cursor INTO v_cdr_id, v_dst_username, v_duration;\n    IF NOT done THEN\n      SET rate_record = 0;\n      SELECT 1, rate_unit, time_unit INTO rate_record, v_rate_unit, v_time_unit\n             FROM billing_rates\n             WHERE rate_group=rgroup AND v_dst_username LIKE concat(prefix, '%')\n             ORDER BY prefix DESC LIMIT 1;\n      IF rate_record = 1 THEN\n        SET vx_cost = v_rate_unit * CEIL(v_duration/v_time_unit);\n        UPDATE cdrs SET rated=1, cost=vx_cost WHERE cdr_id=v_cdr_id;\n      END IF;\n      SET done = 0;\n    END IF;\n  UNTIL done END REPEAT;\nEND ;;\nDELIMITER ;\n/*!50003 SET sql_mode              = @saved_sql_mode */ ;\n/*!50003 SET character_set_client  = @saved_cs_client */ ;\n/*!50003 SET character_set_results = @saved_cs_results */ ;\n/*!50003 SET collation_connection  = @saved_col_connection */ ;\n\n--\n-- Final view structure for view `dsip_prefix_mapping`\n--\n\n/*!50001 DROP TABLE IF EXISTS `dsip_prefix_mapping`*/;\n/*!50001 DROP VIEW IF EXISTS `dsip_prefix_mapping`*/;\n/*!50001 SET @saved_cs_client          = @@character_set_client */;\n/*!50001 SET @saved_cs_results         = @@character_set_results */;\n/*!50001 SET @saved_col_connection     = @@collation_connection */;\n/*!50001 SET character_set_client      = utf8 */;\n/*!50001 SET character_set_results     = utf8 */;\n/*!50001 SET collation_connection      = utf8mb4_general_ci */;\n/*!50001 CREATE ALGORITHM=UNDEFINED */\n/*!50013 DEFINER=`root`@`localhost` SQL SECURITY DEFINER */\n/*!50001 VIEW `dsip_prefix_mapping` AS select `dr_rules`.`prefix` AS `prefix`,cast(`dr_rules`.`ruleid` as char(64) charset utf8mb4) AS `ruleid`,'0' AS `key_type`,'0' AS `value_type` from `dr_rules` where (`dr_rules`.`groupid` = (select `dsip_settings`.`FLT_INBOUND` from `dsip_settings` where `DSIP_ID` = 1 limit 1)) */;\n/*!50001 SET character_set_client      = @saved_cs_client */;\n/*!50001 SET character_set_results     = @saved_cs_results */;\n/*!50001 SET collation_connection      = @saved_col_connection */;\n/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;\n\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n\n-- Dump completed on 2019-09-20 14:59:18\n"
  }
]